/**
 * Deterministic IR scaffolding: the harness owns test-file STRUCTURE, the
 * model only fills bounded holes (implementation imports + case bodies).
 *
 * A suite assembled here passes the structural half of the deterministic
 * gate BY CONSTRUCTION — it imports vitest, has a describe block, and
 * prefixes case names by kind — so a weak model can no longer fail on
 * boilerplate it was never good at. Scaffolding is how bounded repair gets
 * smaller units: the unit is a case body, not a whole file.
 */
import type { FeatureNode } from './types';

export interface IRImport {
  /** Implementation module, e.g. "src/service" or "../src/service". */
  from: string;
  symbols: string[];
}

/** SET 3b — clade membership for schema'd case repair / rebind. */
export interface IRClade {
  branch: 'algebraic' | 'contract' | 'state' | 'representation' | 'freeform';
  family: string;
  bindings: Record<string, string>;
}

export interface IRCaseGen {
  name: string;
  kind: 'behavior' | 'edge' | 'integration';
  /** The code INSIDE the it() callback only. */
  body: string;
  /** SET 3b — typed when template-born or post-hoc classified. */
  clade?: IRClade;
}

/** The structured parts of a suite — kept so repair can be per-case, not per-file. */
export interface IRParts {
  imports: IRImport[];
  cases: IRCaseGen[];
}

export interface CaseRange {
  index: number;
  name: string;
  kind: IRCaseGen['kind'];
  /** 1-indexed inclusive line span of this case's `it(...)` block in the file. */
  start: number;
  end: number;
}

export interface AssemblyMap {
  content: string;
  importRange: { start: number; end: number };
  caseRanges: CaseRange[];
}

const KIND_PREFIX: Record<IRCaseGen['kind'], string> = {
  behavior: '',
  edge: 'edge: ',
  integration: 'integration: ',
};

/** Normalize a model-supplied module path to tests/-relative extensionless. */
function normalizeModule(from: string): string {
  let m = from.trim().replace(/\.ts$/, '').replace(/^\.\//, '');
  if (m.startsWith('src/')) m = '../' + m;
  return m;
}

export function assembleTestFile(
  feature: FeatureNode,
  imports: IRImport[],
  cases: IRCaseGen[],
): string {
  return assembleWithMap(feature, imports, cases).content;
}

/**
 * Assemble the suite AND record the exact 1-indexed line span of every case
 * and the import block. This is what lets the cross-suite gate map a tsc
 * error line back to the single case that owns it, so repair can be bounded
 * to that case while every other case is preserved byte-for-byte.
 */
export function assembleWithMap(
  feature: FeatureNode,
  imports: IRImport[],
  cases: IRCaseGen[],
): AssemblyMap {
  // Dedupe imports per module; only implementation imports are accepted —
  // vitest is scaffolded, and anything else is not part of the contract.
  const byModule = new Map<string, Set<string>>();
  for (const imp of imports) {
    const m = normalizeModule(imp.from);
    if (!m.startsWith('../src/')) continue;
    if (!byModule.has(m)) byModule.set(m, new Set());
    for (const s of imp.symbols) {
      const sym = s.trim();
      if (/^[A-Za-z_$][\w$]*$/.test(sym)) byModule.get(m)!.add(sym);
    }
  }

  const lines: string[] = [];
  lines.push(`import { describe, it, expect } from 'vitest';`);
  for (const [m, syms] of byModule) {
    if (syms.size) lines.push(`import { ${[...syms].join(', ')} } from '${m}';`);
  }
  const importRange = { start: 1, end: lines.length };
  lines.push('');
  lines.push(`describe('${feature.id}: ${feature.name.replace(/'/g, "\\'")}', () => {`);

  const caseRanges: CaseRange[] = [];
  cases.forEach((c, index) => {
    const prefix = KIND_PREFIX[c.kind] ?? '';
    let name = c.name.trim().replace(/\\/g, '\\\\').replace(/'/g, "\\'");
    if (prefix && !name.startsWith(prefix)) name = prefix + name;
    const asyncKw = /\bawait\b/.test(c.body) ? 'async ' : '';
    const start = lines.length + 1; // the `it(...)` line we are about to push
    lines.push(`  it('${name}', ${asyncKw}() => {`);
    // Push each body line individually so line accounting stays exact.
    for (const bl of c.body.replace(/\r\n/g, '\n').split('\n')) {
      lines.push(bl.trim() ? '    ' + bl : '');
    }
    lines.push('  });');
    caseRanges.push({ index, name: c.name, kind: c.kind, start, end: lines.length });
    lines.push('');
  });
  lines.push('});');
  lines.push('');
  return { content: lines.join('\n'), importRange, caseRanges };
}
