import { describe, expect, it, beforeEach, vi, afterEach } from 'vitest';
import type { Program, SourceLocation } from '@promptscript/core';
import { ClaudeFormatter, CLAUDE_VERSIONS } from '../formatters/claude.js';

const createLoc = (): SourceLocation => ({
  file: 'test.prs',
  line: 1,
  column: 1,
});

const createMinimalProgram = (): Program => ({
  type: 'Program',
  uses: [],
  blocks: [],
  extends: [],
  loc: createLoc(),
});

describe('ClaudeFormatter', () => {
  let formatter: ClaudeFormatter;

  beforeEach(() => {
    formatter = new ClaudeFormatter();
  });

  it('should have correct name, outputPath and description', () => {
    expect(formatter.name).toBe('claude');
    expect(formatter.outputPath).toBe('CLAUDE.md');
    expect(formatter.description).toBe('Claude Code instructions (concise Markdown)');
  });

  describe('convention support', () => {
    it('should support markdown convention (default)', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'identity',
            content: {
              type: 'TextContent',
              value: 'Test project',
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };
      const result = formatter.format(ast, { convention: 'markdown' });
      expect(result.content).toContain('## Project');
    });

    it('should support xml convention', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'identity',
            content: {
              type: 'TextContent',
              value: 'Test project',
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };
      const result = formatter.format(ast, { convention: 'xml' });
      // XML convention transforms names to kebab-case
      expect(result.content).toContain('<project>');
      expect(result.content).toContain('</project>');
      // XML convention should not include markdown header
      expect(result.content.startsWith('# CLAUDE.md')).toBe(false);
    });

    it('should default to markdown when no convention specified', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'identity',
            content: {
              type: 'TextContent',
              value: 'Test project',
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };
      const result = formatter.format(ast);
      expect(result.content).toContain('## Project');
    });
  });

  describe('format', () => {
    it('should always start with CLAUDE.md header', () => {
      const ast = createMinimalProgram();
      const result = formatter.format(ast);
      expect(result.path).toBe('CLAUDE.md');
      expect(result.content.startsWith('# CLAUDE.md\n')).toBe(true);
    });

    it('should generate project section from identity block', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'identity',
            content: {
              type: 'TextContent',
              value: 'Checkout microservice for e-commerce platform.\nMore details here.',
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };

      const result = formatter.format(ast);
      expect(result.content).toContain('## Project');
      expect(result.content).toContain('Checkout microservice for e-commerce platform.');
      // Full identity text is now included for completeness
      expect(result.content).toContain('More details here.');
    });

    it('should generate tech stack as comma-separated list', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'standards',
            content: {
              type: 'ObjectContent',
              properties: {
                code: {
                  languages: ['TypeScript'],
                  frameworks: ['React 18', 'TanStack Query'],
                  testing: ['Vitest'],
                },
              },
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };

      const result = formatter.format(ast);
      expect(result.content).toContain('## Tech Stack');
      expect(result.content).toContain('TypeScript, React 18, TanStack Query, Vitest');
    });

    it('should generate commands section as code block', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'shortcuts',
            content: {
              type: 'ObjectContent',
              properties: {
                '/review': 'Code review',
                '/test': 'Write tests',
                '/docs': 'Generate docs',
              },
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };

      const result = formatter.format(ast);
      expect(result.content).toContain('## Commands');
      expect(result.content).toContain('```\n');
      expect(result.content).toContain('/review');
      expect(result.content).toContain('Code review');
      expect(result.content).toContain('/test');
      expect(result.content).toContain('Write tests');
      expect(result.content).toContain('/docs');
      expect(result.content).toContain('Generate docs');
    });

    it('should show full command descriptions (first line only)', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'shortcuts',
            content: {
              type: 'ObjectContent',
              properties: {
                '/cmd': 'This is a very long description that is shown in full',
              },
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };

      const result = formatter.format(ast);
      expect(result.content).toContain('/cmd');
      // Full first line is shown (no truncation)
      expect(result.content).toContain('This is a very long description that is shown in full');
    });

    it('should generate code style section', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'standards',
            content: {
              type: 'ObjectContent',
              properties: {
                code: {
                  style: ['Functional React', 'Custom hooks for logic'],
                  patterns: ['Named exports only'],
                },
              },
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };

      const result = formatter.format(ast);
      expect(result.content).toContain('## Code Style');
      expect(result.content).toContain('- Functional React');
      expect(result.content).toContain('- Custom hooks for logic');
      expect(result.content).toContain('- Named exports only');
    });

    it("should generate don'ts section from restrictions", () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'restrictions',
            content: {
              type: 'ArrayContent',
              elements: ['No secrets in code', 'No any types'],
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };

      const result = formatter.format(ast);
      expect(result.content).toContain("## Don'ts");
      expect(result.content).toContain('- No secrets in code');
      expect(result.content).toContain('- No any types');
    });

    it("should handle text content for don'ts section", () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'restrictions',
            content: {
              type: 'TextContent',
              value: 'No secrets\nNo any types\n- Already has dash',
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };

      const result = formatter.format(ast);
      expect(result.content).toContain("## Don'ts");
      expect(result.content).toContain('- No secrets');
      expect(result.content).toContain('- No any types');
      expect(result.content).toContain('- Already has dash');
    });

    it('should skip empty sections', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'shortcuts',
            content: {
              type: 'ObjectContent',
              properties: {},
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };

      const result = formatter.format(ast);
      expect(result.content).not.toContain('## Commands');
    });

    it('should handle single value arrays', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'standards',
            content: {
              type: 'ObjectContent',
              properties: {
                code: {
                  languages: 'TypeScript',
                  frameworks: 'React',
                },
              },
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };

      const result = formatter.format(ast);
      expect(result.content).toContain('TypeScript, React');
    });

    it('should skip donts section when content is ObjectContent', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'restrictions',
            content: {
              type: 'ObjectContent',
              properties: {
                key: 'value',
              },
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };

      const result = formatter.format(ast);
      expect(result.content).not.toContain("## Don'ts");
    });

    it('should extract donts items from ObjectContent with items array', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'restrictions',
            content: {
              type: 'ObjectContent',
              properties: {
                items: ['Never use any type', 'Never skip tests', 'Never commit secrets'],
              },
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };

      const result = formatter.format(ast);
      expect(result.content).toContain("## Don'ts");
      // Claude formatter transforms "Never" to "Don't"
      expect(result.content).toContain("- Don't use any type");
      expect(result.content).toContain("- Don't skip tests");
      expect(result.content).toContain("- Don't commit secrets");
    });
  });

  describe('version support', () => {
    beforeEach(() => {
      vi.useFakeTimers();
      vi.setSystemTime(new Date('2024-01-01T00:00:00Z'));
    });

    afterEach(() => {
      vi.useRealTimers();
    });

    it('should export CLAUDE_VERSIONS', () => {
      expect(CLAUDE_VERSIONS).toBeDefined();
      expect(CLAUDE_VERSIONS.simple).toBeDefined();
      expect(CLAUDE_VERSIONS.multifile).toBeDefined();
      expect(CLAUDE_VERSIONS.full).toBeDefined();
    });

    it('should have static getSupportedVersions method', () => {
      expect(ClaudeFormatter.getSupportedVersions).toBeDefined();
      expect(ClaudeFormatter.getSupportedVersions()).toBe(CLAUDE_VERSIONS);
    });

    it('should use full mode by default', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'skills',
            content: {
              type: 'ObjectContent',
              properties: {
                commit: {
                  description: 'Create git commits',
                  content: 'Instructions for commit skill...',
                },
              },
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };
      const result = formatter.format(ast);
      // Full mode produces skill files; simple mode would not
      expect(result.additionalFiles).toBeDefined();
      const skillFile = result.additionalFiles?.find((f) => f.path.includes('skills/'));
      expect(skillFile).toBeDefined();
    });

    it('should default to full mode for unknown version', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'skills',
            content: {
              type: 'ObjectContent',
              properties: {
                commit: {
                  description: 'Create git commits',
                  content: 'Instructions for commit skill...',
                },
              },
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };
      const result = formatter.format(ast, { version: 'unknown-version' });
      // Unknown versions fall back to full mode, which produces skill files
      expect(result.additionalFiles).toBeDefined();
      const skillFile = result.additionalFiles?.find((f) => f.path.includes('skills/'));
      expect(skillFile).toBeDefined();
    });

    it('should skip markdown header with XML convention in multifile mode', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'identity',
            content: {
              type: 'TextContent',
              value: 'Test project',
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };
      const result = formatter.format(ast, { version: 'multifile', convention: 'xml' });
      expect(result.content).not.toContain('# CLAUDE.md');
      expect(result.content).toContain('<project>');
    });

    it('should skip markdown header with XML convention in full mode', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'identity',
            content: {
              type: 'TextContent',
              value: 'Test project',
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };
      const result = formatter.format(ast, { version: 'full', convention: 'xml' });
      expect(result.content).not.toContain('# CLAUDE.md');
      expect(result.content).toContain('<project>');
    });

    it('should generate rule files in multifile mode with guards', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'guards',
            content: {
              type: 'ObjectContent',
              properties: {
                globs: ['**/*.ts', '**/*.tsx'],
              },
              loc: createLoc(),
            },
            loc: createLoc(),
          },
          {
            type: 'Block',
            name: 'standards',
            content: {
              type: 'ObjectContent',
              properties: {
                typescript: { strictMode: true },
              },
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };

      const result = formatter.format(ast, { version: 'multifile' });
      expect(result.additionalFiles).toBeDefined();
      expect(result.additionalFiles?.length).toBeGreaterThan(0);
      const codeStyleRule = result.additionalFiles?.find((f) =>
        f.path.includes('.claude/rules/code-style.md')
      );
      expect(codeStyleRule).toBeDefined();
      expect(codeStyleRule?.content).toContain('paths:');
    });

    it('should generate skill files in full mode', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'identity',
            content: {
              type: 'TextContent',
              value: 'You are a helpful assistant.',
              loc: createLoc(),
            },
            loc: createLoc(),
          },
          {
            type: 'Block',
            name: 'skills',
            content: {
              type: 'ObjectContent',
              properties: {
                commit: {
                  description: 'Create git commits',
                  context: 'fork',
                  agent: 'general-purpose',
                  content: 'Instructions for commit skill...',
                },
              },
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };

      const result = formatter.format(ast, { version: 'full' });
      expect(result.additionalFiles).toBeDefined();
      const skillFile = result.additionalFiles?.find((f) =>
        f.path.includes('.claude/skills/commit/SKILL.md')
      );
      expect(skillFile).toBeDefined();
      expect(skillFile?.content).toContain("name: 'commit'");
      expect(skillFile?.content).toContain("description: 'Create git commits'");
      expect(skillFile?.content).toContain('context: fork');
      expect(skillFile?.content).toContain('agent: general-purpose');
    });

    it('should emit disableModelInvocation, argumentHint and model in skill frontmatter', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'skills',
            content: {
              type: 'ObjectContent',
              properties: {
                deploy: {
                  description: 'Deploy to production',
                  disableModelInvocation: true,
                  argumentHint: '<environment>',
                  model: 'opus',
                  content: 'Deploy instructions.',
                },
              },
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };

      const result = formatter.format(ast, { version: 'full' });
      const skillFile = result.additionalFiles?.find((f) =>
        f.path.includes('.claude/skills/deploy/SKILL.md')
      );
      expect(skillFile).toBeDefined();
      expect(skillFile?.content).toContain('disable-model-invocation: true');
      expect(skillFile?.content).toContain("argument-hint: '<environment>'");
      expect(skillFile?.content).toContain('model: opus');
    });

    it('should use raw frontmatter pass-through when __rawFrontmatter is present', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'skills',
            content: {
              type: 'ObjectContent',
              properties: {
                commit: {
                  description: 'Create git commits',
                  context: 'fork',
                  content: 'Instructions for commit skill...',
                  __rawFrontmatter:
                    "name: 'commit'\ndescription: 'Create git commits'\ncontext: fork\ncustom-field: preserved",
                },
              },
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };

      const result = formatter.format(ast, { version: 'full' });
      const skillFile = result.additionalFiles?.find((f) =>
        f.path.includes('.claude/skills/commit/SKILL.md')
      );
      expect(skillFile).toBeDefined();
      // Should use raw frontmatter instead of reconstructing
      expect(skillFile?.content).toContain('custom-field: preserved');
      expect(skillFile?.content).toContain("description: 'Create git commits'");
      // Verify frontmatter delimiters wrap raw content
      expect(skillFile?.content).toMatch(/^---\n.*custom-field: preserved.*\n---/s);
    });

    it('should emit resource files alongside skill SKILL.md in full mode', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'skills',
            content: {
              type: 'ObjectContent',
              properties: {
                'ui-skill': {
                  description: 'UI skill with resources',
                  content: 'Skill content.',
                  resources: [
                    { relativePath: 'data/colors.csv', content: 'red,#ff0000\n' },
                    { relativePath: 'scripts/search.py', content: 'print("hello")\n' },
                  ],
                },
              },
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };

      const result = formatter.format(ast, { version: 'full' });

      const skillFile = result.additionalFiles?.find((f) => f.path.endsWith('SKILL.md'));
      expect(skillFile).toBeDefined();
      expect(skillFile?.path).toBe('.claude/skills/ui-skill/SKILL.md');

      const resourceFiles = skillFile?.additionalFiles;
      expect(resourceFiles).toBeDefined();
      expect(resourceFiles).toHaveLength(2);

      const csv = resourceFiles?.find((f) => f.path.includes('colors.csv'));
      expect(csv?.path).toBe('.claude/skills/ui-skill/data/colors.csv');
      expect(csv?.content).toBe('red,#ff0000\n');

      const py = resourceFiles?.find((f) => f.path.includes('search.py'));
      expect(py?.path).toBe('.claude/skills/ui-skill/scripts/search.py');
      expect(py?.content).toBe('print("hello")\n');
    });

    it('should generate CLAUDE.local.md in full mode with @local block', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'identity',
            content: {
              type: 'TextContent',
              value: 'You are a helpful assistant.',
              loc: createLoc(),
            },
            loc: createLoc(),
          },
          {
            type: 'Block',
            name: 'local',
            content: {
              type: 'TextContent',
              value: 'Private instructions for local development.',
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };

      const result = formatter.format(ast, { version: 'full' });
      expect(result.additionalFiles).toBeDefined();
      const localFile = result.additionalFiles?.find((f) => f.path === 'CLAUDE.local.md');
      expect(localFile).toBeDefined();
      expect(localFile?.content).toContain('# CLAUDE.local.md');
      expect(localFile?.content).toContain('Private instructions');
      expect(localFile?.content).toContain('Private instructions for local development.');

      // Main file must include the @import directive so Claude Code loads the local file
      expect(result.content).toContain('@CLAUDE.local.md');
    });

    it('should not generate local file when @local block is missing', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'identity',
            content: {
              type: 'TextContent',
              value: 'You are a helpful assistant.',
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };

      const result = formatter.format(ast, { version: 'full' });
      const localFile = result.additionalFiles?.find((f) => f.path === 'CLAUDE.local.md');
      expect(localFile).toBeUndefined();
      expect(result.content).not.toContain('@CLAUDE.local.md');
    });

    describe('agent file generation', () => {
      it('should generate agent files in full mode with @agents block', () => {
        const ast: Program = {
          ...createMinimalProgram(),
          blocks: [
            {
              type: 'Block',
              name: 'identity',
              content: {
                type: 'TextContent',
                value: 'You are a helpful assistant.',
                loc: createLoc(),
              },
              loc: createLoc(),
            },
            {
              type: 'Block',
              name: 'agents',
              content: {
                type: 'ObjectContent',
                properties: {
                  'code-reviewer': {
                    description: 'Reviews code for quality and best practices',
                    tools: ['Read', 'Grep', 'Glob', 'Bash'],
                    model: 'sonnet',
                    content: 'You are a senior code reviewer.',
                  },
                },
                loc: createLoc(),
              },
              loc: createLoc(),
            },
          ],
        };

        const result = formatter.format(ast, { version: 'full' });
        expect(result.additionalFiles).toBeDefined();
        const agentFile = result.additionalFiles?.find((f) =>
          f.path.includes('.claude/agents/code-reviewer.md')
        );
        expect(agentFile).toBeDefined();
        expect(agentFile?.content).toContain('name: code-reviewer');
        expect(agentFile?.content).toContain(
          'description: Reviews code for quality and best practices'
        );
        expect(agentFile?.content).toContain("tools: ['Read', 'Grep', 'Glob', 'Bash']");
        expect(agentFile?.content).toContain('model: sonnet');
        expect(agentFile?.content).toContain('You are a senior code reviewer.');
      });

      it('should generate agent with all supported fields', () => {
        const ast: Program = {
          ...createMinimalProgram(),
          blocks: [
            {
              type: 'Block',
              name: 'agents',
              content: {
                type: 'ObjectContent',
                properties: {
                  'db-reader': {
                    description: 'Execute read-only database queries',
                    tools: ['Bash', 'Read'],
                    disallowedTools: ['Write', 'Edit'],
                    model: 'haiku',
                    permissionMode: 'dontAsk',
                    skills: ['sql-patterns', 'data-analysis'],
                    content: 'You are a database analyst with read-only access.',
                  },
                },
                loc: createLoc(),
              },
              loc: createLoc(),
            },
          ],
        };

        const result = formatter.format(ast, { version: 'full' });
        const agentFile = result.additionalFiles?.find((f) =>
          f.path.includes('.claude/agents/db-reader.md')
        );
        expect(agentFile).toBeDefined();
        expect(agentFile?.content).toContain('name: db-reader');
        expect(agentFile?.content).toContain("tools: ['Bash', 'Read']");
        expect(agentFile?.content).toContain("disallowedTools: ['Write', 'Edit']");
        expect(agentFile?.content).toContain('model: haiku');
        expect(agentFile?.content).toContain('permissionMode: dontAsk');
        expect(agentFile?.content).toContain('skills:');
        expect(agentFile?.content).toContain('  - sql-patterns');
        expect(agentFile?.content).toContain('  - data-analysis');
      });

      it('should emit maxTurns, memory, mcpServers, background and isolation in agent frontmatter', () => {
        const ast: Program = {
          ...createMinimalProgram(),
          blocks: [
            {
              type: 'Block',
              name: 'agents',
              content: {
                type: 'ObjectContent',
                properties: {
                  'bg-worker': {
                    description: 'Background data processor',
                    maxTurns: 50,
                    memory: 'project',
                    mcpServers: ['postgres', 'redis'],
                    background: true,
                    isolation: 'worktree',
                    content: 'Process data in the background.',
                  },
                },
                loc: createLoc(),
              },
              loc: createLoc(),
            },
          ],
        };

        const result = formatter.format(ast, { version: 'full' });
        const agentFile = result.additionalFiles?.find(
          (f) => f.path === '.claude/agents/bg-worker.md'
        );
        expect(agentFile).toBeDefined();
        expect(agentFile?.content).toContain('maxTurns: 50');
        expect(agentFile?.content).toContain('memory: project');
        expect(agentFile?.content).toContain("mcpServers: ['postgres', 'redis']");
        expect(agentFile?.content).toContain('background: true');
        expect(agentFile?.content).toContain('isolation: worktree');
      });

      it('should generate multiple agent files', () => {
        const ast: Program = {
          ...createMinimalProgram(),
          blocks: [
            {
              type: 'Block',
              name: 'agents',
              content: {
                type: 'ObjectContent',
                properties: {
                  'code-reviewer': {
                    description: 'Reviews code',
                    content: 'Review code carefully.',
                  },
                  debugger: {
                    description: 'Debug issues',
                    tools: ['Read', 'Edit', 'Bash'],
                    content: 'You are an expert debugger.',
                  },
                },
                loc: createLoc(),
              },
              loc: createLoc(),
            },
          ],
        };

        const result = formatter.format(ast, { version: 'full' });
        expect(result.additionalFiles).toBeDefined();

        const reviewerFile = result.additionalFiles?.find(
          (f) => f.path === '.claude/agents/code-reviewer.md'
        );
        const debuggerFile = result.additionalFiles?.find(
          (f) => f.path === '.claude/agents/debugger.md'
        );

        expect(reviewerFile).toBeDefined();
        expect(debuggerFile).toBeDefined();
        expect(debuggerFile?.content).toContain("tools: ['Read', 'Edit', 'Bash']");
      });

      it('should generate minimal agent with only required fields', () => {
        const ast: Program = {
          ...createMinimalProgram(),
          blocks: [
            {
              type: 'Block',
              name: 'agents',
              content: {
                type: 'ObjectContent',
                properties: {
                  'simple-agent': {
                    description: 'A simple agent',
                    content: 'Do simple tasks.',
                  },
                },
                loc: createLoc(),
              },
              loc: createLoc(),
            },
          ],
        };

        const result = formatter.format(ast, { version: 'full' });
        const agentFile = result.additionalFiles?.find(
          (f) => f.path === '.claude/agents/simple-agent.md'
        );
        expect(agentFile).toBeDefined();
        expect(agentFile?.content).toContain('name: simple-agent');
        expect(agentFile?.content).toContain('description: A simple agent');
        expect(agentFile?.content).not.toContain('tools:');
        expect(agentFile?.content).not.toContain('model:');
        expect(agentFile?.content).not.toContain('permissionMode:');
        expect(agentFile?.content).toContain('Do simple tasks.');
      });

      it('should serialize a single tool as a YAML array', () => {
        const ast: Program = {
          ...createMinimalProgram(),
          blocks: [
            {
              type: 'Block',
              name: 'agents',
              content: {
                type: 'ObjectContent',
                properties: {
                  'single-tool': {
                    description: 'Agent with one tool',
                    tools: ['Bash'],
                    model: 'sonnet',
                    content: { type: 'TextContent', value: 'Do one thing.' },
                  },
                },
                loc: { file: 'test.prs', line: 1, column: 1 },
              },
              loc: { file: 'test.prs', line: 1, column: 1 },
            },
          ],
        };

        const result = formatter.format(ast, { version: 'full' });
        const agentFile = result.additionalFiles?.find(
          (f) => f.path === '.claude/agents/single-tool.md'
        );
        expect(agentFile).toBeDefined();
        expect(agentFile?.content).toContain("tools: ['Bash']");
      });

      it('should not generate agent files in simple mode', () => {
        const ast: Program = {
          ...createMinimalProgram(),
          blocks: [
            {
              type: 'Block',
              name: 'agents',
              content: {
                type: 'ObjectContent',
                properties: {
                  'my-agent': {
                    description: 'An agent',
                    content: 'Agent content.',
                  },
                },
                loc: createLoc(),
              },
              loc: createLoc(),
            },
          ],
        };

        const result = formatter.format(ast, { version: 'simple' });
        expect(result.additionalFiles).toBeUndefined();
      });

      it('should not generate agent files in multifile mode', () => {
        const ast: Program = {
          ...createMinimalProgram(),
          blocks: [
            {
              type: 'Block',
              name: 'agents',
              content: {
                type: 'ObjectContent',
                properties: {
                  'my-agent': {
                    description: 'An agent',
                    content: 'Agent content.',
                  },
                },
                loc: createLoc(),
              },
              loc: createLoc(),
            },
          ],
        };

        const result = formatter.format(ast, { version: 'multifile' });
        const agentFile = result.additionalFiles?.find((f) => f.path.includes('.claude/agents/'));
        expect(agentFile).toBeUndefined();
      });

      it('should skip agents without description', () => {
        const ast: Program = {
          ...createMinimalProgram(),
          blocks: [
            {
              type: 'Block',
              name: 'agents',
              content: {
                type: 'ObjectContent',
                properties: {
                  'invalid-agent': {
                    content: 'No description provided.',
                  },
                  'valid-agent': {
                    description: 'Has description',
                    content: 'Agent content.',
                  },
                },
                loc: createLoc(),
              },
              loc: createLoc(),
            },
          ],
        };

        const result = formatter.format(ast, { version: 'full' });
        const invalidAgent = result.additionalFiles?.find(
          (f) => f.path === '.claude/agents/invalid-agent.md'
        );
        const validAgent = result.additionalFiles?.find(
          (f) => f.path === '.claude/agents/valid-agent.md'
        );

        expect(invalidAgent).toBeUndefined();
        expect(validAgent).toBeDefined();
      });

      it('should validate model values', () => {
        const ast: Program = {
          ...createMinimalProgram(),
          blocks: [
            {
              type: 'Block',
              name: 'agents',
              content: {
                type: 'ObjectContent',
                properties: {
                  'test-agent': {
                    description: 'Test agent',
                    model: 'invalid-model',
                    content: 'Test content.',
                  },
                },
                loc: createLoc(),
              },
              loc: createLoc(),
            },
          ],
        };

        const result = formatter.format(ast, { version: 'full' });
        const agentFile = result.additionalFiles?.find(
          (f) => f.path === '.claude/agents/test-agent.md'
        );
        expect(agentFile).toBeDefined();
        // Invalid model should be omitted
        expect(agentFile?.content).not.toContain('model:');
      });

      it('should validate permissionMode values', () => {
        const ast: Program = {
          ...createMinimalProgram(),
          blocks: [
            {
              type: 'Block',
              name: 'agents',
              content: {
                type: 'ObjectContent',
                properties: {
                  'agent-1': {
                    description: 'Valid permission',
                    permissionMode: 'acceptEdits',
                    content: 'Content.',
                  },
                  'agent-2': {
                    description: 'Invalid permission',
                    permissionMode: 'invalid-mode',
                    content: 'Content.',
                  },
                },
                loc: createLoc(),
              },
              loc: createLoc(),
            },
          ],
        };

        const result = formatter.format(ast, { version: 'full' });
        const agent1 = result.additionalFiles?.find((f) => f.path === '.claude/agents/agent-1.md');
        const agent2 = result.additionalFiles?.find((f) => f.path === '.claude/agents/agent-2.md');

        expect(agent1?.content).toContain('permissionMode: acceptEdits');
        expect(agent2?.content).not.toContain('permissionMode:');
      });

      it('should support all valid model types', () => {
        const ast: Program = {
          ...createMinimalProgram(),
          blocks: [
            {
              type: 'Block',
              name: 'agents',
              content: {
                type: 'ObjectContent',
                properties: {
                  'sonnet-agent': { description: 'Sonnet', model: 'sonnet', content: 'c' },
                  'opus-agent': { description: 'Opus', model: 'opus', content: 'c' },
                  'haiku-agent': { description: 'Haiku', model: 'haiku', content: 'c' },
                  'inherit-agent': { description: 'Inherit', model: 'inherit', content: 'c' },
                },
                loc: createLoc(),
              },
              loc: createLoc(),
            },
          ],
        };

        const result = formatter.format(ast, { version: 'full' });

        const sonnetAgent = result.additionalFiles?.find(
          (f) => f.path === '.claude/agents/sonnet-agent.md'
        );
        const opusAgent = result.additionalFiles?.find(
          (f) => f.path === '.claude/agents/opus-agent.md'
        );
        const haikuAgent = result.additionalFiles?.find(
          (f) => f.path === '.claude/agents/haiku-agent.md'
        );
        const inheritAgent = result.additionalFiles?.find(
          (f) => f.path === '.claude/agents/inherit-agent.md'
        );

        expect(sonnetAgent?.content).toContain('model: sonnet');
        expect(opusAgent?.content).toContain('model: opus');
        expect(haikuAgent?.content).toContain('model: haiku');
        expect(inheritAgent?.content).toContain('model: inherit');
      });
    });
  });

  describe('testing standards extraction for rule files', () => {
    it('should extract testing rules from standards block with nested object for rule file', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'standards',
            content: {
              type: 'ObjectContent',
              properties: {
                testing: {
                  framework: 'vitest',
                  coverage: 80,
                  pattern: 'AAA',
                },
              },
              loc: createLoc(),
            },
            loc: createLoc(),
          },
          {
            type: 'Block',
            name: 'guards',
            content: {
              type: 'ObjectContent',
              properties: {
                // globs as array, containing test patterns
                globs: ['**/*.spec.ts', '**/__tests__/**'],
              },
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };

      const result = formatter.format(ast, { version: 'full' });
      const testingRule = result.additionalFiles?.find((f) =>
        f.path.includes('.claude/rules/testing.md')
      );
      expect(testingRule).toBeDefined();
      expect(testingRule?.content).toContain('Use vitest for testing');
      expect(testingRule?.content).toContain('Target >80% coverage');
      expect(testingRule?.content).toContain('Follow AAA pattern');
    });

    it('should use default testing rule content when no testing standards in rule file', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'standards',
            content: {
              type: 'ObjectContent',
              properties: {
                code: ['Write clean code'],
              },
              loc: createLoc(),
            },
            loc: createLoc(),
          },
          {
            type: 'Block',
            name: 'guards',
            content: {
              type: 'ObjectContent',
              properties: {
                // globs as array, containing test patterns
                globs: ['**/*.spec.ts'],
              },
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };

      const result = formatter.format(ast, { version: 'full' });
      const testingRule = result.additionalFiles?.find((f) =>
        f.path.includes('.claude/rules/testing.md')
      );
      expect(testingRule).toBeDefined();
      // When no testing standards, default message should be used
      expect(testingRule?.content).toContain('testing conventions');
    });
  });

  describe('command file generation', () => {
    it('should generate command files in multifile mode for shortcuts with prompt: true', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'shortcuts',
            content: {
              type: 'ObjectContent',
              properties: {
                deploy: {
                  prompt: true,
                  description: 'Deploy the application',
                  content: 'Run deployment steps...',
                },
                '/review': 'Simple shortcut',
              },
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };

      const result = formatter.format(ast, { version: 'multifile' });
      expect(result.additionalFiles).toBeDefined();
      const commandFile = result.additionalFiles?.find(
        (f) => f.path === '.claude/commands/deploy.md'
      );
      expect(commandFile).toBeDefined();
      expect(commandFile?.content).toContain("description: 'Deploy the application'");
      expect(commandFile?.content).toContain('Run deployment steps...');

      // Simple string shortcuts should NOT generate command files
      const reviewFile = result.additionalFiles?.find(
        (f) => f.path === '.claude/commands/review.md'
      );
      expect(reviewFile).toBeUndefined();
    });

    it('should generate command files in full mode', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'shortcuts',
            content: {
              type: 'ObjectContent',
              properties: {
                test: {
                  content: 'Run all tests with coverage',
                },
              },
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };

      const result = formatter.format(ast, { version: 'full' });
      expect(result.additionalFiles).toBeDefined();
      const commandFile = result.additionalFiles?.find(
        (f) => f.path === '.claude/commands/test.md'
      );
      expect(commandFile).toBeDefined();
      expect(commandFile?.content).toContain('Run all tests with coverage');
    });

    it('should not generate command files in simple mode', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'shortcuts',
            content: {
              type: 'ObjectContent',
              properties: {
                deploy: {
                  prompt: true,
                  description: 'Deploy',
                  content: 'Deploy steps...',
                },
              },
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };

      const result = formatter.format(ast, { version: 'simple' });
      expect(result.additionalFiles).toBeUndefined();
    });

    it('should strip leading slashes from command names', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'shortcuts',
            content: {
              type: 'ObjectContent',
              properties: {
                '/deploy': {
                  prompt: true,
                  description: 'Deploy',
                  content: 'Deploy steps...',
                },
              },
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };

      const result = formatter.format(ast, { version: 'multifile' });
      const commandFile = result.additionalFiles?.find(
        (f) => f.path === '.claude/commands/deploy.md'
      );
      expect(commandFile).toBeDefined();
    });

    it('should use command name as description fallback', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'shortcuts',
            content: {
              type: 'ObjectContent',
              properties: {
                build: {
                  content: 'Build the project',
                },
              },
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };

      const result = formatter.format(ast, { version: 'multifile' });
      const commandFile = result.additionalFiles?.find(
        (f) => f.path === '.claude/commands/build.md'
      );
      expect(commandFile).toBeDefined();
      expect(commandFile?.content).toContain("description: 'build'");
    });

    it('should use double quotes when description contains apostrophe', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'shortcuts',
            content: {
              type: 'ObjectContent',
              properties: {
                test: {
                  prompt: true,
                  description: "Run the project's tests",
                  content: 'Test steps...',
                },
              },
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };

      const result = formatter.format(ast, { version: 'multifile' });
      const commandFile = result.additionalFiles?.find(
        (f) => f.path === '.claude/commands/test.md'
      );
      expect(commandFile).toBeDefined();
      expect(commandFile?.content).toContain('"Run the project\'s tests"');
    });

    it('should generate command for shortcut with content but no prompt flag', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'shortcuts',
            content: {
              type: 'ObjectContent',
              properties: {
                'monitor-ci': {
                  description: 'Monitor CI pipeline',
                  content: 'You are the CI monitor orchestrator.',
                },
              },
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };

      const result = formatter.format(ast, { version: 'full' });
      const commandFile = result.additionalFiles?.find(
        (f) => f.path === '.claude/commands/monitor-ci.md'
      );
      expect(commandFile).toBeDefined();
      expect(commandFile?.content).toContain("description: 'Monitor CI pipeline'");
      expect(commandFile?.content).toContain('You are the CI monitor orchestrator.');
    });
  });

  describe('examples section', () => {
    it('should render examples from @examples block', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'examples',
            content: {
              type: 'ObjectContent',
              properties: {
                'commit-message': {
                  input: 'Fix login bug',
                  output: 'fix(auth): resolve login redirect loop',
                  description: 'Shows conventional commit format',
                },
              },
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };

      const result = formatter.format(ast);
      expect(result.content).toContain('## Examples');
      expect(result.content).toContain('### Example: commit-message');
      expect(result.content).toContain('Shows conventional commit format');
      expect(result.content).toContain('**Input:**');
      expect(result.content).toContain('Fix login bug');
      expect(result.content).toContain('**Output:**');
      expect(result.content).toContain('fix(auth): resolve login redirect loop');
    });

    it('should not render examples section when no @examples block exists', () => {
      const ast = createMinimalProgram();
      const result = formatter.format(ast);
      expect(result.content).not.toContain('## Examples');
    });
  });

  describe('required context in rule files', () => {
    it('should render required context in generated rule files', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'guards',
            content: {
              type: 'ObjectContent',
              properties: {
                'auth-check': {
                  paths: ['src/auth/**/*.ts'],
                  description: 'Auth rules',
                  content: 'Always validate tokens',
                  __resolvedRequires: [
                    { name: 'api-validation', content: 'Validation rules content here...' },
                  ],
                },
              },
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };

      const result = formatter.format(ast, { version: 'multifile' });
      const ruleFile = result.additionalFiles?.find(
        (f) => f.path === '.claude/rules/auth-check.md'
      );
      expect(ruleFile).toBeDefined();
      expect(ruleFile?.content).toContain('## Required Context');
      expect(ruleFile?.content).toContain('### api-validation');
      expect(ruleFile?.content).toContain('Validation rules content here...');
    });
  });

  describe('required context sanitization', () => {
    it('should strip newlines from guard dependency name', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'guards',
            content: {
              type: 'ObjectContent',
              properties: {
                'my-guard': {
                  paths: ['src/**/*.ts'],
                  description: 'Guard rules',
                  content: 'Guard content',
                  __resolvedRequires: [{ name: 'evil\nname\nhere', content: 'some content' }],
                },
              },
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };

      const result = formatter.format(ast, { version: 'multifile' });
      const ruleFile = result.additionalFiles?.find((f) => f.path === '.claude/rules/my-guard.md');
      expect(ruleFile).toBeDefined();
      // The heading should not contain raw newlines
      expect(ruleFile?.content).toContain('### evil name here');
      expect(ruleFile?.content).not.toMatch(/### .*\n.*name/);
    });

    it('should strip --- from guard dependency name', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'guards',
            content: {
              type: 'ObjectContent',
              properties: {
                'my-guard': {
                  paths: ['src/**/*.ts'],
                  description: 'Guard rules',
                  content: 'Guard content',
                  __resolvedRequires: [{ name: 'bad---name', content: 'some content' }],
                },
              },
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };

      const result = formatter.format(ast, { version: 'multifile' });
      const ruleFile = result.additionalFiles?.find((f) => f.path === '.claude/rules/my-guard.md');
      expect(ruleFile).toBeDefined();
      // The heading should not contain raw ---
      expect(ruleFile?.content).not.toContain('### bad---name');
      expect(ruleFile?.content).toContain('### bad\u2014name');
    });

    it('should escape --- at start of guard dependency content', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'guards',
            content: {
              type: 'ObjectContent',
              properties: {
                'my-guard': {
                  paths: ['src/**/*.ts'],
                  description: 'Guard rules',
                  content: 'Guard content',
                  __resolvedRequires: [{ name: 'dep', content: '---\nfrontmatter: true\n---' }],
                },
              },
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };

      const result = formatter.format(ast, { version: 'multifile' });
      const ruleFile = result.additionalFiles?.find((f) => f.path === '.claude/rules/my-guard.md');
      expect(ruleFile).toBeDefined();
      // Content --- should be escaped
      expect(ruleFile?.content).toContain('\\---');
      // Should not have unescaped --- in the required context section (after the frontmatter block)
      const requiredContextIdx = ruleFile!.content.indexOf('## Required Context');
      const afterRequired = ruleFile!.content.substring(requiredContextIdx);
      expect(afterRequired).not.toMatch(/^---$/m);
    });

    it('should skip guard entries with path traversal names', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'guards',
            content: {
              type: 'ObjectContent',
              properties: {
                '../../../etc/passwd': {
                  paths: ['src/**/*.ts'],
                  description: 'Malicious guard',
                  content: 'Evil content',
                },
              },
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };

      const result = formatter.format(ast, { version: 'multifile' });
      // Should not generate a rule file for the malicious name
      const maliciousFile = result.additionalFiles?.find((f) => f.path.includes('passwd'));
      expect(maliciousFile).toBeUndefined();
    });

    it('should strip heading markers from guard dependency name', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'guards',
            content: {
              type: 'ObjectContent',
              properties: {
                'my-guard': {
                  paths: ['src/**/*.ts'],
                  description: 'Guard rules',
                  content: 'Guard content',
                  __resolvedRequires: [{ name: '## heading injection', content: 'some content' }],
                },
              },
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };

      const result = formatter.format(ast, { version: 'multifile' });
      const ruleFile = result.additionalFiles?.find((f) => f.path === '.claude/rules/my-guard.md');
      expect(ruleFile).toBeDefined();
      // Should not contain ## ## (double heading)
      expect(ruleFile?.content).not.toContain('### ## heading');
      expect(ruleFile?.content).toContain('### heading injection');
    });

    it('should filter out invalid __resolvedRequires entries at runtime', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'guards',
            content: {
              type: 'ObjectContent',
              properties: {
                'my-guard': {
                  paths: ['src/**/*.ts'],
                  description: 'Guard rules',
                  content: 'Guard content',
                  __resolvedRequires: [
                    null,
                    { name: 123, content: 'bad' },
                    { name: 'valid', content: 'Valid content' },
                  ],
                },
              },
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };

      const result = formatter.format(ast, { version: 'multifile' });
      const ruleFile = result.additionalFiles?.find((f) => f.path === '.claude/rules/my-guard.md');
      expect(ruleFile).toBeDefined();
      expect(ruleFile?.content).toContain('### valid');
      expect(ruleFile?.content).toContain('Valid content');
    });
  });

  describe('getSkillBasePath', () => {
    it('should return .claude/skills', () => {
      const formatter = new ClaudeFormatter();
      expect(formatter.getSkillBasePath()).toBe('.claude/skills');
    });
  });

  describe('getSkillFileName', () => {
    it('should return SKILL.md', () => {
      const formatter = new ClaudeFormatter();
      expect(formatter.getSkillFileName()).toBe('SKILL.md');
    });
  });

  describe('outputDir override for skills', () => {
    it('writes skill file to custom outputDir path', () => {
      const ast: Program = {
        ...createMinimalProgram(),
        blocks: [
          {
            type: 'Block',
            name: 'skills',
            content: {
              type: 'ObjectContent',
              properties: {
                deploy: {
                  description: 'Deploy skill',
                  content: 'Deploy instructions',
                  __outputDir: 'skills/custom/deploy',
                },
              },
              loc: createLoc(),
            },
            loc: createLoc(),
          },
        ],
      };

      const result = formatter.format(ast, { version: 'full' });
      const skillFile = result.additionalFiles?.find((f) => f.path.endsWith('SKILL.md'));
      expect(skillFile?.path).toBe('.claude/skills/custom/deploy/SKILL.md');
    });
  });
});
