/**
 * SET 58 probe — a finished run's record must survive the persist path:
 * structuredClone (chrome messaging) AND JSON round-trip (storage highway).
 * If anything in the engine result is unserializable or explosive in size,
 * THIS is the persist failure that has eaten every finished run since 1.7.20.
 */
import { describe, expect, it } from 'vitest';
import type { FeatureSpec, GeneratedFile, LlmClient, RepairInput } from '../src/core/types';
import { runTier1Job } from '../src/tier1/runner';
import { makeEvalSuiteRunner } from '../src/tier1/testExec';

const WRONG = 'export function add(a: number, b: number): number { return a - b; }';
const RIGHT = 'export function add(a: number, b: number): number { return a + b; }';

function makeSpec(): FeatureSpec {
  return {
    title: 'Adder', prompt: 'adder',
    features: [{
      id: 'F1', name: 'add', description: 'adds two numbers', parentId: null,
      deps: [], files: ['src/adder.ts'], testFile: 'tests/f1-add.test.ts',
      edgeCases: ['zero'], envVars: [],
      exports: [{ file: 'src/adder.ts', symbol: 'add' }],
      stubs: [{ path: 'src/adder.ts', content: `export function add(a: number, b: number): number { throw new Error('stub'); }` }],
    }],
  } as unknown as FeatureSpec;
}
const CASES = {
  imports: [{ from: 'src/adder', symbols: ['add'] }],
  cases: [
    { name: 'adds', kind: 'behavior' as const, body: 'expect(add(2, 3)).toBe(5);' },
    { name: 'zero', kind: 'edge' as const, body: 'expect(add(7, 0)).toBe(7);' },
  ],
};
function client(stubborn: boolean): LlmClient {
  return {
    async encodeFeatures() { return makeSpec(); },
    async generateTests() { return { content: '', parts: CASES }; },
    async generateImplementation(_s: FeatureSpec, _f: unknown, _t: string, _d: GeneratedFile[], o?: { temperature?: number }) {
      return [{ path: 'src/adder.ts', content: stubborn && !((o?.temperature ?? 0) >= 1) ? WRONG : RIGHT }];
    },
    async repair(_i: RepairInput) { return [{ path: 'src/adder.ts', content: WRONG }]; },
    async fixTestCases() { return { cases: CASES.cases }; },
  } as unknown as LlmClient;
}

function findUnserializable(v: unknown, path = '$', out: string[] = [], seen = new Set<unknown>()): string[] {
  if (v == null) return out;
  const t = typeof v;
  if (t === 'function') { out.push(`${path}: function`); return out; }
  if (t === 'bigint') { out.push(`${path}: bigint`); return out; }
  if (t === 'number' && !Number.isFinite(v as number)) out.push(`${path}: ${String(v)}`);
  if (t !== 'object') return out;
  if (seen.has(v)) { out.push(`${path}: CIRCULAR`); return out; }
  seen.add(v);
  if (v instanceof Map || v instanceof Set) out.push(`${path}: ${v.constructor.name} (JSON-silently-drops)`);
  for (const [k, val] of Object.entries(v as Record<string, unknown>)) {
    findUnserializable(val, `${path}.${k}`, out, seen);
  }
  seen.delete(v);
  return out;
}

describe('persist-path serialization probe', () => {
  for (const [label, stubborn] of [['clean', false], ['stubborn-repair', true]] as const) {
    it(`${label} run record survives structuredClone + JSON round-trip`, async () => {
      const res = await runTier1Job({ prompt: 'adder', llm: client(stubborn), runSuite: makeEvalSuiteRunner() });
      const record = {
        id: 'probe', ts: new Date().toISOString(), model: 'mock', prompt: 'adder',
        events: res.log, problems: res.problems, metrics: res.metrics,
        result: { spec: res.spec, files: res.files, frozenTests: res.frozenTests },
        provenance: res.provenance, escalations: (res as unknown as Record<string, unknown>).escalations ?? null,
      };
      const offenders = findUnserializable(record);
      expect(offenders, offenders.join('; ')).toEqual([]);
      expect(() => structuredClone(record)).not.toThrow();
      const json = JSON.stringify(record);
      expect(() => JSON.parse(json)).not.toThrow();
      const bytes = new TextEncoder().encode(json).length;
      // chrome.runtime message hard limit is 64MB; flag anything near 8MB.
      expect(bytes, `record is ${bytes} bytes`).toBeLessThan(8_000_000);
      console.log(`[probe:${label}] record=${(bytes / 1024).toFixed(1)}KB events=${res.log.length}`);
    });
  }
});
