/**
 * SET 89B — the one transactional gateway that owns all frozen-suite state.
 *
 * `certifyFrozenSuite(...)` is the deterministic certification function that
 * accepts canonical `IRParts` and returns a structured report:
 *   - case certification for every case;
 *   - registration-conformance findings (via case-body conformance);
 *   - shape findings (via validateSuiteShape);
 *   - standalone suite syntax result;
 *   - whole-program parse result;
 *   - gate infrastructure availability;
 *   - certification status;
 *   - explicit rejection reasons per case.
 *
 * `commitFrozenSuite(...)` is the SOLE writer of frozen-suite state. It
 *   1. receives feature, canonical parts, commit reason;
 *   2. assembles and wires the suite (canonical → text);
 *   3. calls certifyFrozenSuite;
 *   4. on rejection: DOES NOT touch state — every incumbent stays
 *      byte-identical;
 *   5. on acceptance: atomically updates all frozen-state representations
 *      (frozenTests, frozenByFeature, irPartsByFeature) — no interval where
 *      one map contains the new suite while another contains the old.
 *
 * No other module in tier1 may push/set frozen-state maps directly. A
 * source-scan ownership check enforces this after migration.
 */
import { assembleTestFile } from '../core/irScaffold';
import type { IRParts } from '../core/irScaffold';
import type { FeatureNode, FeatureSpec, GeneratedFile } from '../core/types';
import { validateSuiteShape } from '../core/specGates';
import { wireSuiteScaffoldImports } from './suiteScaffoldWire';
import type { SuiteRunner } from './testExec';
import { suiteSyntaxError, programParseError, GATE_UNAVAILABLE } from './testExec';
import {
  certifyCaseAgainstStubs,
  isFrozenEligible,
  isProvisionallyFrozenEligible,
} from './caseCertification';
import type { CaseCertification } from './caseCertification';
import type { ActionLedger } from './actionLedger';
import {
  buildCanonicalKey,
  classifyFindingCategory,
  SUITE_SENTINEL,
  type FindingCategory,
} from './defectRegistry';
import { classifyFailure } from './substrate';

/**
 * SET 91b — status is no longer binary. A suite may:
 *   `certified`         — ≥1 non-provisional certified case, other rejected
 *                         cases dropped-with-disclosure. Contributes to the
 *                         artifact-level ship gate.
 *   `partial-certified` — only provisional cases certified (structural
 *                         anchor / dep-seam). Suite freezes so downstream
 *                         features can proceed; blocks ship gate.
 *   `uncertified`       — gate infrastructure was unavailable. Suite freezes
 *                         (fail-open execution) but is NOT certified. Blocks
 *                         ship gate (doctrine 17).
 *   `refused`           — structural fault in the suite itself, OR zero
 *                         cases survived. Suite does NOT freeze.
 * Only `refused` triggers dependency cascade in downstream features
 * (`uses X — dependency uncovered`). See SET 91d.
 */
export type FrozenSuiteStatus =
  | 'certified'
  | 'partial-certified'
  | 'uncertified'
  | 'refused';

/** SET 91b — a case that was dropped from a per-case aggregation. */
export interface DroppedCase {
  caseName: string;
  disposition: CaseCertification['disposition'];
  reason: string;
}

export interface FrozenSuiteReport {
  status: FrozenSuiteStatus;
  featureId: string;
  featureTestFile: string;
  /** Case-by-case certifications, one per case in `parts.cases`. */
  cases: CaseCertification[];
  shapeFindings: string[];
  syntaxError: string | null;
  /** Program parse rejection message; null on clean; `GATE_UNAVAILABLE` when infra unavailable. */
  programParseError: string | null;
  /**
   * True when the parse/probe gate could not run (CSP / EvalError). The run
   * may CONTINUE in an explicitly uncertified diagnostic mode; the suite may
   * not receive certified frozen status.
   */
  gateInfrastructureUnavailable: boolean;
  /**
   * Present when the report is a rejection: the ordered list of named reasons.
   * Never contains the word "red" as a disposition (the taxonomy is closed).
   * SET 91c — infrastructure-unavailable messages live on `infrastructureNotes`
   * instead, so infrastructure failure does not masquerade as a candidate
   * rejection.
   */
  rejectionReasons: string[];
  /** SET 91c — infrastructure diagnostics (parse probe / CSP / EvalError). */
  infrastructureNotes: string[];
  /** SET 91b — cases dropped by per-case aggregation, with disclosure text. */
  droppedCases: DroppedCase[];
  /** The rendered suite content that certification evaluated (may be discarded). */
  suiteContent: string;
}

export interface FrozenState {
  frozenTests: GeneratedFile[];
  frozenByFeature: Map<string, string>;
  irPartsByFeature: Map<string, IRParts>;
}

export interface CertifyFrozenSuiteOpts {
  spec: FeatureSpec;
  feature: FeatureNode;
  prompt: string;
  parts: IRParts;
  runSuite: SuiteRunner;
  /** Stubs for the whole spec. */
  stubs: GeneratedFile[];
  /**
   * SET 89 v2 — post-freeze mutations (mechanical suite fix, IR-side
   * case surgery, judgment refreeze, case-relay adoption) supply the
   * already-patched bytes here instead of reassembling from canonical
   * parts. When set, this content is what suite shape / syntax / parse
   * gates verify AND what is committed on acceptance. Per-case
   * certification still runs from `parts.cases` (each case is
   * independently assembled), so a mechanical patch cannot silently
   * revive a `rejected-vacuous` or `rejected-stub-derived` case.
   */
  overrideContent?: string;
  /**
   * SET 89 v2 — the fixture-fallback path (legacy) supplies suite
   * content without canonical `IRParts`. When true, per-case
   * certification is SKIPPED (there are no cases to certify) but suite
   * shape, standalone syntax, and whole-program parse gates still run.
   * The freeze remains atomic and the incumbent stays byte-identical on
   * rejection. Every use site MUST log a disclosure event.
   */
  bypassCaseCertification?: boolean;
}

/**
 * Deterministic certification: accepts canonical parts, returns the
 * structured report. Does NOT touch any state.
 */
export async function certifyFrozenSuite(
  opts: CertifyFrozenSuiteOpts,
): Promise<FrozenSuiteReport> {
  const { spec, feature, prompt, parts, runSuite, stubs, overrideContent, bypassCaseCertification } = opts;

  // 1) Suite content: prefer `overrideContent` (post-freeze mutation) so
  //    the exact bytes that will be committed are what we certify. Fall
  //    back to canonical reassembly from parts for the initial-freeze path.
  const wired = overrideContent
    ?? wireSuiteScaffoldImports(
      assembleTestFile(feature, parts.imports, parts.cases),
      feature.testFile, spec,
    ).content;

  // 2) Shape findings first — these do not require the parse gate.
  const shape = validateSuiteShape(wired);

  // 3) Standalone suite syntax.
  const syn = suiteSyntaxError(wired);

  // 4) Whole-program parse against the complete stub artifact + this suite.
  const parseMsg = programParseError(stubs, { path: feature.testFile, content: wired });
  const gateInfrastructureUnavailable = parseMsg === GATE_UNAVAILABLE;
  const programParse = gateInfrastructureUnavailable ? null : parseMsg;

  // 5) Case certifications — run in parts.cases order. Bypass ONLY when
  //    the caller documents it (fixture-fallback: no canonical parts to
  //    certify per case).
  const caseCerts: CaseCertification[] = [];
  if (!bypassCaseCertification) {
    for (const c of parts.cases) {
      // eslint-disable-next-line no-await-in-loop -- deterministic order
      const cert = await certifyCaseAgainstStubs({
        spec,
        feature,
        prompt,
        imports: parts.imports,
        caseGen: c,
        runSuite,
        stubs,
      });
      caseCerts.push(cert);
    }
  }

  // 6) SET 91b/c — per-case aggregation. Structural suite faults (shape,
  //    syntax, whole-program parse) refuse the suite outright — those are
  //    "this suite cannot exist as a program." Case-level rejections are
  //    partitioned into dropped-with-disclosure; case-level certifications
  //    determine whether we're `certified` (has non-provisional), only
  //    `partial-certified` (only structural anchors/dep-seams), or `refused`
  //    (zero cases certified). Gate infrastructure failure yields
  //    `uncertified`, not `refused` — the suite freezes but does not certify.
  const rejectionReasons: string[] = [];
  const infrastructureNotes: string[] = [];

  const structuralFault = shape.length > 0 || !!syn || !!programParse;
  if (shape.length) {
    for (const s of shape) rejectionReasons.push(`suite shape: ${s}`);
  }
  if (syn) rejectionReasons.push(`suite syntax: ${syn.split('\n')[0]!.slice(0, 160)}`);
  if (programParse) rejectionReasons.push(`program parse: ${programParse.split('\n')[0]!.slice(0, 160)}`);

  // A suite with canonical parts must have ≥1 case. Bypassed suites (fixture
  // fallback) have no canonical parts and are exempt from this rule.
  if (!bypassCaseCertification && caseCerts.length === 0) {
    rejectionReasons.push('suite has no cases');
  }

  const nonProvisionalCerts = caseCerts.filter(c =>
    (c.disposition === 'certified-behavior-red'
      || c.disposition === 'certified-licensed-throw')
    && !c.provisional);
  const provisionalCerts = caseCerts.filter(c =>
    c.disposition === 'certified-anchor'
    || c.disposition === 'certified-dep-seam'
    || c.provisional);
  const rejectedCases = caseCerts.filter(c => c.disposition.startsWith('rejected'));
  const droppedCases: DroppedCase[] = rejectedCases.map(c => ({
    caseName: c.caseName,
    disposition: c.disposition,
    reason: c.reason,
  }));

  if (gateInfrastructureUnavailable) {
    // Doctrine 17 — infrastructure failure IS NOT candidate failure. It goes
    // on its own diagnostic list; the suite is `uncertified` not `refused`.
    infrastructureNotes.push('parse probe could not run — gate infrastructure unavailable');
  }

  let status: FrozenSuiteStatus;
  if (structuralFault || (!bypassCaseCertification && caseCerts.length === 0)) {
    status = 'refused';
  } else if (gateInfrastructureUnavailable) {
    status = 'uncertified';
  } else if (nonProvisionalCerts.length >= 1) {
    status = 'certified';
  } else if (provisionalCerts.length >= 1) {
    status = 'partial-certified';
  } else if (bypassCaseCertification) {
    // Fixture-fallback path: no cases to certify, no infrastructure fault, no
    // structural fault. This is legacy behavior and remains certified for
    // continuity, matching pre-SET-91 semantics for that path.
    status = 'certified';
  } else {
    status = 'refused';
  }

  return {
    status,
    featureId: feature.id,
    featureTestFile: feature.testFile,
    cases: caseCerts,
    shapeFindings: shape,
    syntaxError: syn,
    programParseError: programParse,
    gateInfrastructureUnavailable,
    rejectionReasons,
    infrastructureNotes,
    droppedCases,
    suiteContent: wired,
  };
}

export interface CommitFrozenSuiteOpts extends CertifyFrozenSuiteOpts {
  state: FrozenState;
  /** Human-readable reason (initial-freeze / mechanical-fix / surgery / relay / …). */
  reason: string;
  /**
   * Optional: when the caller has bookkeeping that must run atomically with
   * the state write on acceptance (e.g. supergit records, metrics), pass a
   * callback. It runs synchronously AFTER the maps are updated on acceptance
   * only, never on rejection.
   */
  onAcceptance?: (report: FrozenSuiteReport) => void;
  /**
   * Optional: run on rejection instead. Never touches state; useful for
   * emitting a diagnostic event.
   */
  onRejection?: (report: FrozenSuiteReport) => void;
  /** SET 89 §11 — structured action ledger for frozen commits. */
  actionLedger?: ActionLedger | null;
  /** Signature of frozen state before mutation (caller-supplied or derived). */
  beforeSignature?: string;
}

export type CommitFrozenSuiteResult =
  | { status: 'accepted'; report: FrozenSuiteReport; actionId?: string }
  | { status: 'rejected'; report: FrozenSuiteReport; actionId?: string };

function frozenStateSignature(state: FrozenState): string {
  const parts = [...state.frozenByFeature.entries()]
    .sort(([a], [b]) => a.localeCompare(b))
    .map(([id, content]) => `${id}:${content.length}:${content.slice(0, 64)}`);
  return parts.join('|');
}

/**
 * Transactional commit: certifies + writes atomically. NO OTHER MODULE MAY
 * mutate `frozenTests` / `frozenByFeature` / `irPartsByFeature`. This is the
 * single source of frozen-state truth (SET 89 §4.4).
 */
export async function commitFrozenSuite(
  opts: CommitFrozenSuiteOpts,
): Promise<CommitFrozenSuiteResult> {
  const ledger = opts.actionLedger ?? null;
  const before = opts.beforeSignature ?? frozenStateSignature(opts.state);
  let actionId: string | undefined;
  if (ledger) {
    actionId = ledger.startAction({
      kind: 'frozen-suite-commit',
      target: opts.feature.id,
      phase: opts.reason,
      beforeSignature: before,
      startEvidence: { reason: opts.reason, testFile: opts.feature.testFile },
    });
  }
  try {
    const report = await certifyFrozenSuite(opts);
    // SET 91b — `refused` is the only non-landing status. `certified`,
    // `partial-certified`, and `uncertified` all commit atomically; the
    // status distinction propagates to the run report and the artifact
    // ship gate (see certifyArtifact), NOT to whether bytes land here.
    if (report.status === 'refused') {
      // Zero writes on refusal — the incumbent stays byte-identical.
      opts.onRejection?.(report);
      if (ledger && actionId) {
        ledger.finishAction({
          actionId,
          disposition: 'rejected',
          afterSignature: frozenStateSignature(opts.state),
          endEvidence: { rejectionReasons: report.rejectionReasons },
        });
      }
      return { status: 'rejected', report, actionId };
    }
    // Atomic write: mutate the frozen-state representations in the same
    // synchronous block. Order chosen to keep `frozenTests` last so a reader
    // that touches `frozenByFeature` first cannot see a torn state.
    //
    // Fixture-fallback callers (bypass mode) do NOT own canonical `IRParts`,
    // so the parts map is not overwritten by them — preserving whatever
    // prior canonical parts the feature had (if any). The bytes map is
    // still updated atomically alongside `frozenTests`.
    const { state, feature } = opts;
    const testFile = feature.testFile;
    const content = report.suiteContent;
    if (!opts.bypassCaseCertification) {
      state.irPartsByFeature.set(feature.id, opts.parts);
    }
    state.frozenByFeature.set(feature.id, content);
    const existingIdx = state.frozenTests.findIndex(f => f.path === testFile);
    if (existingIdx >= 0) state.frozenTests[existingIdx] = { path: testFile, content };
    else state.frozenTests.push({ path: testFile, content });
    opts.onAcceptance?.(report);
    if (ledger && actionId) {
      ledger.finishAction({
        actionId,
        disposition: 'applied',
        afterSignature: frozenStateSignature(state),
        endEvidence: { testFile, bytes: content.length },
      });
    }
    return { status: 'accepted', report, actionId };
  } catch (error) {
    if (ledger && actionId) {
      ledger.finishAction({
        actionId,
        disposition: 'failed',
        failureMode: 'fatal',
        error,
        afterSignature: frozenStateSignature(opts.state),
      });
    }
    throw error;
  }
}

/**
 * SET 89D — terminal whole-artifact certification. Distinct from freeze-time
 * `certifyFrozenSuite` (which requires cases to FAIL against stubs).
 *
 * Terminal certification runs every frozen suite against the FINAL
 * implementation and requires them to PASS. Findings use the same identity /
 * classification machinery as authoritative measurements.
 */
export type TerminalFinding = {
  featureId: string;
  caseId: string;
  classKey: string;
  category: FindingCategory;
  artifactPath?: string;
  expected?: string | null;
  received?: string | null;
  crashSignature?: string | null;
  message: string;
  canonicalKey: string;
};

export interface ArtifactCertification {
  artifactCertified: boolean;
  /** Freeze-time shape reports retained for diagnostics (not the pass oracle). */
  suiteReports: Map<string, FrozenSuiteReport>;
  /** Ordered list of terminal unresolved-defect strings (empty when certified). */
  unresolved: string[];
  /** Structured terminal findings (authoritative for registry reconciliation). */
  findings: TerminalFinding[];
  /** True when any suite ran under gateInfrastructureUnavailable. */
  gateInfrastructureUnavailable: boolean;
  /** True when expected owned files were missing / not vetted. */
  missingOwnedFiles: string[];
  /** True when frozen maps disagree with reassembled suite content. */
  representationInconsistent: string[];
  /** Provisional cases that block certification. */
  provisionalCases: string[];
}

export interface CertifyArtifactOpts {
  spec: FeatureSpec;
  prompt: string;
  runSuite: SuiteRunner;
  /** The FINAL implementation artifact (post-repair, post-relay). */
  artifact: GeneratedFile[];
  /** The frozen state after all mutation paths. */
  state: FrozenState;
  /**
   * When provided, AND-ed into `artifactCertified`. Callers supply registry /
   * ledger conservation (uniqueDefectsUnresolved === 0, actionPending === 0).
   */
  uniqueDefectsUnresolved?: number;
  actionPending?: number;
  hasDuplicateActionTerminals?: boolean;
}

function extractExpectedReceived(message: string): {
  expected: string | null;
  received: string | null;
} {
  const exp = /Expected:\s*([^\n]+)/i.exec(message)?.[1]?.trim() ?? null;
  const rec = /Received:\s*([^\n]+)/i.exec(message)?.[1]?.trim() ?? null;
  return { expected: exp, received: rec };
}

/**
 * Terminal certification against the FINAL artifact.
 *
 * Order (caller must invoke after all mutations):
 *   reassemble final artifact → this function → create terminal result/error
 *   rows → reconcile defect registry → assert action conservation → metrics.
 *
 * Does NOT reuse the freeze-time "must fail against stubs" predicate.
 */
export async function certifyArtifact(
  opts: CertifyArtifactOpts,
): Promise<ArtifactCertification> {
  const { spec, runSuite, artifact, state } = opts;
  const reports = new Map<string, FrozenSuiteReport>();
  const unresolved: string[] = [];
  const findings: TerminalFinding[] = [];
  const missingOwnedFiles: string[] = [];
  const representationInconsistent: string[] = [];
  const provisionalCases: string[] = [];
  let gateUnavailable = false;

  const byPath = new Map(artifact.map(f => [f.path, f]));

  // 1–2) Expected owned files present (non-stub) for every frozen feature.
  for (const feature of spec.features) {
    if (!state.frozenByFeature.has(feature.id)) continue;
    for (const p of feature.files) {
      const f = byPath.get(p);
      if (!f || /stub:\s*not implemented/i.test(f.content)) {
        missingOwnedFiles.push(`${feature.id}:${p}`);
        unresolved.push(`${feature.id}: missing or unvetted owned file ${p}`);
      }
    }
  }

  // 3–5) Representation consistency + provisional + parse/syntax gates,
  // then execute each frozen suite against the FINAL artifact (must PASS).
  for (const feature of spec.features) {
    const frozenContent = state.frozenByFeature.get(feature.id);
    if (!frozenContent) {
      // Features that never froze are not terminal certification subjects
      // unless the run has zero frozen suites (handled below).
      continue;
    }
    const parts = state.irPartsByFeature.get(feature.id);
    if (parts) {
      const reassembled = wireSuiteScaffoldImports(
        assembleTestFile(feature, parts.imports, parts.cases),
        feature.testFile, spec,
      ).content;
      if (reassembled !== frozenContent) {
        representationInconsistent.push(feature.id);
        unresolved.push(
          `${feature.id}: frozen suite bytes disagree with canonical IRParts reassembly`,
        );
      }
      const ownedSymbols = new Set(feature.exports.map(e => e.symbol));
      const depNames = new Set(
        feature.deps
          .map(id => spec.features.find(f => f.id === id)?.name)
          .filter((n): n is string => !!n),
      );
      for (const c of parts.cases) {
        // Shape-only anchors remain provisional and block artifact certification.
        if (/expect\s*\(\s*\(\s*\)\s*=>/.test(c.body) && /\.not\.toThrow\s*\(\s*\)/.test(c.body)
          && !/\.toBe\b|\.toEqual\b|\.toThrow\s*\(\s*['"`/]/.test(c.body)) {
          provisionalCases.push(`${feature.id}:${c.name}`);
          unresolved.push(
            `${feature.id}: case "${c.name}" is provisional (shape-only anchor)`,
          );
          continue;
        }
        // SET 91e — class-anchor and dep-seam cases are structural certifications
        // (provisional under SET 91a). They anchor the suite for build continuity
        // but do NOT confer artifact-level certification; doctrine 19.
        const isAnchor = ownedSymbols.has(c.name);
        const depSeamName = /^uses\s+(.+)$/.exec(c.name)?.[1]?.trim();
        const isDepSeam = c.kind === 'integration'
          || (!!depSeamName && depNames.has(depSeamName));
        if (isAnchor || isDepSeam) {
          provisionalCases.push(`${feature.id}:${c.name}`);
          unresolved.push(
            `${feature.id}: case "${c.name}" is provisional (${isAnchor ? 'class-anchor' : 'dep-seam'})`,
          );
        }
      }
    } else {
      unresolved.push(`${feature.id}: frozen bytes without canonical IRParts`);
    }

    const syn = suiteSyntaxError(frozenContent);
    if (syn) {
      unresolved.push(`${feature.id}: suite syntax: ${syn.split('\n')[0]!.slice(0, 160)}`);
      const category = classifyFindingCategory({ kindHint: 'parse', message: syn });
      const classKey = 'parse|suite-syntax';
      const caseId = SUITE_SENTINEL;
      findings.push({
        featureId: feature.id,
        caseId,
        classKey,
        category,
        artifactPath: feature.testFile,
        message: syn,
        canonicalKey: buildCanonicalKey({
          featureId: feature.id, caseId, classKey, artifactPath: feature.testFile,
          expected: syn, received: null,
        }),
      });
    }
    const parseMsg = programParseError(artifact, { path: feature.testFile, content: frozenContent });
    if (parseMsg === GATE_UNAVAILABLE) {
      gateUnavailable = true;
      unresolved.push(`${feature.id}: gate infrastructure unavailable`);
    } else if (parseMsg) {
      unresolved.push(`${feature.id}: program parse: ${parseMsg.split('\n')[0]!.slice(0, 160)}`);
      const category = classifyFindingCategory({ kindHint: 'parse', message: parseMsg });
      const classKey = 'parse|program';
      const caseId = SUITE_SENTINEL;
      findings.push({
        featureId: feature.id,
        caseId,
        classKey,
        category,
        artifactPath: feature.files[0],
        message: parseMsg,
        canonicalKey: buildCanonicalKey({
          featureId: feature.id, caseId, classKey, artifactPath: feature.files[0],
          expected: parseMsg, received: null,
        }),
      });
    }

    // Diagnostic freeze-time report retained for suiteReports (stubs path) —
    // NOT used as the pass oracle. Prefer parts when available.
    if (parts) {
      // eslint-disable-next-line no-await-in-loop
      const freezeShape = await certifyFrozenSuite({
        spec, feature, prompt: opts.prompt, parts, runSuite, stubs: feature.stubs,
      });
      reports.set(feature.id, freezeShape);
    }

    // 6–8) Execute frozen suite against FINAL artifact — must PASS.
    // eslint-disable-next-line no-await-in-loop
    let run: Awaited<ReturnType<SuiteRunner>>;
    try {
      run = await runSuite(artifact, { path: feature.testFile, content: frozenContent });
    } catch (e) {
      const msg = e instanceof Error ? e.message : String(e);
      unresolved.push(`${feature.id}: harness crash: ${msg.slice(0, 160)}`);
      const classKey = 'crash|harness';
      const caseId = SUITE_SENTINEL;
      findings.push({
        featureId: feature.id,
        caseId,
        classKey,
        category: 'crash',
        message: msg,
        crashSignature: msg,
        canonicalKey: buildCanonicalKey({
          featureId: feature.id, caseId, classKey, crashSignature: msg,
        }),
      });
      continue;
    }

    if (run.crashed) {
      unresolved.push(`${feature.id}: module/harness crash: ${run.crashed.slice(0, 160)}`);
      const cls = classifyFailure('__suite__', run.crashed);
      const classKey = cls.classKey || 'crash|unattributed';
      const caseId = SUITE_SENTINEL;
      findings.push({
        featureId: feature.id,
        caseId,
        classKey,
        category: 'crash',
        artifactPath: cls.detail.file,
        message: run.crashed,
        crashSignature: run.crashed,
        canonicalKey: buildCanonicalKey({
          featureId: feature.id, caseId, classKey,
          artifactPath: cls.detail.file, crashSignature: run.crashed,
        }),
      });
      continue;
    }

    for (const fail of run.failures) {
      const cls = classifyFailure(fail.name, fail.message);
      const { expected, received } = extractExpectedReceived(fail.message);
      const category = classifyFindingCategory({
        classKey: cls.classKey,
        message: fail.message,
        kindHint: cls.kind === 'assertion' || /expected|received|tobe|tothrow/i.test(fail.message)
          ? 'test-failure'
          : undefined,
      });
      const caseId = fail.name || SUITE_SENTINEL;
      const classKey = cls.classKey || `${category}|${caseId}`;
      const finding: TerminalFinding = {
        featureId: feature.id,
        caseId,
        classKey,
        category,
        artifactPath: cls.detail.file ?? feature.files[0],
        expected: expected ?? cls.detail.expected ?? null,
        received: received ?? cls.detail.received ?? null,
        message: fail.message,
        canonicalKey: buildCanonicalKey({
          featureId: feature.id,
          caseId,
          classKey,
          artifactPath: cls.detail.file ?? feature.files[0],
          expected: expected ?? cls.detail.expected,
          received: received ?? cls.detail.received,
        }),
      };
      findings.push(finding);
      unresolved.push(
        `${feature.id}: case "${caseId}" ${category}: ${fail.message.split('\n')[0]!.slice(0, 160)}`,
      );
    }
  }

  if (state.frozenByFeature.size === 0) {
    unresolved.push('no frozen suites — artifact cannot be certified');
  }
  if (gateUnavailable) {
    // Already pushed per-feature; ensure global flag blocks certification.
  }

  const unresolvedDefects = opts.uniqueDefectsUnresolved ?? 0;
  const pendingActions = opts.actionPending ?? 0;
  const dupTerminals = opts.hasDuplicateActionTerminals ?? false;

  const artifactCertified =
    unresolved.length === 0
    && findings.length === 0
    && missingOwnedFiles.length === 0
    && representationInconsistent.length === 0
    && provisionalCases.length === 0
    && !gateUnavailable
    && state.frozenByFeature.size > 0
    && unresolvedDefects === 0
    && pendingActions === 0
    && !dupTerminals;

  return {
    artifactCertified,
    suiteReports: reports,
    unresolved,
    findings,
    gateInfrastructureUnavailable: gateUnavailable,
    missingOwnedFiles,
    representationInconsistent,
    provisionalCases,
  };
}

export { isFrozenEligible, isProvisionallyFrozenEligible };
