/**
 * SET 43c — Export-conformance at the vet gate (mechanical, 0 tokens).
 */
import { describe, expect, it } from 'vitest';
import type { GeneratedFile } from '../src/core/types';
import {
  conformExportsToStubs,
  diffExportsVsStubs,
  parseExportedFunctions,
} from '../src/tier1/exportConformance';

describe('SET 43c parseExportedFunctions', () => {
  it('reads function arity and named re-export aliases', () => {
    const content = `
export function add(a: number, b: number): number { return a + b; }
export const mul = (x: number, y: number) => x * y;
export { mul as product };
`;
    const parsed = parseExportedFunctions(content);
    expect(parsed.find(p => p.symbol === 'add')).toMatchObject({ arity: 2, kind: 'function' });
    expect(parsed.find(p => p.symbol === 'mul')).toMatchObject({ arity: 2 });
    expect(parsed.some(p => p.symbol === 'product')).toBe(true);
  });
});

describe('SET 43c conformExportsToStubs', () => {
  it('missing export → conform adds it', () => {
    const stubs: GeneratedFile[] = [{
      path: 'src/calc.ts',
      content: 'export function add(a: number, b: number): number { throw new Error("stub"); }\n',
    }];
    // No exports at all — unique single-extra rename must not fire.
    const impl: GeneratedFile[] = [{
      path: 'src/calc.ts',
      content: 'const internal = 1;\nexport {};\n',
    }];

    const diffs = diffExportsVsStubs(impl, stubs);
    expect(diffs).toEqual([
      expect.objectContaining({ kind: 'missing', symbol: 'add', stubFile: 'src/calc.ts' }),
    ]);

    const { files, fixes } = conformExportsToStubs(impl, stubs);
    expect(fixes.some(f => f.note === 'added missing export add')).toBe(true);

    const out = files.find(f => f.path === 'src/calc.ts')!.content;
    expect(out).toContain("export function add(...args: unknown[]): unknown { throw new Error('stub'); }");
    expect(parseExportedFunctions(out).some(p => p.symbol === 'add')).toBe(true);
  });

  it('renamed (impl has bar, stub wants foo; bar only export) → alias added', () => {
    const stubs: GeneratedFile[] = [{
      path: 'src/mod.ts',
      content: 'export function foo(x: number): number { throw new Error("stub"); }\n',
    }];
    const impl: GeneratedFile[] = [{
      path: 'src/mod.ts',
      content: 'export function bar(x: number): number { return x + 1; }\n',
    }];

    const diffs = diffExportsVsStubs(impl, stubs);
    expect(diffs).toEqual([
      expect.objectContaining({
        kind: 'renamed',
        stubSymbol: 'foo',
        implSymbol: 'bar',
        stubFile: 'src/mod.ts',
        implFile: 'src/mod.ts',
      }),
    ]);

    const { files, fixes } = conformExportsToStubs(impl, stubs);
    expect(fixes.some(f => f.note.includes('aliased bar as foo'))).toBe(true);
    const out = files.find(f => f.path === 'src/mod.ts')!.content;
    expect(out).toMatch(/export\s*\{\s*bar\s+as\s+foo\s*\}/);
    expect(parseExportedFunctions(out).map(p => p.symbol).sort()).toEqual(['bar', 'foo']);
  });

  it('already conforming → no-op', () => {
    const content = 'export function add(a: number, b: number): number { return a + b; }\n';
    const stubs: GeneratedFile[] = [{ path: 'src/calc.ts', content: content.replace('return a + b', 'throw new Error("stub")') }];
    const impl: GeneratedFile[] = [{ path: 'src/calc.ts', content }];

    expect(diffExportsVsStubs(impl, stubs)).toEqual([]);
    const { files, fixes } = conformExportsToStubs(impl, stubs);
    expect(fixes).toEqual([]);
    expect(files.find(f => f.path === 'src/calc.ts')!.content).toBe(content);
  });

  it('undefined-symbol class avoided: after conform, stub symbols all present', () => {
    const stubs: GeneratedFile[] = [
      {
        path: 'src/a.ts',
        content: [
          'export function encodeId(n: number): string { throw new Error("stub"); }',
          'export function decodeId(s: string): number { throw new Error("stub"); }',
          '',
        ].join('\n'),
      },
      {
        path: 'src/b.ts',
        content: 'export function format(x: string): string { throw new Error("stub"); }\n',
      },
    ];
    const impl: GeneratedFile[] = [
      {
        path: 'src/a.ts',
        // rename + missing: encodeImpl only; decodeId absent
        content: 'export function encodeImpl(n: number): string { return String(n); }\n',
      },
      {
        path: 'src/b.ts',
        content: 'export function format(x: string): string { return x.trim(); }\n',
      },
    ];

    const { files } = conformExportsToStubs(impl, stubs);
    for (const stub of stubs) {
      const stubSyms = parseExportedFunctions(stub.content).map(p => p.symbol);
      const implFile = files.find(f => f.path === stub.path);
      expect(implFile, `impl for ${stub.path}`).toBeTruthy();
      const implSyms = new Set(parseExportedFunctions(implFile!.content).map(p => p.symbol));
      for (const sym of stubSyms) {
        expect(implSyms.has(sym), `${sym} missing from ${stub.path} after conform`).toBe(true);
      }
    }
    // No remaining missing/renamed diffs against stubs.
    const residual = diffExportsVsStubs(files, stubs).filter(d => d.kind === 'missing' || d.kind === 'renamed');
    expect(residual).toEqual([]);
  });
});
