import * as path from 'path';
import { tmpdir } from 'os';

// Mock runCmd to prevent actual package manager execution (used only in Nx scaffold path)
jest.mock('@frontmcp/utils', () => {
  const actual = jest.requireActual('@frontmcp/utils');
  return { ...actual, runCmd: jest.fn().mockResolvedValue(undefined) };
});

// Mock createRequire for Nx scaffold tests (loads nx from installed location)
jest.mock('module', () => {
  const actual = jest.requireActual('module');
  return { ...actual, createRequire: jest.fn() };
});

// Mock child_process for git init tests
const mockExecSync = jest.fn();
jest.mock('child_process', () => ({
  ...jest.requireActual('child_process'),
  execSync: mockExecSync,
}));

import { runCreate } from '../create';
import { runCmd, mkdtemp, mkdir, rm, readFileSync, writeFile, fileExists } from '@frontmcp/utils';
import { createRequire } from 'module';

// Capture console output during tests
let consoleLogs: string[] = [];

describe('runCreate', () => {
  let tempDir: string;
  let originalCwd: string;

  beforeEach(async () => {
    consoleLogs = [];
    jest.spyOn(console, 'log').mockImplementation((...args) => {
      consoleLogs.push(args.join(' '));
    });
    jest.spyOn(console, 'error').mockImplementation((...args) => {
      consoleLogs.push(args.join(' '));
    });

    // Create temp directory for testing
    tempDir = await mkdtemp(path.join(tmpdir(), 'cli-test-'));
    originalCwd = process.cwd();

    // Mock process.chdir to track directory changes
    jest.spyOn(process, 'chdir').mockImplementation(() => {
      // No-op for testing
    });

    // Mock process.exit
    jest.spyOn(process, 'exit').mockImplementation((() => {
      throw new Error('process.exit called');
    }) as never);

    // Set stdin.isTTY to false for non-interactive mode
    Object.defineProperty(process.stdin, 'isTTY', {
      value: false,
      writable: true,
      configurable: true,
    });
  });

  afterEach(async () => {
    jest.restoreAllMocks();
    process.chdir(originalCwd);

    // Clean up temp directory
    try {
      await rm(tempDir, { recursive: true, force: true });
    } catch {
      // Ignore cleanup errors
    }
  });

  describe('non-interactive mode (--yes flag)', () => {
    beforeEach(() => {
      // Override process.cwd to return temp directory
      jest.spyOn(process, 'cwd').mockReturnValue(tempDir);
    });

    it('should use default project name when not provided in non-interactive mode', async () => {
      // When no project name is provided, it defaults to 'frontmcp-app'
      await runCreate(undefined, { yes: true });

      expect(consoleLogs.some((log) => log.includes('frontmcp-app'))).toBe(true);
    });

    it('should use default options when --yes flag is set', async () => {
      await runCreate('test-project', { yes: true });

      // Check that project was created
      expect(consoleLogs.some((log) => log.includes('test-project'))).toBe(true);
      expect(consoleLogs.some((log) => log.includes('Deployment: node'))).toBe(true);
      expect(consoleLogs.some((log) => log.includes('Redis: docker'))).toBe(true);
      expect(consoleLogs.some((log) => log.includes('GitHub Actions: Yes'))).toBe(true);
    });

    it('should override deployment target with --target flag', async () => {
      await runCreate('vercel-project', { yes: true, target: 'vercel' });

      expect(consoleLogs.some((log) => log.includes('Deployment: vercel'))).toBe(true);
    });

    it('should override Redis setup with --redis flag', async () => {
      await runCreate('no-redis-project', { yes: true, redis: 'none' });

      expect(consoleLogs.some((log) => log.includes('Redis: none'))).toBe(true);
    });

    it('should disable GitHub Actions with --no-cicd flag', async () => {
      await runCreate('no-cicd-project', { yes: true, cicd: false });

      expect(consoleLogs.some((log) => log.includes('GitHub Actions: No'))).toBe(true);
    });

    it('should create Docker target files', async () => {
      await runCreate('docker-project', { yes: true, target: 'node' });

      // Verify Docker-specific logs
      expect(consoleLogs.some((log) => log.includes('ci/Dockerfile') || log.includes('Dockerfile'))).toBe(true);
      expect(
        consoleLogs.some((log) => log.includes('ci/docker-compose.yml') || log.includes('docker-compose.yml')),
      ).toBe(true);
    });

    it('should create Vercel target files', async () => {
      await runCreate('vercel-project', { yes: true, target: 'vercel' });

      // Verify Vercel-specific logs
      expect(consoleLogs.some((log) => log.includes('vercel.json'))).toBe(true);
    });

    it('should create Lambda target files', async () => {
      await runCreate('lambda-project', { yes: true, target: 'lambda' });

      // Verify Lambda-specific logs
      expect(consoleLogs.some((log) => log.includes('template.yaml'))).toBe(true);
    });

    it('should create Cloudflare target files', async () => {
      await runCreate('cloudflare-project', { yes: true, target: 'cloudflare' });

      // Verify Cloudflare-specific logs
      expect(consoleLogs.some((log) => log.includes('wrangler.toml'))).toBe(true);
    });
  });

  describe('file scaffolding', () => {
    beforeEach(() => {
      jest.spyOn(process, 'cwd').mockReturnValue(tempDir);
    });

    it('should create src directory with main.ts', async () => {
      await runCreate('my-app', { yes: true });

      expect(consoleLogs.some((log) => log.includes('src/main.ts'))).toBe(true);
    });

    it('should create e2e directory with test file', async () => {
      await runCreate('my-app', { yes: true });

      expect(consoleLogs.some((log) => log.includes('e2e/server.e2e.spec.ts'))).toBe(true);
    });

    it('should create CLAUDE.md and AGENTS.md for AI coding assistants', async () => {
      await runCreate('my-app', { yes: true });

      expect(consoleLogs.some((log) => log.includes('CLAUDE.md'))).toBe(true);
      expect(consoleLogs.some((log) => log.includes('AGENTS.md'))).toBe(true);
    });

    it('should create package.json', async () => {
      await runCreate('my-app', { yes: true });

      expect(consoleLogs.some((log) => log.includes('package.json'))).toBe(true);
    });

    it('should create .gitignore', async () => {
      await runCreate('my-app', { yes: true });

      expect(consoleLogs.some((log) => log.includes('.gitignore'))).toBe(true);
    });

    it('should create .dockerignore for Docker target', async () => {
      await runCreate('docker-ignore-app', { yes: true, target: 'node' });

      expect(consoleLogs.some((log) => log.includes('.dockerignore'))).toBe(true);
    });

    it('should not create .dockerignore for non-Docker targets', async () => {
      await runCreate('vercel-no-dockerignore', { yes: true, target: 'vercel' });

      const base = path.join(tempDir, 'vercel-no-dockerignore');
      expect(await fileExists(path.join(base, '.dockerignore'))).toBe(false);
    });

    it('should create README.md', async () => {
      await runCreate('my-app', { yes: true });

      expect(consoleLogs.some((log) => log.includes('README.md'))).toBe(true);
    });

    it('should create .nvmrc with Node.js 24', async () => {
      await runCreate('nvmrc-app', { yes: true });

      const nvmrcPath = path.join(tempDir, 'nvmrc-app', '.nvmrc');
      expect(await fileExists(nvmrcPath)).toBe(true);
      expect(readFileSync(nvmrcPath, 'utf8').trim()).toBe('24');
    });

    it('should use parent-relative paths in ci/docker-compose.yml for node target', async () => {
      await runCreate('docker-paths-app', { yes: true, target: 'node' });

      const composePath = path.join(tempDir, 'docker-paths-app', 'ci', 'docker-compose.yml');
      const content = readFileSync(composePath, 'utf8');

      expect(content).toContain('context: ..');
      expect(content).not.toMatch(/context:\s*\.(\s|$)/);
    });
  });

  describe('GitHub Actions', () => {
    beforeEach(() => {
      jest.spyOn(process, 'cwd').mockReturnValue(tempDir);
    });

    it('should create GitHub Actions workflows when enabled', async () => {
      await runCreate('my-app', { yes: true, cicd: true });

      expect(consoleLogs.some((log) => log.includes('ci.yml'))).toBe(true);
      expect(consoleLogs.some((log) => log.includes('e2e.yml'))).toBe(true);
      expect(consoleLogs.some((log) => log.includes('deploy.yml'))).toBe(true);
    });

    it('should skip GitHub Actions workflows when disabled', async () => {
      await runCreate('my-app', { yes: true, cicd: false });

      // These files should NOT be created
      const ciYmlLog = consoleLogs.find(
        (log) => log.includes('ci.yml') && (log.includes('created') || log.includes('✓')),
      );
      expect(ciYmlLog).toBeUndefined();
    });
  });

  describe('Docker file scaffolding (target: node)', () => {
    beforeEach(() => {
      jest.spyOn(process, 'cwd').mockReturnValue(tempDir);
    });

    describe('ci/Dockerfile', () => {
      it('should scaffold a multi-stage Dockerfile for Docker builds', async () => {
        await runCreate('docker-app', { yes: true, target: 'node' });

        const content = readFileSync(path.join(tempDir, 'docker-app', 'ci', 'Dockerfile'), 'utf8');

        // Multi-stage build
        expect(content).toContain('AS builder');
        expect(content).toContain('AS runner');

        // Base image
        expect(content).toContain('FROM node:24-slim AS builder');
        expect(content).toContain('FROM node:24-slim AS runner');

        // Builder stage
        expect(content).toContain('RUN npm ci');
        expect(content).toContain('COPY . .');
        expect(content).toContain('RUN npm run build');

        // Runner stage
        expect(content).toContain('ENV NODE_ENV=production');
        expect(content).toContain('COPY --from=builder /app/node_modules ./node_modules');
        expect(content).toContain('COPY --from=builder /app/dist ./dist');
        expect(content).toContain('COPY --from=builder /app/package.json ./');

        // Builder prunes devDeps before copy
        expect(content).toContain('npm prune --omit=dev');

        // Entrypoint
        expect(content).toContain('EXPOSE 3000');
        expect(content).toContain('CMD ["node", "dist/main.js"]');
      });
    });

    describe('ci/docker-compose.yml with redis: docker', () => {
      it('should configure Redis service with healthcheck and app with correct paths', async () => {
        await runCreate('compose-redis-app', { yes: true, target: 'node', redis: 'docker' });

        const content = readFileSync(path.join(tempDir, 'compose-redis-app', 'ci', 'docker-compose.yml'), 'utf8');

        // Redis service
        expect(content).toContain('image: redis:7-alpine');
        expect(content).toContain("'6379:6379'");
        expect(content).toContain('redis-data:/data');
        expect(content).toContain('redis-cli');

        // App service
        expect(content).toContain('context: ..');
        expect(content).toContain('dockerfile: ci/Dockerfile');
        expect(content).toContain('${PORT:-3000}:3000');

        // No source mounting or dev command override in production-oriented compose
        expect(content).not.toContain('../src:/app/src');
        expect(content).not.toMatch(/command:.*run\s+dev/);

        // No deprecated version field
        expect(content).not.toMatch(/^version:/m);

        // Dependencies
        expect(content).toContain('condition: service_healthy');

        // Environment
        expect(content).toContain('PORT=${PORT:-3000}');
        expect(content).toContain('REDIS_HOST=redis');
        expect(content).toContain('REDIS_PORT=6379');
      });
    });

    describe('ci/docker-compose.yml without Redis', () => {
      it('should have only app service when redis is none', async () => {
        await runCreate('compose-no-redis', { yes: true, target: 'node', redis: 'none' });

        const content = readFileSync(path.join(tempDir, 'compose-no-redis', 'ci', 'docker-compose.yml'), 'utf8');

        expect(content).toContain('app:');
        expect(content).toContain('context: ..');
        expect(content).toContain('dockerfile: ci/Dockerfile');
        expect(content).not.toContain('../src:/app/src');
        expect(content).not.toMatch(/command:.*run\s+dev/);

        expect(content).not.toContain('image: redis');
        expect(content).not.toContain('depends_on:');
        expect(content).not.toContain('REDIS_HOST');
        expect(content).not.toMatch(/^\s+redis:\s*$/m);
      });

      it('should use no-Redis compose template when redis is existing', async () => {
        await runCreate('compose-existing-redis', { yes: true, target: 'node', redis: 'existing' });

        const content = readFileSync(path.join(tempDir, 'compose-existing-redis', 'ci', 'docker-compose.yml'), 'utf8');

        expect(content).toContain('app:');
        expect(content).toContain('context: ..');
        expect(content).toContain('dockerfile: ci/Dockerfile');

        expect(content).not.toContain('image: redis');
        expect(content).not.toContain('depends_on:');
        expect(content).not.toContain('REDIS_HOST');
      });
    });

    describe('ci/.env.docker', () => {
      it('should contain Docker-specific environment variables', async () => {
        await runCreate('env-docker-app', { yes: true, target: 'node' });

        const content = readFileSync(path.join(tempDir, 'env-docker-app', 'ci', '.env.docker'), 'utf8');

        expect(content).toContain('REDIS_HOST=redis');
        expect(content).toContain('PORT=3000');
        expect(content).toContain('NODE_ENV=development');
        expect(content).toContain('REDIS_PORT=6379');
      });
    });

    describe('deploy.yml and package.json docker scripts', () => {
      it('should generate Docker deploy workflow for node target with cicd enabled', async () => {
        await runCreate('deploy-docker-app', { yes: true, target: 'node', cicd: true });

        const content = readFileSync(
          path.join(tempDir, 'deploy-docker-app', '.github', 'workflows', 'deploy.yml'),
          'utf8',
        );

        expect(content).toContain('ghcr.io');
        expect(content).toContain('docker/build-push-action');
        expect(content).toContain('file: ./ci/Dockerfile');
      });

      it('should include docker:up/down/build scripts in package.json', async () => {
        await runCreate('docker-scripts-app', { yes: true, target: 'node' });

        const pkgJson = JSON.parse(readFileSync(path.join(tempDir, 'docker-scripts-app', 'package.json'), 'utf8'));

        expect(pkgJson.scripts['docker:up']).toBe('docker compose -f ci/docker-compose.yml up');
        expect(pkgJson.scripts['docker:down']).toBe('docker compose -f ci/docker-compose.yml down');
        expect(pkgJson.scripts['docker:build']).toBe('docker compose -f ci/docker-compose.yml build');
      });
    });

    describe('Docker files excluded for non-node targets', () => {
      it('should not create Docker files for vercel target', async () => {
        await runCreate('vercel-no-docker', { yes: true, target: 'vercel' });

        const base = path.join(tempDir, 'vercel-no-docker');
        expect(await fileExists(path.join(base, 'ci', 'Dockerfile'))).toBe(false);
        expect(await fileExists(path.join(base, 'ci', 'docker-compose.yml'))).toBe(false);
        expect(await fileExists(path.join(base, 'ci', '.env.docker'))).toBe(false);
      });

      it('should not create Docker files for lambda target', async () => {
        await runCreate('lambda-no-docker', { yes: true, target: 'lambda' });

        const base = path.join(tempDir, 'lambda-no-docker');
        expect(await fileExists(path.join(base, 'ci', 'Dockerfile'))).toBe(false);
        expect(await fileExists(path.join(base, 'ci', 'docker-compose.yml'))).toBe(false);
        expect(await fileExists(path.join(base, 'ci', '.env.docker'))).toBe(false);
      });

      it('should not create Docker files for cloudflare target', async () => {
        await runCreate('cloudflare-no-docker', { yes: true, target: 'cloudflare' });

        const base = path.join(tempDir, 'cloudflare-no-docker');
        expect(await fileExists(path.join(base, 'ci', 'Dockerfile'))).toBe(false);
        expect(await fileExists(path.join(base, 'ci', 'docker-compose.yml'))).toBe(false);
        expect(await fileExists(path.join(base, 'ci', '.env.docker'))).toBe(false);
      });

      it('should not include docker scripts in package.json for non-node targets', async () => {
        await runCreate('vercel-no-scripts', { yes: true, target: 'vercel' });

        const pkgJson = JSON.parse(readFileSync(path.join(tempDir, 'vercel-no-scripts', 'package.json'), 'utf8'));

        expect(pkgJson.scripts['docker:up']).toBeUndefined();
        expect(pkgJson.scripts['docker:down']).toBeUndefined();
        expect(pkgJson.scripts['docker:build']).toBeUndefined();
      });
    });
  });

  describe('git initialization', () => {
    beforeEach(() => {
      jest.spyOn(process, 'cwd').mockReturnValue(tempDir);
      mockExecSync.mockReset();
      mockExecSync.mockReturnValue(Buffer.from(''));
    });

    it('should initialize git repository after scaffolding', async () => {
      await runCreate('git-init-app', { yes: true });

      expect(mockExecSync).toHaveBeenCalledWith('git init', expect.objectContaining({ stdio: 'ignore' }));
      expect(mockExecSync).toHaveBeenCalledWith('git add -A', expect.objectContaining({ stdio: 'ignore' }));
      expect(mockExecSync).toHaveBeenCalledWith(
        'git commit -m "Initial commit"',
        expect.objectContaining({ stdio: 'ignore' }),
      );
    });

    it('should log success message after git init', async () => {
      await runCreate('git-msg-app', { yes: true });

      expect(consoleLogs.some((log) => log.includes('Initialized git repository'))).toBe(true);
    });

    it('should silently skip git init when git is not available', async () => {
      mockExecSync.mockImplementation(() => {
        throw new Error('git: command not found');
      });

      // Should not throw — git init failure is silently ignored
      await runCreate('no-git-app', { yes: true });

      // Should NOT have the success message
      expect(consoleLogs.some((log) => log.includes('Initialized git repository'))).toBe(false);
    });
  });

  describe('docker-compose selective rebuild instructions', () => {
    beforeEach(() => {
      jest.spyOn(process, 'cwd').mockReturnValue(tempDir);
    });

    it('should include selective rebuild comment in docker-compose with Redis', async () => {
      await runCreate('rebuild-redis-app', { yes: true, target: 'node', redis: 'docker' });

      const content = readFileSync(path.join(tempDir, 'rebuild-redis-app', 'ci', 'docker-compose.yml'), 'utf8');
      expect(content).toContain('Selective rebuild');
      expect(content).toContain('docker compose');
      expect(content).toContain('--build app');
    });

    it('should include selective rebuild comment in docker-compose without Redis', async () => {
      await runCreate('rebuild-no-redis-app', { yes: true, target: 'node', redis: 'none' });

      const content = readFileSync(path.join(tempDir, 'rebuild-no-redis-app', 'ci', 'docker-compose.yml'), 'utf8');
      expect(content).toContain('Selective rebuild');
      expect(content).toContain('--build app');
    });
  });

  describe('E2E test template', () => {
    beforeEach(() => {
      jest.spyOn(process, 'cwd').mockReturnValue(tempDir);
    });

    it('should include frontmcp test comment in E2E test template', async () => {
      await runCreate('e2e-template-app', { yes: true });

      const content = readFileSync(path.join(tempDir, 'e2e-template-app', 'e2e', 'server.e2e.spec.ts'), 'utf8');
      expect(content).toContain('frontmcp test');
    });

    it('should include resource listing test in E2E template', async () => {
      await runCreate('e2e-resources-app', { yes: true });

      const content = readFileSync(path.join(tempDir, 'e2e-resources-app', 'e2e', 'server.e2e.spec.ts'), 'utf8');
      expect(content).toContain('resources.list()');
    });
  });

  describe('package manager support', () => {
    beforeEach(() => {
      jest.spyOn(process, 'cwd').mockReturnValue(tempDir);
    });

    it('should default to npm package manager', async () => {
      await runCreate('default-pm-app', { yes: true, target: 'node' });

      expect(consoleLogs.some((log) => log.includes('Package manager: npm'))).toBe(true);
    });

    describe('Dockerfile per package manager', () => {
      it('should generate npm Dockerfile with npm ci', async () => {
        await runCreate('npm-docker', { yes: true, target: 'node', pm: 'npm' });

        const content = readFileSync(path.join(tempDir, 'npm-docker', 'ci', 'Dockerfile'), 'utf8');
        expect(content).toContain('RUN npm ci');
        expect(content).toContain('COPY package*.json package-lock.json* ./');
        expect(content).toContain('RUN npm run build');
        expect(content).not.toContain('corepack');
      });

      it('should generate yarn Dockerfile with yarn install --frozen-lockfile', async () => {
        await runCreate('yarn-docker', { yes: true, target: 'node', pm: 'yarn' });

        const content = readFileSync(path.join(tempDir, 'yarn-docker', 'ci', 'Dockerfile'), 'utf8');
        expect(content).toContain('RUN yarn install --frozen-lockfile');
        expect(content).toContain('COPY package.json yarn.lock* ./');
        expect(content).toContain('RUN yarn build');
        expect(content).toContain('RUN corepack enable');
      });

      it('should generate pnpm Dockerfile with corepack enable and pnpm install', async () => {
        await runCreate('pnpm-docker', { yes: true, target: 'node', pm: 'pnpm' });

        const content = readFileSync(path.join(tempDir, 'pnpm-docker', 'ci', 'Dockerfile'), 'utf8');
        expect(content).toContain('RUN corepack enable');
        expect(content).toContain('RUN pnpm install --frozen-lockfile');
        expect(content).toContain('COPY package.json pnpm-lock.yaml* ./');
        expect(content).toContain('RUN pnpm run build');
      });
    });

    describe('GitHub Actions per package manager', () => {
      it('should generate CI workflow with yarn cache', async () => {
        await runCreate('yarn-ci', { yes: true, target: 'node', pm: 'yarn', cicd: true });

        const content = readFileSync(path.join(tempDir, 'yarn-ci', '.github', 'workflows', 'ci.yml'), 'utf8');
        expect(content).toContain("cache: 'yarn'");
        expect(content).toContain('yarn install --frozen-lockfile');
        expect(content).toContain("node-version: '24'");
      });

      it('should generate CI workflow with pnpm setup action', async () => {
        await runCreate('pnpm-ci', { yes: true, target: 'node', pm: 'pnpm', cicd: true });

        const content = readFileSync(path.join(tempDir, 'pnpm-ci', '.github', 'workflows', 'ci.yml'), 'utf8');
        expect(content).toContain('pnpm/action-setup@v4');
        expect(content).toContain("cache: 'pnpm'");
        expect(content).toContain('pnpm install --frozen-lockfile');
      });

      it('should generate CI workflow with npm cache', async () => {
        await runCreate('npm-ci', { yes: true, target: 'node', pm: 'npm', cicd: true });

        const content = readFileSync(path.join(tempDir, 'npm-ci', '.github', 'workflows', 'ci.yml'), 'utf8');
        expect(content).toContain("cache: 'npm'");
        expect(content).toContain('npm ci');
        expect(content).toContain("node-version: '24'");
      });
    });

    describe('package.json engines per package manager', () => {
      it('should set node>=24 and npm>=10 for npm', async () => {
        await runCreate('npm-engines', { yes: true, pm: 'npm' });

        const pkgJson = JSON.parse(readFileSync(path.join(tempDir, 'npm-engines', 'package.json'), 'utf8'));
        expect(pkgJson.engines.node).toBe('>=24');
        expect(pkgJson.engines.npm).toBe('>=10');
      });

      it('should set only node>=24 for yarn (no npm engine)', async () => {
        await runCreate('yarn-engines', { yes: true, pm: 'yarn' });

        const pkgJson = JSON.parse(readFileSync(path.join(tempDir, 'yarn-engines', 'package.json'), 'utf8'));
        expect(pkgJson.engines.node).toBe('>=24');
        expect(pkgJson.engines.npm).toBeUndefined();
      });

      it('should set only node>=24 for pnpm (no npm engine)', async () => {
        await runCreate('pnpm-engines', { yes: true, pm: 'pnpm' });

        const pkgJson = JSON.parse(readFileSync(path.join(tempDir, 'pnpm-engines', 'package.json'), 'utf8'));
        expect(pkgJson.engines.node).toBe('>=24');
        expect(pkgJson.engines.npm).toBeUndefined();
      });
    });
  });

  describe('project name sanitization', () => {
    beforeEach(() => {
      jest.spyOn(process, 'cwd').mockReturnValue(tempDir);
    });

    it('should sanitize project name for folder', async () => {
      await runCreate('My Project Name', { yes: true });

      // Should be converted to lowercase with dashes
      expect(consoleLogs.some((log) => log.includes('my-project-name'))).toBe(true);
    });

    it('should handle scoped npm package names', async () => {
      await runCreate('@myorg/my-package', { yes: true });

      // Folder should be just the package name (without scope)
      expect(consoleLogs.some((log) => log.includes('my-package'))).toBe(true);
    });
  });
});

describe('helper functions', () => {
  // These tests import and test the internal helper functions
  // Since they're not exported, we test them indirectly through runCreate

  describe('sanitizeForFolder', () => {
    beforeEach(async () => {
      const tempDir = await mkdtemp(path.join(tmpdir(), 'cli-test-'));
      jest.spyOn(process, 'cwd').mockReturnValue(tempDir);
      jest.spyOn(process, 'chdir').mockImplementation(() => {
        // No-op for testing
      });
      jest.spyOn(process, 'exit').mockImplementation(((code?: number) => {
        throw new Error(`process.exit(${code})`);
      }) as () => never);
      Object.defineProperty(process.stdin, 'isTTY', { value: false, writable: true, configurable: true });
    });

    afterEach(() => {
      jest.restoreAllMocks();
    });

    it('should convert uppercase to lowercase', async () => {
      const logs: string[] = [];
      jest.spyOn(console, 'log').mockImplementation((...args) => logs.push(args.join(' ')));

      await runCreate('MyProject', { yes: true });

      expect(logs.some((log) => log.includes('myproject'))).toBe(true);
    });

    it('should replace special characters with dashes', async () => {
      const logs: string[] = [];
      jest.spyOn(console, 'log').mockImplementation((...args) => logs.push(args.join(' ')));

      await runCreate('my@project!name', { yes: true });

      expect(logs.some((log) => log.includes('my-project-name'))).toBe(true);
    });
  });
});

describe('Nx scaffold (--nx flag)', () => {
  let tempDir: string;
  let consoleLogs: string[] = [];
  const mockedCreateRequire = jest.mocked(createRequire);
  const mockedRunCmd = jest.mocked(runCmd);

  let mockWorkspaceGenerator: jest.Mock;
  let mockFlushChanges: jest.Mock;
  let mockFsTree: jest.Mock;

  beforeEach(async () => {
    consoleLogs = [];
    jest.spyOn(console, 'log').mockImplementation((...args) => {
      consoleLogs.push(args.join(' '));
    });
    jest.spyOn(console, 'error').mockImplementation((...args) => {
      consoleLogs.push(args.join(' '));
    });

    tempDir = await mkdtemp(path.join(tmpdir(), 'cli-nx-test-'));
    jest.spyOn(process, 'cwd').mockReturnValue(tempDir);
    jest.spyOn(process, 'chdir').mockImplementation(() => {});
    jest.spyOn(process, 'exit').mockImplementation((() => {
      throw new Error('process.exit called');
    }) as never);

    Object.defineProperty(process.stdin, 'isTTY', {
      value: false,
      writable: true,
      configurable: true,
    });

    // Set up Nx mocks
    mockWorkspaceGenerator = jest.fn().mockResolvedValue(jest.fn());
    mockFlushChanges = jest.fn();
    mockFsTree = jest.fn().mockImplementation((root: string) => ({
      root,
      listChanges: jest.fn().mockReturnValue([]),
    }));

    const mockLocalRequire = jest.fn().mockImplementation((specifier: string) => {
      if (specifier === 'nx/src/generators/tree.js') {
        return { FsTree: mockFsTree, flushChanges: mockFlushChanges };
      }
      if (specifier === '@frontmcp/nx') {
        return { workspaceGenerator: mockWorkspaceGenerator };
      }
      throw new Error(`Cannot find module '${specifier}'`);
    });

    mockedCreateRequire.mockReturnValue(mockLocalRequire as unknown as NodeRequire);
    mockedRunCmd.mockResolvedValue(undefined);
  });

  afterEach(async () => {
    jest.restoreAllMocks();
    mockedCreateRequire.mockReset();
    mockedRunCmd.mockReset();

    try {
      await rm(tempDir, { recursive: true, force: true });
    } catch {
      // Ignore cleanup errors
    }
  });

  it('should create bootstrap package.json with nx dependencies', async () => {
    await runCreate('my-nx-app', { nx: true });

    const bootstrapPath = path.join(tempDir, 'my-nx-app', 'package.json');
    expect(await fileExists(bootstrapPath)).toBe(true);
    const pkg = JSON.parse(readFileSync(bootstrapPath, 'utf8'));
    expect(pkg.devDependencies['nx']).toBe('22.3.3');
    expect(pkg.devDependencies['@nx/devkit']).toBe('22.3.3');
    expect(pkg.devDependencies['@frontmcp/nx']).toBeDefined();
  });

  it('should run package manager install in project directory', async () => {
    await runCreate('my-nx-app', { nx: true });

    expect(mockedRunCmd).toHaveBeenCalledWith(
      'npm',
      ['install'],
      expect.objectContaining({ cwd: path.join(tempDir, 'my-nx-app') }),
    );
  });

  it('should use createRequire to load nx from installed location', async () => {
    await runCreate('my-nx-app', { nx: true });

    expect(mockedCreateRequire).toHaveBeenCalledWith(path.join(tempDir, 'my-nx-app', 'package.json'));
  });

  it('should call workspaceGenerator with skipInstall: true', async () => {
    await runCreate('my-nx-app', { nx: true });

    expect(mockWorkspaceGenerator).toHaveBeenCalledWith(
      expect.anything(),
      expect.objectContaining({
        name: 'my-nx-app',
        skipInstall: true,
        createSampleApp: true,
      }),
    );
  });

  it('should run install twice (bootstrap + full)', async () => {
    await runCreate('my-nx-app', { nx: true });

    expect(mockedRunCmd).toHaveBeenCalledTimes(2);
  });

  it('should use specified package manager for install', async () => {
    await runCreate('my-nx-app', { nx: true, pm: 'yarn' });

    expect(mockedRunCmd).toHaveBeenCalledWith(
      'yarn',
      ['install'],
      expect.objectContaining({ cwd: path.join(tempDir, 'my-nx-app') }),
    );
  });

  it('should use pnpm when specified', async () => {
    await runCreate('my-nx-app', { nx: true, pm: 'pnpm' });

    expect(mockedRunCmd).toHaveBeenCalledWith(
      'pnpm',
      ['install'],
      expect.objectContaining({ cwd: path.join(tempDir, 'my-nx-app') }),
    );
  });

  it('should sanitize project name for directory', async () => {
    await runCreate('My Nx Project', { nx: true });

    expect(mockedRunCmd).toHaveBeenCalledWith(
      'npm',
      ['install'],
      expect.objectContaining({ cwd: path.join(tempDir, 'my-nx-project') }),
    );
  });

  it('should show success message on completion', async () => {
    await runCreate('my-nx-app', { nx: true });

    expect(consoleLogs.some((log) => log.includes('Nx monorepo created'))).toBe(true);
    expect(consoleLogs.some((log) => log.includes('my-nx-app'))).toBe(true);
  });

  it('should show next steps after successful scaffold', async () => {
    await runCreate('my-nx-app', { nx: true });

    expect(consoleLogs.some((log) => log.includes('nx g @frontmcp/nx:app'))).toBe(true);
    expect(consoleLogs.some((log) => log.includes('nx dev demo'))).toBe(true);
  });

  it('should handle install failure with helpful message', async () => {
    mockedRunCmd.mockRejectedValueOnce(new Error('npm exited with code 1'));

    await expect(runCreate('my-nx-app', { nx: true })).rejects.toThrow('process.exit called');

    expect(consoleLogs.some((log) => log.includes('Failed to scaffold Nx workspace'))).toBe(true);
  });

  it('should handle createRequire failure with network message', async () => {
    mockedRunCmd.mockResolvedValueOnce(undefined);
    const failingRequire = jest.fn().mockImplementation(() => {
      throw new Error('Cannot find package nx');
    });
    mockedCreateRequire.mockReturnValue(failingRequire as unknown as NodeRequire);

    await expect(runCreate('my-nx-app', { nx: true })).rejects.toThrow('process.exit called');

    expect(consoleLogs.some((log) => log.includes('Failed to install Nx tooling'))).toBe(true);
  });

  it('should refuse to scaffold into non-empty directory', async () => {
    const projectDir = path.join(tempDir, 'existing-dir');
    await mkdir(projectDir);
    await writeFile(path.join(projectDir, 'file.txt'), 'content');

    await expect(runCreate('existing-dir', { nx: true })).rejects.toThrow('process.exit called');

    expect(consoleLogs.some((log) => log.includes('non-empty directory'))).toBe(true);
  });

  it('should default project name to frontmcp-app when not provided', async () => {
    await runCreate(undefined, { nx: true });

    expect(mockWorkspaceGenerator).toHaveBeenCalledWith(
      expect.anything(),
      expect.objectContaining({ name: 'frontmcp-app' }),
    );
  });
});
