/**
 * T3/T4 acceptance: IR-gate contract growth (TS2305/2304), targeted arity
 * widening (constructor / M>N only), and suite byte-identity when the error
 * is contract-owned.
 *
 * Workspaces live under the repo (not os.tmpdir) so tsc can upward-resolve
 * @types/node from this package's node_modules — same as e2e.mock.test.ts.
 */
import { describe, expect, it, beforeEach, afterEach } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import type { FeatureNode, JobState, LlmClient } from '../src/core/types';
import { runEncodingAndIR } from '../src/core/pipeline';
import { assembleTestFile } from '../src/core/irScaffold';
import { STUB_FIX_SYSTEM } from '../src/core/prompts';

const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');

function baseFeature(partial: Partial<FeatureNode> & { id: string }): FeatureNode {
  return {
    name: partial.id,
    description: 'x',
    parentId: null,
    deps: [],
    files: [`src/${partial.id.toLowerCase()}.ts`],
    testFile: `tests/${partial.id.toLowerCase()}.test.ts`,
    edgeCases: ['e1'],
    exports: [],
    envVars: [],
    stubs: [],
    ...partial,
  };
}

function freshJob(dir: string): JobState {
  return {
    id: 'ir-gate',
    title: '',
    prompt: 't',
    phase: 'created',
    createdAt: new Date().toISOString(),
    workspaceDir: dir,
    spec: null,
    manifest: null,
    buildResults: [],
    report: null,
    promptAssessment: null,
    irEscalations: [],
    error: null,
    events: [],
    llmUsage: [],
  };
}

function baseClient(overrides: Partial<LlmClient>): LlmClient {
  const boom = async () => { throw new Error('unexpected LLM call'); };
  return {
    enumerateFeatures: async () => [],
    probeFeature: async () => ({ clear: true, assertion: 'x'.repeat(20), question: '' }),
    encodeFeatures: boom as LlmClient['encodeFeatures'],
    repairFeatures: boom as LlmClient['repairFeatures'],
    fixStub: async () => 'export {}\n',
    generateTests: boom as LlmClient['generateTests'],
    fixTestCases: boom as LlmClient['fixTestCases'],
    generateImplementation: boom as LlmClient['generateImplementation'],
    repair: boom as LlmClient['repair'],
    ...overrides,
  };
}

describe('T3/T4 IR-gate contract routing', () => {
  let dir: string;

  beforeEach(() => {
    dir = path.join(ROOT, 'workspaces', `ir-gate-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`);
    fs.mkdirSync(path.dirname(dir), { recursive: true });
  });

  afterEach(() => {
    fs.rmSync(dir, { recursive: true, force: true });
  });

  it('TS2305 grows the stub; suite stays byte-identical', async () => {
    const feature = baseFeature({
      id: 'F2',
      files: ['src/revision_manager.ts'],
      testFile: 'tests/f2.test.ts',
      stubs: [{ path: 'src/revision_manager.ts', content: 'export interface Revision { id: string }\n' }],
    });
    const imports = [{ from: 'src/revision_manager', symbols: ['RevisionManager'] }];
    const cases = [{ name: 'constructs', kind: 'behavior' as const, body: 'expect(new RevisionManager()).toBeTruthy();' }];
    const suiteContent = assembleTestFile(feature, imports, cases);
    let fixStubCalls = 0;
    let touchedOtherStub = false;

    const llm = baseClient({
      encodeFeatures: async (prompt) => ({ title: 't', prompt, features: [feature] }),
      generateTests: async () => ({ content: suiteContent, parts: { imports, cases } }),
      fixStub: async (p, ctx) => {
        fixStubCalls++;
        if (p !== 'src/revision_manager.ts') touchedOtherStub = true;
        if (ctx.diagnostics.includes('RevisionManager')) {
          return `export interface Revision { id: string }\n\nexport class RevisionManager {\n  constructor() { throw new Error('stub: not implemented'); }\n}\n`;
        }
        return `export interface Revision { id: string }\n`;
      },
    });

    const job = freshJob(dir);
    await runEncodingAndIR(job, llm, () => {}, { skipGate: true });
    expect(job.phase).toBe('awaiting-ir-approval');
    expect(fixStubCalls).toBeGreaterThan(0);
    expect(touchedOtherStub).toBe(false);
    expect(fs.readFileSync(path.join(dir, 'tests/f2.test.ts'), 'utf8')).toBe(suiteContent);
    expect(fs.readFileSync(path.join(dir, 'src/revision_manager.ts'), 'utf8')).toContain('class RevisionManager');
  }, 60_000);

  it('TS2304 grows the feature\'s own stub', async () => {
    const feature = baseFeature({
      id: 'F5',
      files: ['src/links.ts'],
      testFile: 'tests/f5.test.ts',
      stubs: [{ path: 'src/links.ts', content: 'export interface Link { href: string }\n' }],
    });
    const imports: { from: string; symbols: string[] }[] = [];
    const cases = [{ name: 'validates', kind: 'behavior' as const, body: 'expect(linkValidator("https://x")).toBe(true);' }];
    const suiteContent = assembleTestFile(feature, imports, cases);
    let grew = false;

    const llm = baseClient({
      encodeFeatures: async (prompt) => ({ title: 't', prompt, features: [feature] }),
      generateTests: async () => ({ content: suiteContent, parts: { imports, cases } }),
      fixStub: async (_p, ctx) => {
        if (ctx.diagnostics.includes('linkValidator')) {
          grew = true;
          return `export interface Link { href: string }\n\nexport function linkValidator(_u: string): boolean { throw new Error('stub: not implemented'); }\n`;
        }
        return `export interface Link { href: string }\n`;
      },
    });

    const job = freshJob(dir);
    // Bare-name TS2304 still needs an import in the suite after the stub grows —
    // the gate may escalate; acceptance is that the contract was grown first.
    try {
      await runEncodingAndIR(job, llm, () => {}, { skipGate: true });
    } catch (e) {
      expect((e as Error).message).toMatch(/escalat/i);
    }
    expect(grew).toBe(true);
    expect(fs.readFileSync(path.join(dir, 'src/links.ts'), 'utf8')).toContain('linkValidator');
  }, 60_000);

  it('new C(x) adds a constructor; only the owning stub is widened', async () => {
    const feature = baseFeature({
      id: 'F3',
      files: ['src/revert.ts', 'src/other.ts'],
      testFile: 'tests/f3.test.ts',
      stubs: [
        {
          path: 'src/revert.ts',
          content: 'export class RevertService {\n  run(): void { throw new Error(\'stub: not implemented\'); }\n}\n',
        },
        {
          path: 'src/other.ts',
          content: 'export function helper(): void { throw new Error(\'stub: not implemented\'); }\n',
        },
      ],
    });
    const imports = [{ from: 'src/revert', symbols: ['RevertService'] }];
    const cases = [{
      name: 'constructs with dep',
      kind: 'behavior' as const,
      body: 'const rm = {} as any;\nexpect(new RevertService(rm)).toBeTruthy();',
    }];
    const suiteContent = assembleTestFile(feature, imports, cases);
    const otherBefore = feature.stubs[1].content;
    const touched = new Set<string>();

    const llm = baseClient({
      encodeFeatures: async (prompt) => ({ title: 't', prompt, features: [feature] }),
      generateTests: async () => ({ content: suiteContent, parts: { imports, cases } }),
      fixStub: async (p, ctx) => {
        touched.add(p);
        if (ctx.diagnostics.includes('WIDEN') || ctx.diagnostics.includes('RevertService') || ctx.diagnostics.includes('Expected')) {
          return 'export class RevertService {\n  constructor(_rm: unknown) { throw new Error(\'stub: not implemented\'); }\n  run(): void { throw new Error(\'stub: not implemented\'); }\n}\n';
        }
        return 'export class RevertService {\n  run(): void { throw new Error(\'stub: not implemented\'); }\n}\n';
      },
    });

    const job = freshJob(dir);
    await runEncodingAndIR(job, llm, () => {}, { skipGate: true });
    expect(job.phase).toBe('awaiting-ir-approval');
    expect(touched.has('src/revert.ts')).toBe(true);
    expect(touched.has('src/other.ts')).toBe(false);
    expect(fs.readFileSync(path.join(dir, 'src/revert.ts'), 'utf8')).toMatch(/constructor\s*\(/);
    expect(fs.readFileSync(path.join(dir, 'src/other.ts'), 'utf8')).toBe(otherBefore);
  }, 60_000);

  it('PRE-FREEZE prompt licenses constructor add and rejects fewer-args widening', () => {
    expect(STUB_FIX_SYSTEM).toContain('new C(...)');
    expect(STUB_FIX_SYSTEM).toContain('ADD one');
    expect(STUB_FIX_SYSTEM).toMatch(/widen|WIDENING/i);
  });
});
