/**
 * SET 34 — Report diagnostics: every verdict carries its evidence.
 *
 * 34a seam dump ±120 around each boundary on attribution-miss
 * 34b unparseable drops carry parse snippet ≤80
 * 34c spec sha256-8 line (NEW carve | matches run)
 * 34d superbuffer efficacy table vocabulary
 * 34e planner bypass when 0 substrate-templated + matching templates
 * 34f template/lesson provenance on superbuffer logLine
 */
import { describe, expect, it } from 'vitest';
import type { FeatureNode, FeatureSpec, GeneratedFile, LlmClient } from '../src/core/types';
import {
  collectReportEvidence,
  countMatchingTemplatesForPlan,
  encodeOutputCanonical,
  formatAttributionMissEvidence,
  formatParseEvidence,
  formatPlannerBypass,
  formatSpecHashLine,
  formatSuperbufferEfficacy,
  formatUnparseableDropReason,
  sha256Hex8,
} from '../src/tier1/reportEvidence';
import { dumpConcatenationSeams } from '../src/tier1/testExec';
import { assembleSuperbuffer } from '../src/tier1/superbuffer';
import { runTier1Job } from '../src/tier1/runner';
import { buildRunLogTxt, buildRunLogJson } from '../web/src/exportRunLog';
import type { PlannedCase } from '../src/tier1/planSuite';

function feature(partial: Partial<FeatureNode> & { id: string }): FeatureNode {
  return {
    name: partial.name ?? partial.id,
    description: partial.description ?? 'd',
    parentId: null,
    deps: partial.deps ?? [],
    files: partial.files ?? [`src/${partial.id.toLowerCase()}.ts`],
    testFile: partial.testFile ?? `tests/${partial.id.toLowerCase()}.test.ts`,
    edgeCases: partial.edgeCases ?? ['roundtrip', 'empty'],
    envVars: [],
    exports: partial.exports ?? [{ file: `src/${partial.id.toLowerCase()}.ts`, symbol: 'fn' }],
    stubs: partial.stubs ?? [{
      path: `src/${partial.id.toLowerCase()}.ts`,
      content: 'export function fn(x: string): string { throw new Error("stub: not implemented"); }\n',
    }],
    ...partial,
  };
}

describe('SET 34a seam dump', () => {
  it('dumps ±120 chars around each file boundary labeled A ⨝ B', () => {
    const files: GeneratedFile[] = [
      { path: 'src/a.ts', content: 'export const a = 1;\n' },
      { path: 'src/b.ts', content: 'export const b = 2;\n' },
    ];
    const test: GeneratedFile = {
      path: 'tests/t.test.ts',
      content: "import { describe, it, expect } from 'vitest';\ndescribe('t', () => { it('ok', () => { expect(1).toBe(1); }); });\n",
    };
    const dumps = dumpConcatenationSeams(files, test, { radius: 120 });
    expect(dumps.length).toBe(2); // a|b and b|test
    expect(dumps[0]!.leftPath).toBe('src/a.ts');
    expect(dumps[0]!.rightPath).toBe('src/b.ts');
    expect(dumps[0]!.snippet.length).toBeLessThanOrEqual(120 + 120 + 1 + 2); // ±120 + join + ellipsis room
    const lines = formatAttributionMissEvidence({
      miss: 'attribution-miss: program crashed, 3/3 probes clean — probe set diff: none',
      crashMessage: 'Invalid or unexpected token',
      seams: dumps,
    });
    expect(lines[0]).toMatch(/^attribution-miss:/);
    expect(lines[1]).toBe('crash: Invalid or unexpected token');
    expect(lines.some(l => l.startsWith('seam-dump: src/a.ts ⨝ src/b.ts'))).toBe(true);
  });
});

describe('SET 34b parse evidence', () => {
  it('bounds offending snippet to ≤80 chars and keeps drop vocabulary', () => {
    const body = 'expect(foo(' + 'x'.repeat(200) + ')).toBe(1);';
    const { firstLine, snippet } = formatParseEvidence(
      'Unexpected token (1:20)',
      body,
    );
    expect(firstLine).toContain('Unexpected token');
    expect(snippet.length).toBeLessThanOrEqual(80);
    const reason = formatUnparseableDropReason('Unexpected token (1:20)', body);
    expect(reason).toMatch(/^dropped — unparseable: /);
    expect(reason).toContain('at "');
    // DROPPED_CASE_RE in clades.ts still matches enriched form.
    const problem = `F1: case "ArticleModel" ${reason} (coverage reduced)`;
    expect(problem).toMatch(/case "ArticleModel"\s+dropped/);
  });
});

describe('SET 34c spec hash', () => {
  it('emits sha256-8 line — NEW carve or matches run', async () => {
    const spec: FeatureSpec = {
      title: 'T',
      prompt: 'p',
      features: [feature({ id: 'F1', name: 'One' })],
    };
    const hash = await sha256Hex8(encodeOutputCanonical(spec));
    expect(hash).toMatch(/^[0-9a-f]{8}$/);
    expect(formatSpecHashLine(hash, null)).toBe(
      `spec: ${hash} of encode output — NEW carve`,
    );
    expect(formatSpecHashLine(hash, '5abf390b')).toBe(
      `spec: ${hash} of encode output — matches run 5abf390b`,
    );
  });

  it('runner emits spec hash evidence on encode', async () => {
    const f = feature({ id: 'F1', name: 'fn', edgeCases: [] });
    const spec: FeatureSpec = { title: 'Add', prompt: 'add', features: [f] };
    const llm = {
      async encodeFeatures() { return structuredClone(spec); },
      async repairFeatures() { return []; },
      async enumerateFeatures() { return []; },
      async probeFeature() { return { clear: true, assertion: '', question: '' }; },
      async fixStub() { return ''; },
      async generateTests() { return { content: '' }; },
      async fixTestCases(
        _s: unknown, _f: unknown, broken: { name: string; kind: string }[],
      ) {
        return {
          cases: broken.map(b => ({
            name: b.name,
            kind: b.kind as 'behavior' | 'edge' | 'integration',
            body: "expect(fn('a')).toBe('a');",
          })),
        };
      },
      async generateImplementation() {
        return [{ path: f.files[0]!, content: 'export function fn(x: string): string { return x; }\n' }];
      },
      async repair() { return []; },
    } as unknown as LlmClient;

    const result = await runTier1Job({
      prompt: 'add',
      llm,
      gates: { maxPasses: 1, stallLimit: 1, progressStall: 1 },
      priorSpecHashes: [],
    });
    const specLine = result.evidence?.find(l => l.startsWith('spec:') && l.includes('of encode output'));
    expect(specLine).toBeTruthy();
    expect(specLine).toMatch(/NEW carve$/);
    expect(result.log.some(e => e.message === specLine)).toBe(true);
  });
});

describe('SET 34d efficacy vocabulary', () => {
  it('formats buffer had templates / no templates table', () => {
    const line = formatSuperbufferEfficacy(
      { cases: 22, filled: 21 },
      { cases: 4, filled: 1 },
    );
    expect(line).toBe(
      'buffer had templates: 22 cases, 21 filled (95%) · no templates: 4 cases, 1 filled (25%)',
    );
  });
});

describe('SET 34e planner bypass', () => {
  it('counts matching templates and formats tripwire line', () => {
    const cases: PlannedCase[] = [{
      name: 'roundtrip',
      kind: 'edge',
      license: 'roundtrip',
      source: 'edge',
      symbol: 'fn',
      signature: { name: 'fn', params: [{ name: 'x', type: 'string' }], returnType: 'string' },
    }];
    const n = countMatchingTemplatesForPlan(cases, ['roundtrip']);
    expect(n).toBeGreaterThanOrEqual(1);
    expect(formatPlannerBypass(n)).toBe(`planner bypass: ${n} matching templates unused`);
  });
});

describe('SET 34f template provenance', () => {
  it('superbuffer logLine names template families / lesson ids', () => {
    const f = feature({
      id: 'F1',
      name: 'Round',
      edgeCases: ['roundtrip'],
      stubs: [{
        path: 'src/f1.ts',
        content: 'export function fn(x: string): string { throw new Error("stub: not implemented"); }\n',
      }],
    });
    const spec: FeatureSpec = { title: 'T', prompt: 'p', features: [f] };
    const assembly = assembleSuperbuffer({
      spec,
      feature: f,
      parts: {
        imports: [],
        cases: [{ name: 'roundtrip', kind: 'edge', body: "expect(fn('a')).toBe('a');" }],
      },
      stubs: f.stubs,
      lessons: [{
        classKey: 'regex-literal|unterminated',
        idiom: 'close the class',
        resolution: 'add ]',
        ts: new Date().toISOString(),
      }],
      siblingParts: new Map(),
      target: 'roundtrip',
      signature: { name: 'fn', params: ['string'], returnType: 'string' },
    });
    expect(assembly.logLine).toMatch(/^superbuffer: assembled \(/);
    // Provenance brackets appear when templates/lessons were packed.
    if (assembly.templates.length > 0) {
      expect(assembly.logLine).toMatch(/\d+ templates \[[^\]]+\]/);
    }
    if (assembly.lessons.length > 0) {
      expect(assembly.logLine).toMatch(/\d+ lessons \[[^\]]+\]/);
    }
  });
});

describe('SET 34 export + collect', () => {
  it('attribution-miss export includes seam dump section', () => {
    const events = [
      { ts: '2026-07-26T12:00:00.000Z', phase: 'encode', featureId: null, message: 'start' },
      {
        ts: '2026-07-26T12:01:00.000Z',
        phase: 'rha',
        featureId: null,
        message: 'attribution-miss: program crashed, 2/2 probes clean — probe set diff: none',
      },
      {
        ts: '2026-07-26T12:01:00.000Z',
        phase: 'rha',
        featureId: null,
        message: 'crash: Invalid or unexpected token',
      },
      {
        ts: '2026-07-26T12:01:00.000Z',
        phase: 'rha',
        featureId: null,
        message: 'seam-dump: src/a.ts ⨝ src/b.ts · «;})();\n;(function(){»',
      },
      {
        ts: '2026-07-26T12:01:01.000Z',
        phase: 'encode',
        featureId: null,
        message: 'spec: deadbeef of encode output — NEW carve',
      },
    ];
    const collected = collectReportEvidence(events);
    expect(collected.seamDumps.length).toBe(1);
    expect(collected.specHash).toContain('deadbeef');

    const txt = buildRunLogTxt({
      id: '5abf390b',
      ts: '2026-07-26T12:00:00.000Z',
      events,
      problems: ['F1: crash|unattributed'],
      metrics: {},
    });
    expect(txt).toContain('--- evidence ---');
    expect(txt).toContain('seam-dump: src/a.ts ⨝ src/b.ts');
    expect(txt).toContain('attribution-miss:');

    const j = JSON.parse(buildRunLogJson({
      id: '5abf390b',
      ts: '2026-07-26T12:00:00.000Z',
      events,
      problems: ['F1: crash|unattributed'],
    })) as { evidence: string[] };
    expect(j.evidence.some(l => l.startsWith('seam-dump:'))).toBe(true);
  });
});
