/**
 * SET 89A — deterministic per-case certification against the stub artifact.
 *
 * `certifyCaseAgainstStubs(...)` is a pure coordinator that:
 *   1. runs case-body conformance;
 *   2. assembles a suite containing exactly one case using the same scaffold
 *      and import wiring the real freeze uses;
 *   3. runs whole-program parse and suite syntax gates;
 *   4. executes the single-case suite against the complete stub artifact;
 *   5. requires that exactly one case registered;
 *   6. rejects any crash (never `red`);
 *   7. attributes the observed result to the exact case (no substring match);
 *   8. classifies the terminal disposition from the closed §2.1 taxonomy.
 *
 * There is NO generic `red` disposition. Every rejected case names a class:
 *   rejected-vacuous | rejected-unlicensed-throw | rejected-stub-derived
 *   rejected-registration | rejected-syntax | rejected-conformance
 *   rejected-crash | rejected-unattributed
 *
 * Two certified dispositions exist:
 *   certified-behavior-red   — the case fails against stubs for an honest
 *                              implementation-absence reason;
 *   certified-licensed-throw — a throw-expecting case whose expected error
 *                              literal / pattern is licensed by the prompt
 *                              via `licenseChainFor`.
 *
 * Shape-only anchors (`expect(() => f()).not.toThrow()`) may certify as
 * behavior-red but carry `provisional: true`. Frozen-suite acceptance
 * downstream must not treat provisional certifications as certified frozen
 * status — they are build scaffolds, not behavioral contracts.
 */
import type { IRImport, IRCaseGen } from '../core/irScaffold';
import { assembleTestFile } from '../core/irScaffold';
import type { FeatureNode, FeatureSpec, GeneratedFile } from '../core/types';
import { wireSuiteScaffoldImports } from './suiteScaffoldWire';
import type { SuiteRunResult, SuiteRunner } from './testExec';
import { suiteSyntaxError, programParseError } from './testExec';
import { conformCaseBody, stripStringsAndComments } from './caseBodyConformance';
import { licenseChainFor } from './judgment';

export type CaseCertificationDisposition =
  | 'certified-behavior-red'
  | 'certified-licensed-throw'
  /**
   * SET 91a — structural certifications. `certified-anchor` proves the class
   * export exists and is referenced; `certified-dep-seam` proves the import
   * wiring for a declared dependency resolves. Both are `provisional: true`
   * (they anchor the suite for build continuity without asserting behavior).
   * A suite whose ONLY certified case is provisional is `partial-certified`,
   * not `certified` — see FrozenSuiteStatus.
   */
  | 'certified-anchor'
  | 'certified-dep-seam'
  | 'rejected-vacuous'
  | 'rejected-unlicensed-throw'
  | 'rejected-stub-derived'
  | 'rejected-registration'
  | 'rejected-syntax'
  | 'rejected-conformance'
  | 'rejected-crash'
  | 'rejected-unattributed';

export interface CaseCertification {
  disposition: CaseCertificationDisposition;
  caseName: string;
  featureId: string;
  /** True when the case body was attributed to a single-case run result. */
  attributedToCase: boolean;
  /** Present iff disposition === 'rejected-crash'. */
  crashSignature?: string;
  /**
   * Provisional certifications (shape-only anchors) are NOT behavioral
   * contracts. Frozen-suite acceptance may include them but must NOT
   * increment `featuresFrozenWithoutRedraw` on their behalf, and the
   * artifact-level certification must not certify a feature that has ONLY
   * provisional cases.
   */
  provisional: boolean;
  /** Human-readable single-line summary (never used as the acceptance oracle). */
  reason: string;
  /** Attached diagnostic: what the throw analyzer saw, when applicable. */
  throwAnalysis?: ThrowAnalysis;
}

export interface ThrowAnalysis {
  hasThrowAssertion: boolean;
  /** True when the assertion is `.not.toThrow(...)`. */
  negated: boolean;
  /** True when `rejects.toThrow(...)` (async form). */
  async: boolean;
  /**
   * Extracted expected literal or pattern string, when the source form
   * yields one (string arg, regex source). Null when bare `.toThrow()`.
   */
  expectedLiteral: string | null;
  /**
   * True when the throw assertion is the SOLE substantive assertion in the
   * body (a single `expect(...).toThrow` and nothing else that could carry
   * a behavioral contract).
   */
  isSoleAssertion: boolean;
  /** Raw form the analyzer matched, for debugging. */
  form?: 'toThrow' | 'toThrowError' | 'rejects.toThrow' | 'not.toThrow' | null;
}

// ---------------------------------------------------------------------------
// Throw expectation analysis — token-aware, never `body.includes`.
// ---------------------------------------------------------------------------

/**
 * SET 89 §3.4 — token-aware analysis of a throw-expecting case body.
 *
 * Distinguishes at minimum:
 *   .toThrow()
 *   .toThrow(<literal|/regex/>)
 *   .toThrowError(...)
 *   rejects.toThrow(...)
 *   not.toThrow(...)
 *   multi-assertion bodies where a throw assertion is not the whole contract
 *   matcher-like text in comments / strings (must be ignored)
 */
export function analyzeThrowExpectation(body: string): ThrowAnalysis {
  const stripped = stripStringsAndComments(body);
  // Count all expect(...) top-level assertions (a rough measure of whether
  // the throw is the sole contract). `expect(` appearing anywhere at any
  // depth is counted; multi-line expressions count once.
  const expectMatches = stripped.match(/\bexpect\s*\(/g) ?? [];
  const expectCount = expectMatches.length;

  // rejects.toThrow — must come before generic toThrow to disambiguate.
  const rej = matchThrowCall(stripped, /\brejects\s*\.\s*(toThrow|toThrowError)\s*\(/);
  if (rej) {
    return {
      hasThrowAssertion: true,
      negated: false,
      async: true,
      expectedLiteral: extractLiteralFromSpan(body, rej.argStart, rej.argEnd),
      isSoleAssertion: expectCount <= 1,
      form: 'rejects.toThrow',
    };
  }

  // not.toThrow
  const nt = matchThrowCall(stripped, /\bnot\s*\.\s*(toThrow|toThrowError)\s*\(/);
  if (nt) {
    return {
      hasThrowAssertion: true,
      negated: true,
      async: false,
      expectedLiteral: extractLiteralFromSpan(body, nt.argStart, nt.argEnd),
      isSoleAssertion: expectCount <= 1,
      form: 'not.toThrow',
    };
  }

  // Generic .toThrow / .toThrowError (positive).
  const gen = matchThrowCall(stripped, /\.(toThrow|toThrowError)\s*\(/);
  if (gen) {
    return {
      hasThrowAssertion: true,
      negated: false,
      async: false,
      expectedLiteral: extractLiteralFromSpan(body, gen.argStart, gen.argEnd),
      isSoleAssertion: expectCount <= 1,
      form: gen.name as 'toThrow' | 'toThrowError',
    };
  }

  return {
    hasThrowAssertion: false,
    negated: false,
    async: false,
    expectedLiteral: null,
    isSoleAssertion: expectCount <= 1,
    form: null,
  };
}

interface ThrowCallMatch {
  argStart: number;
  argEnd: number;
  name: string;
}

/**
 * Match a `.toThrow(` / `rejects.toThrow(` / `not.toThrow(` head on the
 * stripped source and return the argument span (start/end indices measured
 * in the ORIGINAL body — positions are preserved because stripping keeps
 * length). Balanced-paren scanning finds the matching close paren.
 */
function matchThrowCall(stripped: string, headRe: RegExp): ThrowCallMatch | null {
  const m = headRe.exec(stripped);
  if (!m) return null;
  const name = m[1] ?? m[0];
  const argStart = m.index + m[0].length;
  let depth = 1;
  let i = argStart;
  while (i < stripped.length && depth > 0) {
    const c = stripped[i]!;
    if (c === '(') depth++;
    else if (c === ')') depth--;
    if (depth === 0) break;
    i++;
  }
  const argEnd = i;
  return { argStart, argEnd, name };
}

/**
 * Extract a leading string/regex literal from `body[start:end]`. The stripped
 * body preserves indices, so the span is meaningful in the original.
 */
function extractLiteralFromSpan(body: string, start: number, end: number): string | null {
  const raw = body.slice(start, end).trim();
  if (!raw) return null;
  // String literal — single, double, or template.
  const strM = /^(['"`])([\s\S]*?)\1/.exec(raw);
  if (strM) return strM[2]!;
  // Regex literal — /pattern/flags.
  const reM = /^\/([\s\S]*?)\/[gimsuy]*/.exec(raw);
  if (reM) return reM[1]!;
  // Bare identifier or other — no extractable literal.
  return null;
}

/** True when the string equals or contains a known stub-body sentinel. */
export function isStubDerivedLiteral(
  literal: string | null,
  spec: FeatureSpec,
  feature: FeatureNode,
): boolean {
  if (!literal) return false;
  const l = literal.toLowerCase();
  // Universal sentinels first.
  if (/\bstub\b/.test(l)) return true;
  if (/\bnot\s+implemented\b/.test(l)) return true;
  // Substring match against any stub body of any owned or shared file.
  const bodies = spec.features.flatMap(f => f.stubs.map(s => s.content));
  for (const b of bodies) {
    // Look for the literal appearing inside a stub as a throw message.
    if (b.toLowerCase().includes(l)) {
      // Only treat as stub-derived if the containing stub actually THROWS
      // (this avoids false positives when a helpful comment mentions the
      // literal). Cheap heuristic: the stub file contains `throw` and the
      // literal appears within a reasonable neighborhood.
      if (/\bthrow\b/.test(b)) return true;
    }
  }
  void feature;
  return false;
}

// ---------------------------------------------------------------------------
// SET 91a — case-kind detection for structural certifications.
// ---------------------------------------------------------------------------

/**
 * A case is a CLASS-ANCHOR when its name equals one of the feature's owned
 * export symbols. The planner injects these deterministically (see
 * `injectClassAnchor` in planSuite.ts). Structurally, they prove the export
 * exists — they need not assert behavior.
 */
export function isClassAnchorCase(
  caseGen: Pick<IRCaseGen, 'name'>,
  feature: FeatureNode,
): boolean {
  const owned = new Set(feature.exports.map(e => e.symbol));
  return owned.has(caseGen.name);
}

/**
 * A case is a DEP-SEAM when it tests import wiring for a declared dependency.
 * The planner emits these with `kind: 'integration'` and names them
 * `uses <dep-name>`. Structurally, they prove the import chain resolves —
 * they need not assert behavior.
 */
export function isDepSeamCase(
  caseGen: Pick<IRCaseGen, 'name' | 'kind'>,
  feature: FeatureNode,
  spec: FeatureSpec,
): boolean {
  if (caseGen.kind === 'integration') return true;
  if (!/^uses\s+/.test(caseGen.name)) return false;
  // Sanity: the name should match at least one declared dependency's name.
  const depNames = new Set(
    feature.deps
      .map(id => spec.features.find(f => f.id === id)?.name)
      .filter((n): n is string => !!n),
  );
  const target = caseGen.name.replace(/^uses\s+/, '').trim();
  return depNames.has(target);
}

/**
 * Structural check for anchor cases — the body must (a) parse (already
 * enforced by conformance upstream), (b) reference the anchored symbol at
 * least once. That's it: absence-of-implementation is not a defect for a
 * type-existence anchor.
 */
function anchorBodyReferencesSymbol(body: string, symbol: string): boolean {
  const stripped = stripStringsAndComments(body);
  const re = new RegExp(`(?<![\\w$])${escapeRegex(symbol)}(?![\\w$])`);
  return re.test(stripped);
}

/**
 * Structural check for dep-seam cases — the body must (a) parse, (b)
 * reference the dependency in some form. Because dep-seam bodies vary
 * widely (import assertion, symbol reference, mock verification), we accept
 * any reference to a symbol declared by the dependency's exports OR the
 * literal dependency name.
 */
function depSeamBodyReferencesDep(
  body: string,
  dep: FeatureNode,
): boolean {
  const stripped = stripStringsAndComments(body);
  if (stripped.includes(dep.name)) return true;
  for (const ex of dep.exports) {
    const re = new RegExp(`(?<![\\w$])${escapeRegex(ex.symbol)}(?![\\w$])`);
    if (re.test(stripped)) return true;
  }
  return false;
}

function escapeRegex(s: string): string {
  return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

// ---------------------------------------------------------------------------
// Certification coordinator.
// ---------------------------------------------------------------------------

export interface CertifyCaseOpts {
  spec: FeatureSpec;
  feature: FeatureNode;
  prompt: string;
  imports: IRImport[];
  caseGen: IRCaseGen;
  runSuite: SuiteRunner;
  /** Stub artifact for the whole spec (produced by stubArtifact(spec)). */
  stubs: GeneratedFile[];
}

/**
 * Certify one case against the stub artifact. Returns a structured record;
 * never throws. Every terminal disposition is drawn from the closed §2.1
 * taxonomy — there is NO generic `red`.
 */
export async function certifyCaseAgainstStubs(
  opts: CertifyCaseOpts,
): Promise<CaseCertification> {
  const { spec, feature, prompt, imports, caseGen, runSuite, stubs } = opts;

  const base: Omit<CaseCertification, 'disposition'> = {
    caseName: caseGen.name,
    featureId: feature.id,
    attributedToCase: false,
    provisional: false,
    reason: '',
  };

  // 1) Case-body conformance (unwrap + registration + syntax + assertion).
  const conf = conformCaseBody(caseGen.body);
  if (conf.disposition === 'rejected-registration') {
    return { ...base, disposition: 'rejected-registration', reason: conf.reason };
  }
  if (conf.disposition === 'rejected-syntax') {
    return { ...base, disposition: 'rejected-syntax', reason: conf.reason };
  }
  if (conf.disposition === 'rejected-dynamic-eval') {
    return { ...base, disposition: 'rejected-conformance', reason: conf.reason };
  }
  if (conf.disposition === 'rejected-empty') {
    return { ...base, disposition: 'rejected-vacuous', reason: conf.reason };
  }

  const canonicalBody = conf.body;

  // 2) Assemble a single-case suite using the same scaffold + wiring that the
  //    real freeze uses. The suite goes through wireSuiteScaffoldImports so
  //    scaffold-owned identifiers (describe/it/expect/beforeEach/etc.) do not
  //    appear as `undefined-symbol` after import elision.
  const singleCase: IRCaseGen = { ...caseGen, body: canonicalBody };
  const assembled = assembleTestFile(feature, imports, [singleCase]);
  const wired = wireSuiteScaffoldImports(assembled, feature.testFile, spec).content;

  // 3) Standalone suite syntax gate.
  const suiteSyn = suiteSyntaxError(wired);
  if (suiteSyn) {
    return { ...base, disposition: 'rejected-syntax', reason: suiteSyn.split('\n')[0]!.slice(0, 160) };
  }

  // 4) Whole-program parse gate against the complete stub artifact.
  //    (SET 40a — GATE_UNAVAILABLE returns null; not a rejection.)
  const parseMsg = programParseError(stubs, { path: feature.testFile, content: wired });
  if (parseMsg && parseMsg !== 'GATE_UNAVAILABLE') {
    return { ...base, disposition: 'rejected-syntax', reason: `program parse: ${parseMsg.split('\n')[0]!.slice(0, 140)}` };
  }

  // 5) Execute the single-case suite against the complete stub artifact.
  let run: SuiteRunResult;
  try {
    run = await runSuite(stubs, { path: feature.testFile, content: wired });
  } catch (e) {
    return {
      ...base,
      disposition: 'rejected-crash',
      crashSignature: e instanceof Error ? e.message.slice(0, 200) : String(e).slice(0, 200),
      reason: 'suite runner threw during case certification',
    };
  }

  // 6) Crash → rejected-crash; NEVER red.
  if (run.crashed) {
    return {
      ...base,
      disposition: 'rejected-crash',
      crashSignature: run.crashed.split('\n')[0]!.slice(0, 200),
      reason: 'suite crashed during single-case execution — proves oracle did not complete',
    };
  }

  // 7) Registration count check — exactly one case must have registered.
  if (run.total !== 1) {
    return {
      ...base,
      disposition: 'rejected-registration',
      reason: `single-case suite registered ${run.total} cases (expected 1)`,
    };
  }

  // 8) Exact case attribution — single-case suite means at most one failure
  //    and it must belong to this case name; no substring inference required.
  const attributed = run.failures.length === 0
    ? true
    : run.failures.length === 1
      && (run.failures[0]!.name === singleCase.name
        || run.failures[0]!.name.endsWith(`> ${singleCase.name}`)
        || run.failures[0]!.name.includes(singleCase.name));
  if (run.failures.length > 0 && !attributed) {
    return {
      ...base,
      disposition: 'rejected-unattributed',
      reason: `case name did not match single-case run failure: ${run.failures[0]!.name}`,
    };
  }
  base.attributedToCase = true;

  // 9a) SET 91a — STRUCTURAL certifications. Class-anchors prove the export
  //     exists; dep-seams prove the import wiring resolves. Neither asserts
  //     behavior, and calling them "vacuous" was the operative cause of the
  //     78e36cc1 collapse (5 of 10 features died on a single anchor/seam
  //     case that had no `expect(...)` in the current sense).
  //     These certifications are `provisional: true` — they may live in a
  //     frozen suite for build continuity, but they do NOT confer artifact
  //     certification. See FrozenSuiteStatus + certifyArtifact.
  if (isClassAnchorCase(caseGen, feature)) {
    if (!anchorBodyReferencesSymbol(canonicalBody, caseGen.name)) {
      return {
        ...base,
        disposition: 'rejected-vacuous',
        reason: `class-anchor body does not reference the anchored symbol "${caseGen.name}"`,
      };
    }
    return {
      ...base,
      disposition: 'certified-anchor',
      provisional: true,
      reason: `structural anchor for exported symbol "${caseGen.name}"`,
    };
  }
  if (isDepSeamCase(caseGen, feature, spec)) {
    // Find the referenced dep — if we can't identify one, fall through to
    // the behavioral path so we don't silently promote a mislabeled case.
    const target = caseGen.name.replace(/^uses\s+/, '').trim();
    const dep = spec.features.find(f => f.name === target)
      ?? spec.features.find(f => feature.deps.includes(f.id));
    if (dep && depSeamBodyReferencesDep(canonicalBody, dep)) {
      return {
        ...base,
        disposition: 'certified-dep-seam',
        provisional: true,
        reason: `structural dep-seam for dependency "${dep.name}"`,
      };
    }
    // Falls through to behavioral certification below.
  }

  // 9) Throw-expecting cases get the licensing pass.
  const ta = analyzeThrowExpectation(canonicalBody);
  if (ta.hasThrowAssertion) {
    // Shape-only anchors: `not.toThrow()` with no argument — provisional.
    if (ta.negated && !ta.expectedLiteral) {
      return {
        ...base,
        disposition: 'certified-behavior-red',
        provisional: true,
        reason: 'shape-only anchor (not.toThrow) — provisional build scaffold, not behavioral certification',
        throwAnalysis: ta,
      };
    }
    // A .toThrow() case that passes against stubs BY VIRTUE OF the stub
    // throwing is undecidable from the impl side. Whether it certifies
    // depends entirely on licensing.
    if (isStubDerivedLiteral(ta.expectedLiteral, spec, feature)) {
      return {
        ...base,
        disposition: 'rejected-stub-derived',
        reason: `expected literal "${ta.expectedLiteral}" derives from stub sentinel text`,
        throwAnalysis: ta,
      };
    }
    // Deterministic licensing chain — case → edge/export → description → prompt.
    const lic = licenseChainFor({
      caseName: singleCase.name,
      expected: ta.expectedLiteral,
      feature,
      prompt,
    });
    if (!lic.licensed) {
      return {
        ...base,
        disposition: 'rejected-unlicensed-throw',
        reason: lic.searched,
        throwAnalysis: ta,
      };
    }
    return {
      ...base,
      disposition: 'certified-licensed-throw',
      reason: lic.searched,
      throwAnalysis: ta,
    };
  }

  // 10) Non-throw cases. Green against stubs = vacuous (no observable
  //     behavioral contract). One honest failure = certified-behavior-red.
  if (run.failures.length === 0) {
    return {
      ...base,
      disposition: 'rejected-vacuous',
      reason: 'case passes against pure stubs — asserts nothing an implementation could fail',
    };
  }

  return {
    ...base,
    disposition: 'certified-behavior-red',
    reason: `honest absence-of-implementation failure (${run.failures[0]!.message.split('\n')[0]!.slice(0, 120)})`,
  };
}

/** True when a certification is behaviorally freeze-eligible (not provisional). */
export function isFrozenEligible(cert: CaseCertification): boolean {
  return (
    (cert.disposition === 'certified-behavior-red'
      || cert.disposition === 'certified-licensed-throw')
    && !cert.provisional
  );
}

/**
 * True when the certification confers ANY frozen status (provisional or
 * behavioral). Provisional anchors and dep-seams may live in a frozen suite
 * for dependency continuity, but must never confer artifact-level
 * certification. See SET 91b.
 */
export function isProvisionallyFrozenEligible(cert: CaseCertification): boolean {
  return (
    cert.disposition === 'certified-behavior-red'
    || cert.disposition === 'certified-licensed-throw'
    || cert.disposition === 'certified-anchor'
    || cert.disposition === 'certified-dep-seam'
  );
}
