import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { resolve } from 'path';
import type { CliServices } from '../services.js';

// Mock the compiler module - using vi.hoisted to access the mock in vi.mock
const {
  mockCompile,
  MockCompiler,
  mockWriteFile,
  mockMkdir,
  mockReadFile,
  mockExistsSync,
  mockIsTTY,
  mockLoadConfig,
  mockChokidarWatch,
  mockWatcherOn,
  mockIsVerbose,
  mockIsDebug,
} = vi.hoisted(() => {
  const mockCompile = vi.fn();
  const mockWriteFile = vi.fn();
  const mockMkdir = vi.fn();
  const mockReadFile = vi.fn();
  const mockExistsSync = vi.fn();
  const mockIsTTY = vi.fn();
  const mockLoadConfig = vi.fn();
  const mockWatcherOn = vi.fn().mockReturnThis();
  const mockChokidarWatch = vi.fn().mockReturnValue({
    on: mockWatcherOn,
  });
  const mockIsVerbose = vi.fn().mockReturnValue(false);
  const mockIsDebug = vi.fn().mockReturnValue(false);
  return {
    mockCompile,
    MockCompiler: class {
      compile = mockCompile;
    },
    mockWriteFile,
    mockMkdir,
    mockReadFile,
    mockExistsSync,
    mockIsTTY,
    mockLoadConfig,
    mockChokidarWatch,
    mockWatcherOn,
    mockIsVerbose,
    mockIsDebug,
  };
});

vi.mock('@promptscript/compiler', () => ({
  Compiler: MockCompiler,
}));

// Mock the config loader
vi.mock('../config/loader.js', () => ({
  loadConfig: () => mockLoadConfig(),
  CONFIG_FILES: [
    'promptscript.yaml',
    'promptscript.yml',
    '.promptscriptrc.yaml',
    '.promptscriptrc.yml',
  ],
}));

// Mock fs/promises
vi.mock('fs/promises', () => ({
  writeFile: (...args: unknown[]) => mockWriteFile(...args),
  mkdir: (...args: unknown[]) => mockMkdir(...args),
  readFile: (...args: unknown[]) => mockReadFile(...args),
  readdir: vi.fn().mockResolvedValue([]),
}));

// Mock prettier loader
vi.mock('../prettier/loader.js', () => ({
  resolvePrettierOptions: vi.fn().mockResolvedValue({}),
}));

// Mock prettier post-format so test fixtures are written verbatim
vi.mock('../prettier/post-format.js', () => ({
  postFormatWithPrettier: vi.fn().mockResolvedValue(undefined),
}));

// Mock ora
vi.mock('ora', () => ({
  default: vi.fn().mockReturnValue({
    start: vi.fn().mockReturnThis(),
    succeed: vi.fn().mockReturnThis(),
    fail: vi.fn().mockReturnThis(),
    stop: vi.fn().mockReturnThis(),
    text: '',
  }),
}));

// Mock console output module for isVerbose and isDebug
vi.mock('../output/console.js', async (importOriginal) => {
  const actual = await importOriginal<typeof import('../output/console.js')>();
  return {
    ...actual,
    isVerbose: () => mockIsVerbose(),
    isDebug: () => mockIsDebug(),
  };
});

// Mock chalk
vi.mock('chalk', () => ({
  default: {
    green: (s: string) => s,
    red: (s: string) => s,
    yellow: (s: string) => s,
    blue: (s: string) => s,
    gray: (s: string) => s,
  },
}));

// Mock fs.existsSync for the entry file check
vi.mock('fs', () => ({
  existsSync: (...args: unknown[]) => mockExistsSync(...args),
  readFileSync: vi.fn().mockReturnValue(''),
}));

// Mock pager for isTTY
vi.mock('../output/pager.js', () => ({
  isTTY: () => mockIsTTY(),
}));

// Mock chokidar for watch mode
vi.mock('chokidar', () => ({
  default: {
    watch: (...args: unknown[]) => mockChokidarWatch(...args),
  },
}));

// Import after mocks are set up
import { compileCommand } from '../commands/compile.js';

const PROMPTSCRIPT_MARKER = '> Auto-generated by PromptScript';
const PROMPTSCRIPT_HTML_MARKER = '<!-- PromptScript 2024-01-01T00:00:00.000Z - do not edit -->';
const PROMPTSCRIPT_YAML_MARKER = '# promptscript-generated: 2024-01-01T00:00:00.000Z';

describe('compile command - overwrite protection', () => {
  let consoleSpy: ReturnType<typeof vi.spyOn>;
  let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
  let mockPrompts: {
    input: ReturnType<typeof vi.fn>;
    confirm: ReturnType<typeof vi.fn>;
    checkbox: ReturnType<typeof vi.fn>;
    select: ReturnType<typeof vi.fn>;
  };
  let mockServices: CliServices;

  const defaultConfig = {
    targets: ['claude'],
    registry: { path: './registry' },
  };

  const createMockOutput = (path: string, content: string) => ({
    path,
    content,
    target: 'claude',
  });

  beforeEach(() => {
    vi.clearAllMocks();
    process.exitCode = undefined;
    consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
    consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});

    // Reset fs/promises mocks
    mockWriteFile.mockResolvedValue(undefined);
    mockMkdir.mockResolvedValue(undefined);
    mockReadFile.mockResolvedValue('');

    mockPrompts = {
      input: vi.fn().mockResolvedValue(''),
      confirm: vi.fn().mockResolvedValue(false),
      checkbox: vi.fn().mockResolvedValue([]),
      select: vi.fn().mockResolvedValue('no'),
    };

    mockServices = {
      fs: {
        existsSync: vi.fn(),
        writeFile: vi.fn(),
        mkdir: vi.fn(),
        readFile: vi.fn(),
        readdir: vi.fn(),
        readFileSync: vi.fn(),
      } as unknown as CliServices['fs'],
      prompts: mockPrompts as unknown as CliServices['prompts'],
      cwd: '/mock/project',
    };

    // Default: entry file exists, other files don't
    mockExistsSync.mockImplementation((path: string) => {
      if (path.includes('project.prs')) {
        return true;
      }
      return false;
    });

    mockLoadConfig.mockResolvedValue(defaultConfig);

    // Default: isTTY returns true (interactive mode)
    mockIsTTY.mockReturnValue(true);

    // Default: verbose and debug mode disabled
    mockIsVerbose.mockReturnValue(false);
    mockIsDebug.mockReturnValue(false);
  });

  afterEach(() => {
    consoleSpy.mockRestore();
    consoleErrorSpy.mockRestore();
  });

  describe('when file does not exist', () => {
    it('should write file normally without prompting', async () => {
      const outputs = new Map([['CLAUDE.md', createMockOutput('CLAUDE.md', 'content')]]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      // File does not exist
      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        return false;
      });

      await compileCommand({}, mockServices);

      expect(mockWriteFile).toHaveBeenCalledWith(resolve('CLAUDE.md'), 'content', 'utf-8');
      expect(mockPrompts.select).not.toHaveBeenCalled();
    });
  });

  describe('when file exists with PromptScript marker', () => {
    it('should overwrite silently without prompting', async () => {
      const outputs = new Map([['CLAUDE.md', createMockOutput('CLAUDE.md', 'new content')]]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      // File exists
      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        if (path.includes('CLAUDE.md')) return true;
        return false;
      });

      // File contains PromptScript marker
      mockReadFile.mockResolvedValue(`# Some header\n${PROMPTSCRIPT_MARKER}\n\nOld content`);

      await compileCommand({}, mockServices);

      expect(mockWriteFile).toHaveBeenCalledWith(resolve('CLAUDE.md'), 'new content', 'utf-8');
      expect(mockPrompts.select).not.toHaveBeenCalled();
    });
  });

  describe('when file exists without PromptScript marker', () => {
    beforeEach(() => {
      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        if (path.includes('CLAUDE.md')) return true;
        return false;
      });

      // File does NOT contain PromptScript marker
      mockReadFile.mockResolvedValue('# My custom rules\n\nCustom content');
    });

    describe('in interactive mode', () => {
      beforeEach(() => {
        mockIsTTY.mockReturnValue(true);
      });

      it('should prompt user and skip file when user selects no', async () => {
        const outputs = new Map([['CLAUDE.md', createMockOutput('CLAUDE.md', 'new content')]]);

        mockCompile.mockResolvedValue({
          success: true,
          outputs,
          stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
          warnings: [],
          errors: [],
        });

        mockPrompts.select.mockResolvedValue('no');

        await compileCommand({}, mockServices);

        expect(mockPrompts.select).toHaveBeenCalledTimes(1);
        expect(mockWriteFile).not.toHaveBeenCalled();
        // Should show skipped message
        expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('⊘'));
      });

      it('should prompt user and overwrite file when user selects yes', async () => {
        const outputs = new Map([['CLAUDE.md', createMockOutput('CLAUDE.md', 'new content')]]);

        mockCompile.mockResolvedValue({
          success: true,
          outputs,
          stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
          warnings: [],
          errors: [],
        });

        mockPrompts.select.mockResolvedValue('yes');

        await compileCommand({}, mockServices);

        expect(mockPrompts.select).toHaveBeenCalledTimes(1);
        expect(mockWriteFile).toHaveBeenCalledWith(resolve('CLAUDE.md'), 'new content', 'utf-8');
      });

      it('should overwrite all remaining files when user selects all', async () => {
        const outputs = new Map([
          ['CLAUDE.md', createMockOutput('CLAUDE.md', 'content1')],
          ['AGENTS.md', createMockOutput('AGENTS.md', 'content2')],
          ['rules.md', createMockOutput('rules.md', 'content3')],
        ]);

        mockCompile.mockResolvedValue({
          success: true,
          outputs,
          stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
          warnings: [],
          errors: [],
        });

        // All files exist without marker
        mockExistsSync.mockImplementation((path: string) => {
          if (path.includes('project.prs')) return true;
          return true; // All output files exist
        });

        // First prompt: select 'all'
        mockPrompts.select.mockResolvedValue('all');

        await compileCommand({}, mockServices);

        // Should only prompt once (for the first file)
        expect(mockPrompts.select).toHaveBeenCalledTimes(1);
        // All files should be written
        expect(mockWriteFile).toHaveBeenCalledTimes(3);
      });

      it('should handle mixed responses (yes, no, all)', async () => {
        const outputs = new Map([
          ['file1.md', createMockOutput('file1.md', 'content1')],
          ['file2.md', createMockOutput('file2.md', 'content2')],
          ['file3.md', createMockOutput('file3.md', 'content3')],
        ]);

        mockCompile.mockResolvedValue({
          success: true,
          outputs,
          stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
          warnings: [],
          errors: [],
        });

        mockExistsSync.mockImplementation((path: string) => {
          if (path.includes('project.prs')) return true;
          return true;
        });

        // First: yes, Second: no
        mockPrompts.select
          .mockResolvedValueOnce('yes')
          .mockResolvedValueOnce('no')
          .mockResolvedValueOnce('yes');

        await compileCommand({}, mockServices);

        expect(mockPrompts.select).toHaveBeenCalledTimes(3);
        expect(mockWriteFile).toHaveBeenCalledTimes(2); // Only 2 files written
      });
    });

    describe('in non-interactive mode', () => {
      beforeEach(() => {
        mockIsTTY.mockReturnValue(false);
      });

      it('should fail with error listing conflicts', async () => {
        const outputs = new Map([
          ['CLAUDE.md', createMockOutput('CLAUDE.md', 'new content')],
          ['AGENTS.md', createMockOutput('AGENTS.md', 'other content')],
        ]);

        mockCompile.mockResolvedValue({
          success: true,
          outputs,
          stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
          warnings: [],
          errors: [],
        });

        mockExistsSync.mockImplementation((path: string) => {
          if (path.includes('project.prs')) return true;
          return true; // All files exist
        });

        await compileCommand({}, mockServices);

        expect(process.exitCode).toBe(1);
        expect(consoleErrorSpy).toHaveBeenCalledWith(
          expect.stringContaining('Cannot overwrite files not generated by PromptScript')
        );
        expect(mockPrompts.select).not.toHaveBeenCalled();
      });
    });

    describe('with --force flag', () => {
      it('should overwrite without prompting', async () => {
        const outputs = new Map([['CLAUDE.md', createMockOutput('CLAUDE.md', 'new content')]]);

        mockCompile.mockResolvedValue({
          success: true,
          outputs,
          stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
          warnings: [],
          errors: [],
        });

        await compileCommand({ force: true }, mockServices);

        expect(mockPrompts.select).not.toHaveBeenCalled();
        expect(mockWriteFile).toHaveBeenCalledWith(resolve('CLAUDE.md'), 'new content', 'utf-8');
      });

      it('should work in non-interactive mode with --force', async () => {
        mockIsTTY.mockReturnValue(false);

        const outputs = new Map([['CLAUDE.md', createMockOutput('CLAUDE.md', 'new content')]]);

        mockCompile.mockResolvedValue({
          success: true,
          outputs,
          stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
          warnings: [],
          errors: [],
        });

        await compileCommand({ force: true }, mockServices);

        expect(mockWriteFile).toHaveBeenCalled();
        expect(process.exitCode).toBeUndefined();
      });
    });
  });

  describe('with --dry-run flag', () => {
    it('should show what would happen for new files', async () => {
      const outputs = new Map([['CLAUDE.md', createMockOutput('CLAUDE.md', 'content')]]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        return false;
      });

      await compileCommand({ dryRun: true }, mockServices);

      expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Would write:'));
      expect(mockWriteFile).not.toHaveBeenCalled();
      expect(mockPrompts.select).not.toHaveBeenCalled();
    });

    it('should show overwrite for PromptScript-generated files', async () => {
      const outputs = new Map([['CLAUDE.md', createMockOutput('CLAUDE.md', 'content')]]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        if (path.includes('CLAUDE.md')) return true;
        return false;
      });

      mockReadFile.mockResolvedValue(`${PROMPTSCRIPT_MARKER}\ncontent`);

      await compileCommand({ dryRun: true }, mockServices);

      expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Would overwrite:'));
      expect(mockWriteFile).not.toHaveBeenCalled();
    });

    it('should show conflict warning for non-PromptScript files', async () => {
      const outputs = new Map([['CLAUDE.md', createMockOutput('CLAUDE.md', 'content')]]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        if (path.includes('CLAUDE.md')) return true;
        return false;
      });

      mockReadFile.mockResolvedValue('# Custom rules');

      await compileCommand({ dryRun: true }, mockServices);

      expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Would conflict:'));
      expect(mockWriteFile).not.toHaveBeenCalled();
      expect(mockPrompts.select).not.toHaveBeenCalled();
    });
  });

  describe('user cancellation (Ctrl+C)', () => {
    it('should handle ExitPromptError gracefully', async () => {
      const outputs = new Map([['CLAUDE.md', createMockOutput('CLAUDE.md', 'content')]]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        if (path.includes('CLAUDE.md')) return true;
        return false;
      });

      mockReadFile.mockResolvedValue('# Custom rules');

      const exitPromptError = new Error('User cancelled');
      exitPromptError.name = 'ExitPromptError';
      mockPrompts.select.mockRejectedValue(exitPromptError);

      // Should not throw
      await compileCommand({}, mockServices);

      expect(mockWriteFile).not.toHaveBeenCalled();
      expect(process.exitCode).toBeUndefined();
      expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Compilation cancelled'));
    });
  });

  describe('PromptScript marker detection', () => {
    it('should detect marker in first 10 lines', async () => {
      const outputs = new Map([['CLAUDE.md', createMockOutput('CLAUDE.md', 'content')]]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        if (path.includes('CLAUDE.md')) return true;
        return false;
      });

      // Marker on line 9 (within first 10 lines)
      const lines = Array(8).fill('# Some content');
      lines.push(PROMPTSCRIPT_MARKER);
      mockReadFile.mockResolvedValue(lines.join('\n'));

      await compileCommand({}, mockServices);

      // Should overwrite without prompt since marker is present
      expect(mockPrompts.select).not.toHaveBeenCalled();
      expect(mockWriteFile).toHaveBeenCalled();
    });

    it('should detect marker in extended YAML frontmatter (line 45)', async () => {
      const outputs = new Map([['CLAUDE.md', createMockOutput('CLAUDE.md', 'content')]]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        if (path.includes('CLAUDE.md')) return true;
        return false;
      });

      // Marker on line 45 (after long YAML frontmatter, within first 50 lines)
      const lines = Array(44).fill('# Some content');
      lines.push(PROMPTSCRIPT_MARKER);
      mockReadFile.mockResolvedValue(lines.join('\n'));

      await compileCommand({}, mockServices);

      // Should NOT prompt because marker is within first 50 lines
      expect(mockPrompts.select).not.toHaveBeenCalled();
      expect(mockWriteFile).toHaveBeenCalled();
    });

    it('should NOT detect marker after line 50', async () => {
      const outputs = new Map([['CLAUDE.md', createMockOutput('CLAUDE.md', 'content')]]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        if (path.includes('CLAUDE.md')) return true;
        return false;
      });

      // Marker on line 55 (after first 50 lines)
      const lines = Array(54).fill('# Some content');
      lines.push(PROMPTSCRIPT_MARKER);
      mockReadFile.mockResolvedValue(lines.join('\n'));

      mockPrompts.select.mockResolvedValue('no');

      await compileCommand({}, mockServices);

      // Should prompt because marker is not in first 50 lines
      expect(mockPrompts.select).toHaveBeenCalled();
    });
  });

  describe('skipped files reporting', () => {
    it('should report number of skipped files', async () => {
      const outputs = new Map([
        ['file1.md', createMockOutput('file1.md', 'content1')],
        ['file2.md', createMockOutput('file2.md', 'content2')],
      ]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        return true;
      });

      mockReadFile.mockResolvedValue('# Custom content');
      mockPrompts.select.mockResolvedValue('no');

      await compileCommand({}, mockServices);

      expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Skipped 2 file(s)'));
    });
  });

  describe('unchanged files detection', () => {
    it('should skip writing when content is unchanged (only marker differs)', async () => {
      const newContent = `${PROMPTSCRIPT_HTML_MARKER}\n\n# Title\n\nSome content`;
      const outputs = new Map([['CLAUDE.md', createMockOutput('CLAUDE.md', newContent)]]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        if (path.includes('CLAUDE.md')) return true;
        return false;
      });

      // Existing file has different timestamp but same content
      const existingContent = `<!-- PromptScript 2023-06-15T12:00:00.000Z - do not edit -->\n\n# Title\n\nSome content`;
      mockReadFile.mockResolvedValue(existingContent);

      await compileCommand({}, mockServices);

      // Should NOT write because content is the same
      expect(mockWriteFile).not.toHaveBeenCalled();
      // Should show unchanged indicator
      expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('○'));
    });

    it('should write when content has actually changed', async () => {
      const newContent = `${PROMPTSCRIPT_HTML_MARKER}\n\n# Title\n\nUpdated content`;
      const outputs = new Map([['CLAUDE.md', createMockOutput('CLAUDE.md', newContent)]]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        if (path.includes('CLAUDE.md')) return true;
        return false;
      });

      // Existing file has different content
      const existingContent = `<!-- PromptScript 2023-06-15T12:00:00.000Z - do not edit -->\n\n# Title\n\nOriginal content`;
      mockReadFile.mockResolvedValue(existingContent);

      await compileCommand({}, mockServices);

      // Should write because content changed
      expect(mockWriteFile).toHaveBeenCalledWith(resolve('CLAUDE.md'), newContent, 'utf-8');
      // Should show success indicator
      expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('✓'));
    });

    it('should report number of unchanged files', async () => {
      const content1 = `${PROMPTSCRIPT_HTML_MARKER}\n\nContent 1`;
      const content2 = `${PROMPTSCRIPT_HTML_MARKER}\n\nContent 2`;
      const outputs = new Map([
        ['file1.md', createMockOutput('file1.md', content1)],
        ['file2.md', createMockOutput('file2.md', content2)],
      ]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        return true;
      });

      // Both files have same content but different timestamps
      mockReadFile.mockImplementation((path: string) => {
        if (path.includes('file1.md')) {
          return Promise.resolve(
            `<!-- PromptScript 2023-01-01T00:00:00.000Z - do not edit -->\n\nContent 1`
          );
        }
        return Promise.resolve(
          `<!-- PromptScript 2023-01-01T00:00:00.000Z - do not edit -->\n\nContent 2`
        );
      });

      await compileCommand({}, mockServices);

      expect(mockWriteFile).not.toHaveBeenCalled();
      expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Unchanged 2 file(s)'));
    });

    it('should handle mix of unchanged and changed files', async () => {
      const unchangedContent = `${PROMPTSCRIPT_HTML_MARKER}\n\nUnchanged`;
      const changedContent = `${PROMPTSCRIPT_HTML_MARKER}\n\nChanged content`;
      const outputs = new Map([
        ['unchanged.md', createMockOutput('unchanged.md', unchangedContent)],
        ['changed.md', createMockOutput('changed.md', changedContent)],
      ]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        return true;
      });

      mockReadFile.mockImplementation((path: string) => {
        if (path.includes('unchanged.md')) {
          return Promise.resolve(
            `<!-- PromptScript 2023-01-01T00:00:00.000Z - do not edit -->\n\nUnchanged`
          );
        }
        return Promise.resolve(
          `<!-- PromptScript 2023-01-01T00:00:00.000Z - do not edit -->\n\nOriginal`
        );
      });

      await compileCommand({}, mockServices);

      // Only changed.md should be written
      expect(mockWriteFile).toHaveBeenCalledTimes(1);
      expect(mockWriteFile).toHaveBeenCalledWith(resolve('changed.md'), changedContent, 'utf-8');
      expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Unchanged 1 file(s)'));
    });
  });

  describe('--force with unchanged files', () => {
    it('should rewrite PromptScript-generated files even when content is unchanged', async () => {
      const newContent = `${PROMPTSCRIPT_HTML_MARKER}\n\n# Title\n\nSame content`;
      const outputs = new Map([['CLAUDE.md', createMockOutput('CLAUDE.md', newContent)]]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        if (path.includes('CLAUDE.md')) return true;
        return false;
      });

      // Existing file has different timestamp but same body content
      const existingContent = `<!-- PromptScript 2023-06-15T12:00:00.000Z - do not edit -->\n\n# Title\n\nSame content`;
      mockReadFile.mockResolvedValue(existingContent);

      await compileCommand({ force: true }, mockServices);

      // --force should rewrite the file even though body content is unchanged
      expect(mockWriteFile).toHaveBeenCalledWith(resolve('CLAUDE.md'), newContent, 'utf-8');
      expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('✓'));
    });

    it('should rewrite non-marker files even when content matches', async () => {
      const resourceContent = 'import os\nprint("hello")\n';
      const outputs = new Map([
        ['skills/myskill/helper.py', createMockOutput('skills/myskill/helper.py', resourceContent)],
      ]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        if (path.includes('helper.py')) return true;
        return false;
      });

      // Identical content on disk
      mockReadFile.mockResolvedValue(resourceContent);

      await compileCommand({ force: true }, mockServices);

      // --force should rewrite the file even though content is identical
      expect(mockWriteFile).toHaveBeenCalledWith(
        resolve('skills/myskill/helper.py'),
        resourceContent,
        'utf-8'
      );
      expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('✓'));
    });
  });

  describe('dry-run with unchanged detection', () => {
    it('should show unchanged status in dry-run mode', async () => {
      const content = `${PROMPTSCRIPT_HTML_MARKER}\n\nSame content`;
      const outputs = new Map([['CLAUDE.md', createMockOutput('CLAUDE.md', content)]]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        if (path.includes('CLAUDE.md')) return true;
        return false;
      });

      // Same content with different timestamp
      mockReadFile.mockResolvedValue(
        `<!-- PromptScript 2020-01-01T00:00:00.000Z - do not edit -->\n\nSame content`
      );

      await compileCommand({ dryRun: true }, mockServices);

      expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Unchanged:'));
      expect(mockWriteFile).not.toHaveBeenCalled();
    });

    it('should show would overwrite in dry-run mode when content changed', async () => {
      const content = `${PROMPTSCRIPT_HTML_MARKER}\n\nNew content`;
      const outputs = new Map([['CLAUDE.md', createMockOutput('CLAUDE.md', content)]]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        if (path.includes('CLAUDE.md')) return true;
        return false;
      });

      mockReadFile.mockResolvedValue(
        `<!-- PromptScript 2020-01-01T00:00:00.000Z - do not edit -->\n\nOld content`
      );

      await compileCommand({ dryRun: true }, mockServices);

      expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Would overwrite:'));
      expect(mockWriteFile).not.toHaveBeenCalled();
    });
  });

  describe('non-marker file unchanged detection', () => {
    it('should report unchanged when resource file content matches', async () => {
      const resourceContent = 'import os\nprint("hello")\n';
      const outputs = new Map([
        ['skills/myskill/helper.py', createMockOutput('skills/myskill/helper.py', resourceContent)],
      ]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        if (path.includes('helper.py')) return true;
        return false;
      });

      // Existing file has identical content (no PromptScript marker)
      mockReadFile.mockResolvedValue(resourceContent);

      await compileCommand({}, mockServices);

      expect(mockWriteFile).not.toHaveBeenCalled();
      expect(mockPrompts.select).not.toHaveBeenCalled();
      expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('○'));
    });

    it('should report unchanged in dry-run for non-marker files with matching content', async () => {
      const csvContent = 'a,b,c\n1,2,3\n';
      const outputs = new Map([
        ['skills/myskill/data.csv', createMockOutput('skills/myskill/data.csv', csvContent)],
      ]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        if (path.includes('data.csv')) return true;
        return false;
      });

      // Same content, no marker
      mockReadFile.mockResolvedValue(csvContent);

      await compileCommand({ dryRun: true }, mockServices);

      expect(mockWriteFile).not.toHaveBeenCalled();
      expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Unchanged:'));
    });
  });

  describe('output directory option', () => {
    it('should write files to custom output directory', async () => {
      const outputs = new Map([['CLAUDE.md', createMockOutput('CLAUDE.md', 'content')]]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        return false;
      });

      await compileCommand({ output: '/custom/output' }, mockServices);

      expect(mockWriteFile).toHaveBeenCalledWith(
        resolve('/custom/output', 'CLAUDE.md'),
        'content',
        'utf-8'
      );
    });
  });

  describe('HTML marker detection', () => {
    it('should detect HTML comment marker format', async () => {
      const outputs = new Map([['CLAUDE.md', createMockOutput('CLAUDE.md', 'new content')]]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        if (path.includes('CLAUDE.md')) return true;
        return false;
      });

      // File contains HTML comment marker
      mockReadFile.mockResolvedValue(`# Title\n\n${PROMPTSCRIPT_HTML_MARKER}\n\nOld content`);

      await compileCommand({}, mockServices);

      // Should recognize as PromptScript-generated and not prompt
      expect(mockPrompts.select).not.toHaveBeenCalled();
    });
  });

  describe('YAML marker detection', () => {
    it('should detect YAML comment marker in frontmatter files', async () => {
      const outputs = new Map([
        [
          '.factory/skills/commit/SKILL.md',
          createMockOutput('.factory/skills/commit/SKILL.md', 'new content'),
        ],
      ]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        if (path.includes('SKILL.md')) return true;
        return false;
      });

      // File contains YAML comment marker inside frontmatter
      mockReadFile.mockResolvedValue(
        `---\n${PROMPTSCRIPT_YAML_MARKER}\nname: commit\ndescription: Create git commits\n---\n\nOld content`
      );

      await compileCommand({}, mockServices);

      // Should recognize as PromptScript-generated and not prompt
      expect(mockPrompts.select).not.toHaveBeenCalled();
    });
  });

  describe('compilation warnings', () => {
    it('should print warnings when present', async () => {
      const outputs = new Map([['CLAUDE.md', createMockOutput('CLAUDE.md', 'content')]]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [
          { ruleId: 'WARN001', message: 'Test warning 1', severity: 'warning' },
          { ruleId: 'WARN002', message: 'Test warning 2', severity: 'warning' },
        ],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        return false;
      });

      await compileCommand({}, mockServices);

      expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Warnings (2)'));
      expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('WARN001'));
      expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Test warning 1'));
    });

    it('should not print warnings section when no warnings', async () => {
      const outputs = new Map([['CLAUDE.md', createMockOutput('CLAUDE.md', 'content')]]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        return false;
      });

      await compileCommand({}, mockServices);

      expect(consoleSpy).not.toHaveBeenCalledWith(expect.stringContaining('Warnings'));
    });
  });

  describe('compilation errors', () => {
    it('should print errors with location when compilation fails', async () => {
      mockCompile.mockResolvedValue({
        success: false,
        outputs: new Map(),
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [
          {
            code: 'ERR001',
            message: 'Test error',
            location: { file: 'test.prs', line: 10, column: 5 },
          },
        ],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        return false;
      });

      await compileCommand({}, mockServices);

      expect(process.exitCode).toBe(1);
      expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Test error'));
      expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('at test.prs:10:5'));
    });
  });

  describe('entry file not found', () => {
    it('should exit with error when project.prs does not exist', async () => {
      mockExistsSync.mockReturnValue(false);

      await compileCommand({}, mockServices);

      expect(process.exitCode).toBe(1);
      expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('File not found'));
      expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('prs init'));
    });

    it('should use config.input.entry when specified', async () => {
      mockLoadConfig.mockResolvedValue({
        ...defaultConfig,
        input: { entry: 'custom/entry.prs' },
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('custom/entry.prs')) return true;
        return false;
      });

      const outputs = new Map([['CLAUDE.md', createMockOutput('CLAUDE.md', 'content')]]);
      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      await compileCommand({}, mockServices);

      expect(mockCompile).toHaveBeenCalledWith(expect.stringContaining('custom/entry.prs'));
    });
  });

  describe('target and format options', () => {
    it('should use --target option when specified', async () => {
      const outputs = new Map([['CLAUDE.md', createMockOutput('CLAUDE.md', 'content')]]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        return false;
      });

      await compileCommand({ target: 'github' }, mockServices);

      expect(mockWriteFile).toHaveBeenCalled();
    });

    it('should use --format as alias for --target', async () => {
      const outputs = new Map([['CLAUDE.md', createMockOutput('CLAUDE.md', 'content')]]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        return false;
      });

      await compileCommand({ format: 'claude' }, mockServices);

      expect(mockWriteFile).toHaveBeenCalled();
    });
  });

  describe('registry option', () => {
    it('should use --registry option when specified', async () => {
      const outputs = new Map([['CLAUDE.md', createMockOutput('CLAUDE.md', 'content')]]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        return false;
      });

      await compileCommand({ registry: '/custom/registry' }, mockServices);

      expect(mockWriteFile).toHaveBeenCalled();
    });
  });

  describe('verbose and debug logging', () => {
    it('should output verbose logs when verbose mode is enabled', async () => {
      mockIsVerbose.mockReturnValue(true);
      mockIsDebug.mockReturnValue(false);

      const outputs = new Map([['CLAUDE.md', createMockOutput('CLAUDE.md', 'content')]]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        return false;
      });

      await compileCommand({}, mockServices);

      // Verbose logging is enabled via the Compiler's logger, which we mock
      // The test verifies the compilation completes successfully with verbose mode
      expect(mockWriteFile).toHaveBeenCalled();
    });

    it('should output debug logs when debug mode is enabled', async () => {
      mockIsVerbose.mockReturnValue(false);
      mockIsDebug.mockReturnValue(true);

      const outputs = new Map([['CLAUDE.md', createMockOutput('CLAUDE.md', 'content')]]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        return false;
      });

      await compileCommand({}, mockServices);

      // Debug logging is enabled via the Compiler's logger
      expect(mockWriteFile).toHaveBeenCalled();
    });
  });

  describe('target configuration parsing', () => {
    it('should parse object format targets with config', async () => {
      const outputs = new Map([['CLAUDE.md', createMockOutput('CLAUDE.md', 'content')]]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        return false;
      });

      // Use object format in config
      mockLoadConfig.mockResolvedValue({
        targets: [{ github: { convention: 'xml' } }],
        registry: { path: './registry' },
      });

      await compileCommand({}, mockServices);

      expect(mockWriteFile).toHaveBeenCalled();
    });

    it('should filter out disabled targets', async () => {
      const outputs = new Map([['CLAUDE.md', createMockOutput('CLAUDE.md', 'content')]]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        return false;
      });

      // One enabled, one disabled
      mockLoadConfig.mockResolvedValue({
        targets: [{ github: { enabled: true } }, { claude: { enabled: false } }],
        registry: { path: './registry' },
      });

      await compileCommand({}, mockServices);

      expect(mockWriteFile).toHaveBeenCalled();
    });

    it('should throw error for empty target configuration', async () => {
      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        return false;
      });

      // Empty object target
      mockLoadConfig.mockResolvedValue({
        targets: [{}],
        registry: { path: './registry' },
      });

      await compileCommand({}, mockServices);
      expect(process.exitCode).toBe(1);
      expect(consoleErrorSpy).toHaveBeenCalledWith(
        expect.stringContaining('Empty target configuration')
      );
    });
  });

  describe('isPromptScriptGenerated error handling', () => {
    it('should treat file as not generated if read fails', async () => {
      const outputs = new Map([['CLAUDE.md', createMockOutput('CLAUDE.md', 'content')]]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        if (path.includes('CLAUDE.md')) return true;
        return false;
      });

      // Simulate read error
      mockReadFile.mockRejectedValue(new Error('Permission denied'));
      mockPrompts.select.mockResolvedValue('yes');

      await compileCommand({}, mockServices);

      // Should prompt since file cannot be read (treated as not generated)
      expect(mockPrompts.select).toHaveBeenCalled();
    });
  });

  describe('watch mode', () => {
    beforeEach(() => {
      mockChokidarWatch.mockClear();
      mockWatcherOn.mockClear();
    });

    it('should start watching when --watch flag is provided', async () => {
      const outputs = new Map([['CLAUDE.md', createMockOutput('CLAUDE.md', 'content')]]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        return false;
      });

      await compileCommand({ watch: true }, mockServices);

      // Should start chokidar watcher (path is resolved to absolute from projectRoot)
      expect(mockChokidarWatch).toHaveBeenCalledWith(
        expect.stringContaining('.promptscript/**/*.prs'),
        expect.objectContaining({
          persistent: true,
          ignoreInitial: true,
        })
      );

      // Should register event handlers
      expect(mockWatcherOn).toHaveBeenCalledWith('change', expect.any(Function));
      expect(mockWatcherOn).toHaveBeenCalledWith('add', expect.any(Function));
      expect(mockWatcherOn).toHaveBeenCalledWith('unlink', expect.any(Function));
      expect(mockWatcherOn).toHaveBeenCalledWith('error', expect.any(Function));

      // Should show watching message
      expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Watching for changes'));
    });

    it('should trigger recompilation on file change', async () => {
      const outputs = new Map([['CLAUDE.md', createMockOutput('CLAUDE.md', 'content')]]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        return false;
      });

      // Capture the change handler
      let changeHandler: ((filename: string) => void) | undefined;
      mockWatcherOn.mockImplementation((event: string, handler: (filename: string) => void) => {
        if (event === 'change') {
          changeHandler = handler;
        }
        return { on: mockWatcherOn };
      });

      await compileCommand({ watch: true }, mockServices);

      // Verify change handler was registered
      expect(changeHandler).toBeDefined();
    });

    it('should handle file add event', async () => {
      const outputs = new Map([['CLAUDE.md', createMockOutput('CLAUDE.md', 'content')]]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        return false;
      });

      // Capture the add handler
      let addHandler: ((filename: string) => void) | undefined;
      mockWatcherOn.mockImplementation((event: string, handler: (filename: string) => void) => {
        if (event === 'add') {
          addHandler = handler;
        }
        return { on: mockWatcherOn };
      });

      await compileCommand({ watch: true }, mockServices);

      // Verify add handler was registered
      expect(addHandler).toBeDefined();

      // Trigger add event
      if (addHandler) {
        addHandler('test.prs');
        expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('File added'));
      }
    });

    it('should handle file unlink event', async () => {
      const outputs = new Map([['CLAUDE.md', createMockOutput('CLAUDE.md', 'content')]]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        return false;
      });

      // Capture the unlink handler
      let unlinkHandler: ((filename: string) => void) | undefined;
      mockWatcherOn.mockImplementation((event: string, handler: (filename: string) => void) => {
        if (event === 'unlink') {
          unlinkHandler = handler;
        }
        return { on: mockWatcherOn };
      });

      await compileCommand({ watch: true }, mockServices);

      // Verify unlink handler was registered and trigger it
      expect(unlinkHandler).toBeDefined();
      if (unlinkHandler) {
        unlinkHandler('test.prs');
        expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('File removed'));
      }
    });

    it('should handle watcher error event', async () => {
      const outputs = new Map([['CLAUDE.md', createMockOutput('CLAUDE.md', 'content')]]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        return false;
      });

      // Capture the error handler
      let errorHandler: ((error: unknown) => void) | undefined;
      mockWatcherOn.mockImplementation((event: string, handler: (arg: unknown) => void) => {
        if (event === 'error') {
          errorHandler = handler;
        }
        return { on: mockWatcherOn };
      });

      await compileCommand({ watch: true }, mockServices);

      // Verify error handler was registered and trigger it
      expect(errorHandler).toBeDefined();
      if (errorHandler) {
        errorHandler(new Error('Watch error'));
        expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Watcher error'));
      }
    });

    it('should handle non-Error objects in watcher error event', async () => {
      const outputs = new Map([['CLAUDE.md', createMockOutput('CLAUDE.md', 'content')]]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        return false;
      });

      // Capture the error handler
      let errorHandler: ((error: unknown) => void) | undefined;
      mockWatcherOn.mockImplementation((event: string, handler: (arg: unknown) => void) => {
        if (event === 'error') {
          errorHandler = handler;
        }
        return { on: mockWatcherOn };
      });

      await compileCommand({ watch: true }, mockServices);

      // Trigger with non-Error object
      if (errorHandler) {
        errorHandler('string error');
        expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('string error'));
      }
    });

    it('should debounce rapid file changes', async () => {
      vi.useFakeTimers();

      const outputs = new Map([['CLAUDE.md', createMockOutput('CLAUDE.md', 'content')]]);

      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 100, resolveTime: 50, validateTime: 25, formatTime: 25 },
        warnings: [],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path.includes('project.prs')) return true;
        return false;
      });

      // Capture the change handler
      let changeHandler: ((filename: string) => void) | undefined;
      mockWatcherOn.mockImplementation((event: string, handler: (filename: string) => void) => {
        if (event === 'change') {
          changeHandler = handler;
        }
        return { on: mockWatcherOn };
      });

      await compileCommand({ watch: true }, mockServices);

      // Reset compile mock to track recompilation calls
      mockCompile.mockClear();

      // Trigger multiple rapid changes
      if (changeHandler) {
        changeHandler('file1.prs');
        changeHandler('file2.prs');
        changeHandler('file3.prs');

        // Advance timer past debounce period (100ms)
        await vi.advanceTimersByTimeAsync(150);

        // Should show file changed message after debounce
        expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('File changed'));
      }

      vi.useRealTimers();
    });
  });

  describe('--cwd option', () => {
    it('should resolve .promptscript and config from cwd directory', async () => {
      mockLoadConfig.mockReturnValue(defaultConfig);

      const outputs = new Map([['test', createMockOutput('CLAUDE.md', '# Test')]]);
      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 10, resolveTime: 5, validateTime: 3, formatTime: 2 },
        warnings: [],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        // Config file found in the custom cwd
        if (path === resolve('/my/project', 'promptscript.yaml')) return true;
        // Entry file found in the custom cwd
        if (path === resolve('/my/project', '.promptscript', 'project.prs')) return true;
        // Output file doesn't exist yet
        return false;
      });

      await compileCommand({ cwd: '/my/project' }, mockServices);

      // Should have been called with config path from the cwd directory
      expect(mockLoadConfig).toHaveBeenCalled();
    });

    it('should use cwd as default output directory', async () => {
      mockLoadConfig.mockReturnValue(defaultConfig);

      const outputs = new Map([['test', createMockOutput('CLAUDE.md', '# Test content')]]);
      mockCompile.mockResolvedValue({
        success: true,
        outputs,
        stats: { totalTime: 10, resolveTime: 5, validateTime: 3, formatTime: 2 },
        warnings: [],
        errors: [],
      });

      mockExistsSync.mockImplementation((path: string) => {
        if (path === resolve('/custom/project', 'promptscript.yaml')) return true;
        if (path === resolve('/custom/project', '.promptscript', 'project.prs')) return true;
        return false;
      });

      await compileCommand({ cwd: '/custom/project' }, mockServices);

      // Output should be written relative to the project root (cwd), not current CWD
      expect(mockWriteFile).toHaveBeenCalledWith(
        resolve('/custom/project', 'CLAUDE.md'),
        '# Test content',
        'utf-8'
      );
    });

    it('should fail when entry file not found in cwd directory', async () => {
      mockLoadConfig.mockReturnValue(defaultConfig);

      mockExistsSync.mockImplementation((path: string) => {
        if (path === resolve('/empty/project', 'promptscript.yaml')) return true;
        // Entry file does NOT exist
        return false;
      });

      await compileCommand({ cwd: '/empty/project' }, mockServices);
      expect(process.exitCode).toBe(1);
    });

    it('should fall back to cwd when no config files found', async () => {
      // No config files found in cwd - loadConfig should still be called with undefined
      mockExistsSync.mockReturnValue(false);

      // loadConfig throws when no config file found (simulate)
      mockLoadConfig.mockImplementation(() => {
        throw new Error('Config file not found');
      });

      await compileCommand({ cwd: '/nonexistent' }, mockServices);
      expect(process.exitCode).toBe(1);
    });
  });
});
