/**
 * SET 36 — Combat round 2: attribution, vet gate, build superbuffer,
 * stall-adaptive entropy, assertion diffs, seam-dump hygiene.
 */
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
import type { FeatureSpec, GeneratedFile, LlmClient } from '../src/core/types';
import { buildUser } from '../src/core/prompts';
import {
  defaultBatchParams,
  escalateBatchParams,
  escalateEntropyEvent,
  ESCALATED_TEMP,
} from '../src/tier1/permutation';
import {
  formatAttributionMissEvidence,
  programStateHash,
  shouldEmitSeamDumps,
} from '../src/tier1/reportEvidence';
import { runTier1Job } from '../src/tier1/runner';
import { classifyFailure, formatAssertionDiff } from '../src/tier1/substrate';
import { assembleSuperbuffer } from '../src/tier1/superbuffer';
import {
  assembleTestProgram,
  attributeEvalCrashDetailed,
  findTranspilePlaceholders,
  formatExecutionCrash,
  locateProgramLine,
  makeEvalSuiteRunner,
  parseExecutionCrash,
  vetArtifactWrites,
} from '../src/tier1/testExec';

function baseSpec(overrides?: Partial<FeatureSpec['features'][0]>): FeatureSpec {
  return {
    title: 'S36',
    prompt: 'set36',
    features: [{
      id: 'F1',
      name: 'mod',
      description: 'mod',
      parentId: null,
      deps: [],
      files: ['src/a.ts', 'src/b.ts', 'src/c.ts'],
      testFile: 'tests/f1.test.ts',
      edgeCases: [],
      envVars: [],
      exports: [{ file: 'src/c.ts', symbol: 'c' }],
      stubs: [
        { path: 'src/a.ts', content: 'export function a(): number { return 1; }\n' },
        { path: 'src/b.ts', content: 'export function b(): number { return 2; }\n' },
        { path: 'src/c.ts', content: 'export function c(): number { return 3; }\n' },
      ],
      ...overrides,
    }],
  } as FeatureSpec;
}

describe('SET 36a program-line → module attribution', () => {
  it('load-time throw in module 3 reports file:localLine (not unknown)', async () => {
    const files: GeneratedFile[] = [
      { path: 'src/a.ts', content: 'export function a(): number { return 1; }\n' },
      { path: 'src/b.ts', content: 'export function b(): number { return 2; }\n' },
      {
        path: 'src/c.ts',
        content: 'export function c(): number { throw new Error("boom-at-load"); }\nthrow new Error("load-detonate");\n',
      },
    ];
    const test: GeneratedFile = {
      path: 'tests/f1.test.ts',
      content: [
        "import { describe, it, expect } from 'vitest';",
        "import { c } from '../src/c';",
        "describe('t', () => { it('x', () => { expect(c()).toBe(3); }); });",
      ].join('\n'),
    };
    const { spans } = assembleTestProgram(files, test);
    expect(spans.map(s => s.path)).toEqual([
      'src/a.ts', 'src/b.ts', 'src/c.ts', 'tests/f1.test.ts',
    ]);
    const cSpan = spans.find(s => s.path === 'src/c.ts')!;
    const mid = Math.floor((cSpan.startLine + cSpan.endLine) / 2);
    const loc = locateProgramLine(spans, mid);
    expect(loc?.path).toBe('src/c.ts');

    const runner = makeEvalSuiteRunner();
    const r = await runner(files, test);
    expect(r.crashed).toBeTruthy();
    // Prefer named loading attribution; accept executing with module key as fallback.
    expect(r.crashed!).toMatch(/crash while (loading src\/c\.ts:\d+|executing src\/c #)/);
    expect(r.crashed!).not.toMatch(/\(unknown\)/);
    const parsed = parseExecutionCrash(r.crashed!);
    expect(parsed?.module).toMatch(/c/);
  });

  it('formatExecutionCrash maps stack line via table', () => {
    const spans = [
      { path: 'src/a.ts', key: 'src/a', startLine: 10, endLine: 20 },
      { path: 'src/b.ts', key: 'src/b', startLine: 21, endLine: 40 },
    ];
    const err = new Error('x');
    err.stack = 'Error: x\n    at eval (<anonymous>:25:1)\n    at Object.fn (t:2:3)';
    const msg = formatExecutionCrash(err, null, spans);
    expect(msg).toMatch(/^crash while loading src\/b\.ts:5/);
  });
});

describe('SET 36b placeholder-aware attribution', () => {
  it('artifact with one placeholder attributes crash to it without probes', () => {
    const files: GeneratedFile[] = [
      { path: 'src/ok.ts', content: 'export function ok(): number { return 1; }\n' },
      { path: 'src/i18n_formatter.ts', content: 'export function fmt(x: string): string {\n  const broken = {\n' },
    ];
    expect(findTranspilePlaceholders(files).map(f => f.path)).toEqual(['src/i18n_formatter.ts']);
    const test: GeneratedFile = {
      path: 'tests/t.test.ts',
      content: "import { describe, it, expect } from 'vitest';\ndescribe('t', () => { it('x', () => { expect(1).toBe(1); }); });\n",
    };
    const attr = attributeEvalCrashDetailed(files, test, 'something exploded at runtime');
    expect(attr.file).toBe('src/i18n_formatter.ts');
  });
});

describe('SET 36c vet every artifact write', () => {
  it('reset candidate with syntax error is rejected; incumbent bytes identical', async () => {
    // Dual `0` literals block SET 43b unique constant-replace so this case
    // still exercises reset-draw rejection (not spec-constant heal).
    const incumbent = 'export function e(): number { const _pad = 0; return 0; }\n';
    const spec: FeatureSpec = {
      title: 'E', prompt: 'e',
      features: [{
        id: 'F1', name: 'e', description: 'e', parentId: null,
        deps: [], files: ['src/e.ts'], testFile: 'tests/f1.test.ts',
        edgeCases: [], envVars: [],
        exports: [{ file: 'src/e.ts', symbol: 'e' }],
        stubs: [{ path: 'src/e.ts', content: 'export function e(): number { throw new Error("stub"); }\n' }],
      }],
    } as unknown as FeatureSpec;

    let fresh = 0;
    const llm: LlmClient = {
      async enumerateFeatures() { return []; },
      async probeFeature() { return { clear: true, assertion: 'x', question: '' }; },
      async encodeFeatures() { return spec; },
      async repairFeatures() { return []; },
      async fixStub() { return ''; },
      async generateTests() {
        return {
          content: '',
          parts: {
            imports: [{ from: 'src/e', symbols: ['e'] }],
            cases: [{ name: 'ok', kind: 'behavior', body: 'expect(e()).toBe(1);' }],
          },
        };
      },
      async fixTestCases(_s, _f, broken) {
        return { cases: broken.map(b => ({ name: b.name, kind: 'behavior' as const, body: 'expect(e()).toBe(1);' })) };
      },
      async generateImplementation() {
        fresh++;
        if (fresh === 1) return [{ path: 'src/e.ts', content: incumbent }];
        // Entropy-reset draw — syntax poison.
        return [{ path: 'src/e.ts', content: 'export function e(): number { return {\n' }];
      },
      async repair() {
        return [{ path: 'src/e.ts', content: incumbent }];
      },
    };

    const res = await runTier1Job({
      prompt: 'e', llm, runSuite: makeEvalSuiteRunner(),
      gates: { maxPasses: 4, stallLimit: 1, progressStall: 1 },
    });
    expect(res.log.some(e => /reset draw rejected:/.test(e.message))).toBe(true);
    const final = res.files.find(f => f.path === 'src/e.ts')?.content ?? '';
    // SET 73 — shell-lock re-emits accepted builds through the deterministic
    // frame, so the incumbent is CANONICALLY formatted, not byte-echoed. The
    // invariant under test is unchanged: a rejected reset leaves the
    // incumbent's code intact (same statements, no draw content adopted).
    const norm = (s: string) => s.replace(/\s+/g, ' ').trim();
    expect(norm(final)).toBe(norm(incumbent));

    const vet = vetArtifactWrites(
      [{ path: 'src/e.ts', content: 'export function e(): number { return {\n' }],
      [{ path: 'src/e.ts', content: incumbent }],
    );
    expect(vet.rejected.length).toBe(1);
    expect(vet.rejected[0]!.reason).toMatch(/transpile failed/);
  });
});

describe('SET 36d build-stage superbuffer + LLM decoupling', () => {
  it('buildUser packs superbuffer context under the contract', () => {
    const spec = baseSpec({
      files: ['src/c.ts'],
      exports: [{ file: 'src/c.ts', symbol: 'c' }],
      stubs: [{ path: 'src/c.ts', content: 'export function c(): number { throw new Error("stub"); }\n' }],
    });
    const feature = spec.features[0]!;
    const assembly = assembleSuperbuffer({
      spec,
      feature,
      parts: {
        imports: [],
        cases: [{ name: 'ok', kind: 'behavior', body: 'expect(c()).toBe(3);' }],
      },
      stubs: feature.stubs,
      lessons: [],
      siblingParts: new Map(),
      siblingImpls: [{ path: 'src/sib.ts', content: 'export function sib(): number { return 9; }\n' }],
      target: `build|${feature.id}`,
      signature: { name: 'c', params: [], returnType: 'number' },
    });
    expect(assembly.logLine).toMatch(/^superbuffer: assembled/);
    const user = buildUser(spec, feature, 'expect(c()).toBe(3);', [], undefined, assembly.promptBlock);
    expect(user).toContain('Superbuffer');
    expect(user).toContain('Sibling impl src/sib.ts');
  });

  it('LLM client holds NO conversation state between calls (each request self-contained)', () => {
    // Pin the decoupling invariant Andy asked about: LocalLlmClient.call always
    // builds a fresh [system, user] messages array — never this.history /
    // conversation accumulation.
    const src = readFileSync(new URL('../src/core/localLlm.ts', import.meta.url), 'utf8');
    expect(src).toMatch(/messages:\s*\[\s*\{\s*role:\s*'system',\s*content:\s*system\s*\},\s*\{\s*role:\s*'user',\s*content:\s*user\s*\}/);
    expect(src).not.toMatch(/this\.history|this\.messages\.push|conversationHistory|chatHistory/);
    // Two buildUser calls with distinct context stay independent.
    const spec = baseSpec({
      files: ['src/c.ts'],
      exports: [{ file: 'src/c.ts', symbol: 'c' }],
      stubs: [{ path: 'src/c.ts', content: 'export function c(): number { throw new Error("stub"); }\n' }],
    });
    const f = spec.features[0]!;
    const u1 = buildUser(spec, f, 't1', [], undefined, 'CTX_A_ONLY');
    const u2 = buildUser(spec, f, 't2', [], undefined, 'CTX_B_ONLY');
    expect(u1).toContain('CTX_A_ONLY');
    expect(u1).not.toContain('CTX_B_ONLY');
    expect(u2).toContain('CTX_B_ONLY');
    expect(u2).not.toContain('CTX_A_ONLY');
  });
});

describe('SET 36e stall-adaptive entropy escalation', () => {
  it('escalates batch params after 2 no-improvement passes (no identical third)', () => {
    const a = defaultBatchParams();
    expect(a.temps).toEqual([0.3, 0.7, 1.0]);
    expect(a.k).toBe(3);
    const b = escalateBatchParams(a);
    expect(b.escalated).toBe(true);
    expect(b.temps).toContain(ESCALATED_TEMP);
    expect(b.k).toBeGreaterThan(a.k);
    expect(b.temps).not.toEqual(a.temps);
    expect(escalateEntropyEvent(3, 'spread widened')).toBe(
      'pass 3: escalating entropy (spread widened)',
    );
  });

  it('stalled fixture changes batch K by pass 3', async () => {
    const bad = 'export function add(a: number, b: number): number { return a - b; }\n';
    const spec: FeatureSpec = {
      title: 'Add', prompt: 'add numbers correctly',
      features: [{
        id: 'F1', name: 'calc', description: 'add', parentId: null,
        deps: [], files: ['src/calc.ts'], testFile: 'tests/f1.test.ts',
        edgeCases: ['add(0, 0) is 0'], envVars: [],
        exports: [{ file: 'src/calc.ts', symbol: 'add' }],
        stubs: [{ path: 'src/calc.ts', content: 'export function add(a: number, b: number): number { throw new Error("stub"); }\n' }],
      }],
    } as unknown as FeatureSpec;

    const batchSizes: number[] = [];
    const llm: LlmClient = {
      async enumerateFeatures() { return []; },
      async probeFeature() { return { clear: true, assertion: 'x', question: '' }; },
      async encodeFeatures() { return spec; },
      async repairFeatures() { return []; },
      async fixStub() { return ''; },
      async generateTests() { return { content: '' }; },
      async fixTestCases(_s, _f, broken) {
        return {
          cases: broken.map(b => ({
            name: b.name,
            kind: b.kind as 'behavior' | 'edge',
            body: /0/.test(b.name) || /edge/i.test(b.kind)
              ? 'expect(add(0, 0)).toBe(0);'
              : 'expect(add(2, 3)).toBe(5);',
          })),
        };
      },
      async generateImplementation() {
        return [{ path: 'src/calc.ts', content: bad }];
      },
      async repair(_input, opts) {
        if (opts?.candidate) batchSizes.push(opts.candidate.k);
        return [{ path: 'src/calc.ts', content: bad }];
      },
    };

    await runTier1Job({
      prompt: 'add numbers correctly', llm, runSuite: makeEvalSuiteRunner(),
      // SET 64 / SET 101c — stall gates widened so L2 (now often short-circuited
      // after pass 1) still reaches the L3 K-batch contract under test.
      gates: { maxPasses: 6, stallLimit: 7, progressStall: 7 },
    });
    // SET 101c — L2 pass 1 no-improvement + unchanged finding hash skips
    // passes 2/3; the K-batch escalation contract applies to L3 that follows.
    expect(batchSizes.length).toBeGreaterThanOrEqual(4);
    const l2 = batchSizes.filter(k => k === 1);
    const l3 = batchSizes.filter(k => k > 1);
    expect(l2.length).toBe(1);
    expect(l3.length).toBeGreaterThanOrEqual(3);
    expect(l3.some(k => k > 3)).toBe(true); // escalated beyond the default K=3
  });
});

describe('SET 36f assertion repairs carry the diff', () => {
  it('repair prompt for toBe failure contains both values verbatim', () => {
    const cls = classifyFailure(
      't > add',
      'expect(...).toBe failed — actual: 4, expected: 5',
    );
    expect(cls.kind).toBe('assertion');
    expect(cls.detail.received).toBe('4');
    expect(cls.detail.expected).toBe('5');
    const diff = formatAssertionDiff(cls)!;
    expect(diff).toContain('4');
    expect(diff).toContain('5');
    expect(diff).toMatch(/expected 5/);
    expect(diff).toMatch(/received 4/);
  });

  it('throw-mismatch carries expected throw/no-throw vs actual', () => {
    const cls = classifyFailure(
      't > edge',
      'expect(...).toThrow failed — actual: undefined, expected: /boom/',
    );
    expect(cls.kind).toBe('throw-mismatch');
    const diff = formatAssertionDiff(cls)!;
    expect(diff).toMatch(/throw-mismatch/);
    expect(diff).toMatch(/expected throw|expected no-throw/);
  });
});

describe('SET 36g seam-dump hygiene', () => {
  it('suppresses seams for runtime/load crashes; dedupes by program hash', () => {
    expect(shouldEmitSeamDumps('crash while loading src/c.ts:12\nboom')).toBe(false);
    expect(shouldEmitSeamDumps('crash while executing src/c # "(load)"\nboom')).toBe(false);
    expect(shouldEmitSeamDumps('Invalid or unexpected token')).toBe(true);

    const files = [{ path: 'src/a.ts', content: 'export const a = 1;\n' }];
    const test = { path: 'tests/t.test.ts', content: 'export {};\n' };
    const h = programStateHash(files, test);
    const first = formatAttributionMissEvidence({
      miss: 'attribution-miss: program crashed, 2/2 probes clean — probe set diff: none',
      crashMessage: 'Invalid or unexpected token',
      seams: [{ leftPath: 'src/a.ts', rightPath: 'tests/t.test.ts', snippet: 'join' }],
      programHash: h,
    });
    expect(first.some(l => l.startsWith('seam-dump: src/a.ts'))).toBe(true);
    expect(first.some(l => l.startsWith('seam-dump-program:'))).toBe(true);

    const repeat = formatAttributionMissEvidence({
      miss: 'attribution-miss: program crashed, 2/2 probes clean — probe set diff: none',
      crashMessage: 'Invalid or unexpected token',
      seams: [],
      seamRefHash: h,
    });
    expect(repeat.some(l => l === `seam-dump: (see program ${h})`)).toBe(true);
    expect(repeat.filter(l => l.startsWith('seam-dump: src/')).length).toBe(0);

    const suppressed = formatAttributionMissEvidence({
      miss: 'attribution-miss: x',
      crashMessage: 'crash while loading src/c.ts:12\nbloom',
      seams: [{ leftPath: 'a', rightPath: 'b', snippet: 'x' }],
      suppressSeams: true,
    });
    expect(suppressed.every(l => !l.startsWith('seam-dump:'))).toBe(true);
  });
});
