import { describe, it, expect, vi, afterEach } from 'vitest';
import { isAgentRegistryMarkdown, parseAgentsMd } from '../agents.js';

describe('isAgentRegistryMarkdown', () => {
  it('rejects Cursor navigation AGENTS.md at repo root', () => {
    expect(
      isAgentRegistryMarkdown('# Agent navigation\n\n## Doc map\n'),
    ).toBe(false);
  });

  it('accepts the global data/AGENTS.md registry shape', () => {
    expect(isAgentRegistryMarkdown('# Agents\n\n## Code Reviewer\n---\n')).toBe(true);
  });
});

describe('parseAgentsMd', () => {
  it('does not emit errors for navigation sections when file is skipped upstream', () => {
    // When isAgentRegistryMarkdown returns false, getAgentsForProject never calls this.
    // Sanity: a nav-shaped file would produce six "missing fence" errors if parsed.
    const nav = `# Agent navigation

## Doc map
| Need | Read |
|------|------|

## Task routing
Start here
`;
    const r = parseAgentsMd(nav);
    expect(r.agents).toHaveLength(0);
    expect(r.errors.length).toBeGreaterThan(0);
  });
});

// v2.6 sampling-streamjson-tokens (#11): per-agent llama.cpp sampler extensions.
describe('parseAgentsMd: v2.6 sampling knobs', () => {
  afterEach(() => {
    vi.restoreAllMocks();
  });

  const withFrontmatter = (lines: string) => `# Agents

## Sampler
---
temperature: 0.6
${lines}
tools: [view_file]
description: test
---
You sample.
`;

  it('parses top_n_sigma and the dry_* family from frontmatter', () => {
    const md = withFrontmatter(
      [
        'top_n_sigma: 1.5',
        'dry_multiplier: 0.8',
        'dry_base: 1.75',
        'dry_allowed_length: 2',
        'dry_penalty_last_n: -1',
      ].join('\n'),
    );
    const { agents, errors } = parseAgentsMd(md);
    expect(errors).toHaveLength(0);
    expect(agents).toHaveLength(1);
    const a = agents[0]!;
    expect(a.top_n_sigma).toBe(1.5);
    expect(a.dry_multiplier).toBe(0.8);
    expect(a.dry_base).toBe(1.75);
    expect(a.dry_allowed_length).toBe(2);
    expect(a.dry_penalty_last_n).toBe(-1);
  });

  it('defaults the new sampler fields to null when omitted', () => {
    const { agents } = parseAgentsMd(withFrontmatter('top_p: 0.95'));
    const a = agents[0]!;
    expect(a.top_n_sigma).toBeNull();
    expect(a.dry_multiplier).toBeNull();
    expect(a.dry_base).toBeNull();
    expect(a.dry_allowed_length).toBeNull();
    expect(a.dry_penalty_last_n).toBeNull();
  });

  it('warns (does not error) on out-of-range top_n_sigma / dry_* values', () => {
    const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
    const md = withFrontmatter(
      [
        'top_n_sigma: -1',
        'dry_multiplier: -0.5',
        'dry_base: -2',
        'dry_allowed_length: -3',
        'dry_penalty_last_n: -5',
      ].join('\n'),
    );
    const { agents, errors } = parseAgentsMd(md);
    expect(errors).toHaveLength(0);
    expect(agents).toHaveLength(1);
    // Mirrors top_k/min_p: out-of-range still stored, with a warning.
    expect(warn).toHaveBeenCalled();
    const warnings = warn.mock.calls.map((c) => String(c[0])).join('\n');
    expect(warnings).toContain('top_n_sigma');
    expect(warnings).toContain('dry_multiplier');
    expect(warnings).toContain('dry_base');
    expect(warnings).toContain('dry_allowed_length');
    expect(warnings).toContain('dry_penalty_last_n');
  });

  it('errors on non-numeric / non-integer sampler values', () => {
    const md = withFrontmatter(
      ['top_n_sigma: high', 'dry_allowed_length: 2.5'].join('\n'),
    );
    const { errors } = parseAgentsMd(md);
    const joined = errors.map((e) => e.reason).join('\n');
    expect(joined).toContain('top_n_sigma must be a number');
    expect(joined).toContain('dry_allowed_length must be an integer');
  });
});
