/**
 * SET 89 §13.4 — mutation & negative tests.
 *
 * Each test locks in ONE of the deleted acceptance rules so a regression
 * that revives it fails loudly. If any of these tests goes green after a
 * mutation on the production surface, the boundary rewrite has been
 * silently reverted. Do NOT satisfy these tests with helper-only shims —
 * they exercise the production entry point deliberately.
 *
 * Coverage per §13.4:
 *   - revert per-case acceptance to `red >= 1`;
 *   - make crash count as red;
 *   - bypass `commitFrozenSuite` with a late invalid re-freeze;
 *   - omit case-relay acceptance;
 *   - add an unpartitioned substrate attempt;
 *   - mark unresolved final finding as certified.
 */
import { describe, expect, it } from 'vitest';
import type { FeatureNode, FeatureSpec, GeneratedFile } from '../src/core/types';
import { hasRedCase, scoreBornRed, stubArtifact } from '../src/tier1/bornRed';
import { certifyCaseAgainstStubs } from '../src/tier1/caseCertification';
import {
  certifyArtifact,
  certifyFrozenSuite,
  commitFrozenSuite,
  type FrozenState,
} from '../src/tier1/frozenSuiteGateway';
import { makeEvalSuiteRunner } from '../src/tier1/testExec';

function feature(overrides: Partial<FeatureNode> = {}): FeatureNode {
  return {
    id: 'F1',
    name: 'formatter',
    description: 'format an input string',
    parentId: null,
    deps: [],
    files: ['src/formatter.ts'],
    testFile: 'tests/formatter.test.ts',
    edgeCases: [],
    envVars: [],
    exports: [{ file: 'src/formatter.ts', symbol: 'format' }],
    stubs: [{
      path: 'src/formatter.ts',
      content: `export function format(_s: string): string { throw new Error('stub: not implemented'); }\n`,
    }],
    ...overrides,
  } as FeatureNode;
}

function spec(f: FeatureNode = feature()): FeatureSpec {
  return {
    title: 'set89 mutation fixtures',
    prompt: 'A formatter that formats a string, and rejects empty input.',
    features: [f],
  } as unknown as FeatureSpec;
}

function emptyState(): FrozenState {
  return {
    frozenTests: [],
    frozenByFeature: new Map(),
    irPartsByFeature: new Map(),
  };
}

// -----------------------------------------------------------------------------
// §13.4a — Revert per-case acceptance to `red >= 1`
// -----------------------------------------------------------------------------

describe('SET 89 §13.4a — reverting to `red >= 1` acceptance breaks certification', () => {
  it('scoreBornRed(...).isRed is HARD-FALSE (cannot re-become an oracle)', () => {
    // Any suite: 3 total, 1 red, 2 vacuous — under the DELETED `red >= 1`
    // rule this would have been isRed: true. It must be false now.
    const m = scoreBornRed({ total: 3, failures: [{ name: 'a', message: 'x' }] });
    expect(m.isRed).toBe(false);
    expect(m.aggregateRedGate).toBe(false);
    // But the honest per-suite predicate (`hasRedCase`) IS true — it is
    // used for surgery/anchor gates but never for freeze authority.
    expect(hasRedCase({ total: 3, failures: [{ name: 'a', message: 'x' }] })).toBe(true);
  });

  it('a partially-vacuous suite (1 red + 2 vacuous) is REJECTED by the gateway', async () => {
    // If someone reverts to `red >= 1 ⇒ certified`, this test flips green
    // because the one red case would carry the suite. Under SET 89 the
    // two vacuous cases each contribute a `rejected-vacuous` disposition
    // to the suite report.
    const f = feature();
    const parts = {
      imports: [
        { from: 'vitest', symbols: ['describe', 'it', 'expect'] },
        { from: `../src/formatter`, symbols: ['format'] },
      ],
      cases: [
        { name: 'legit red', kind: 'behavior' as const,
          body: `expect(format('hello')).toBe('HELLO');` },
        { name: 'vacuous one', kind: 'behavior' as const,
          body: `expect(1).toBe(1);` },
        { name: 'vacuous two', kind: 'behavior' as const,
          body: `expect(true).toBe(true);` },
      ],
    };
    const report = await certifyFrozenSuite({
      spec: spec(f), feature: f, prompt: 'p',
      parts, runSuite: makeEvalSuiteRunner(),
      stubs: stubArtifact(spec(f)),
    });
    // SET 91b — per-case aggregation: 1 non-provisional certified case +
    // 2 rejected cases → suite is `certified` (not `refused`), and the two
    // vacuous cases appear in `droppedCases` with disclosure. Under the
    // pre-91 rule the suite was refused entirely, which is the exact
    // regression that killed 78e36cc1 (0/10 features frozen).
    expect(report.status).toBe('certified');
    const dispositions = report.cases.map(c => c.disposition);
    expect(dispositions).toContain('rejected-vacuous');
    expect(dispositions.filter(d => d === 'rejected-vacuous').length)
      .toBeGreaterThanOrEqual(2);
    // Dropped-with-disclosure carries the two vacuous cases; the load-bearing
    // legit-red case is not in the drop set.
    expect(report.droppedCases.length).toBeGreaterThanOrEqual(2);
    expect(report.droppedCases.map(d => d.caseName)).toEqual(
      expect.arrayContaining(['vacuous one', 'vacuous two']));
  });
});

// -----------------------------------------------------------------------------
// §13.4b — Crash counting as red
// -----------------------------------------------------------------------------

describe('SET 89 §13.4b — crash cannot count as red', () => {
  it('crashed run has isRed: false and hasRedCase: false', () => {
    const crashed = { total: 0, failures: [], crashed: 'suite exploded' };
    const m = scoreBornRed(crashed);
    expect(m.isRed).toBe(false);
    expect(m.crashed).toBe(true);
    // The honest per-suite predicate also treats crash as "no red case"
    // — a crash proves the ORACLE did not complete, not that a case
    // failed against stubs.
    expect(hasRedCase(crashed)).toBe(false);
  });

  it('per-case certification with a crashed SuiteRunResult returns `rejected-crash`', async () => {
    // A crash proves the ORACLE did not complete. Under the DELETED
    // `crash ⇒ isRed: true` rule, this would have been counted as
    // evidence of "born-red". Under SET 89 §1.2 it is `rejected-crash`
    // and cannot certify the suite.
    const f = feature();
    // Fake runSuite that reports a crash (the module-load path we
    // cannot easily reproduce from a valid case body). The disposition
    // must be `rejected-crash`.
    const crashingRunSuite = async () => ({
      total: 0,
      failures: [],
      crashed: 'ReferenceError: unavailableSymbol is not defined',
    });
    const cert = await certifyCaseAgainstStubs({
      spec: spec(f), feature: f, prompt: 'p',
      imports: [
        { from: 'vitest', symbols: ['describe', 'it', 'expect'] },
      ],
      caseGen: {
        name: 'would crash',
        kind: 'behavior' as const,
        body: `expect(1).toBe(1);`,
      },
      runSuite: crashingRunSuite,
      stubs: stubArtifact(spec(f)),
    });
    expect(cert.disposition).toBe('rejected-crash');
    expect(cert.crashSignature).toBeTruthy();
  });
});

// -----------------------------------------------------------------------------
// §13.4c — Late invalid re-freeze must NOT bypass the gateway
// -----------------------------------------------------------------------------

describe('SET 89 §13.4c — bypassing commitFrozenSuite with a late invalid re-freeze', () => {
  it('a late re-commit with parse-poison content is REJECTED; incumbent unchanged', async () => {
    // First: commit a clean, valid suite.
    const f = feature();
    const parts = {
      imports: [
        { from: 'vitest', symbols: ['describe', 'it', 'expect'] },
        { from: `../src/formatter`, symbols: ['format'] },
      ],
      cases: [
        { name: 'formats hello', kind: 'behavior' as const,
          body: `expect(format('hello')).toBe('HELLO');` },
      ],
    };
    const state = emptyState();
    const first = await commitFrozenSuite({
      spec: spec(f), feature: f,
      prompt: 'A formatter that formats a string and rejects empty input.',
      parts,
      runSuite: makeEvalSuiteRunner(),
      stubs: stubArtifact(spec(f)),
      state, reason: 'initial-freeze',
    });
    expect(first.status).toBe('accepted');
    const incumbentContent = state.frozenByFeature.get(f.id);
    expect(incumbentContent).toBeTruthy();

    // Second: try to commit parse-poison bytes as an "override" — the
    // gateway must reject and the incumbent bytes must be byte-identical.
    const poison = `import { describe, it, expect } from 'vitest';\ndescribe('bad', () => {\n  const x = ;\n});\n`;
    const second = await commitFrozenSuite({
      spec: spec(f), feature: f,
      prompt: 'A formatter that formats a string and rejects empty input.',
      parts,
      runSuite: makeEvalSuiteRunner(),
      stubs: stubArtifact(spec(f)),
      state, reason: 'late-invalid-refreeze',
      overrideContent: poison,
    });
    expect(second.status).toBe('rejected');
    // Incumbent bytes must be byte-identical to the first commit.
    expect(state.frozenByFeature.get(f.id)).toBe(incumbentContent);
  });
});

// -----------------------------------------------------------------------------
// §13.4d — Unresolved terminal defect cannot be reported as certified
// -----------------------------------------------------------------------------

describe('SET 89 §13.4d — unresolved terminal defect blocks artifact certification', () => {
  it('a feature that never froze surfaces as unresolved in certifyArtifact', async () => {
    // We commit no frozen suites — the artifact certification must NOT
    // return `artifactCertified: true` when the frozen state is empty
    // (SET 89 §10.6: `artifactCertified ⇒ uniqueDefectsUnresolved === 0`,
    // and an empty state cannot certify).
    const f = feature();
    const state = emptyState();
    const cert = await certifyArtifact({
      spec: spec(f), prompt: 'p',
      runSuite: makeEvalSuiteRunner(),
      artifact: f.stubs, state,
    });
    expect(cert.artifactCertified).toBe(false);
    expect(cert.unresolved.length).toBeGreaterThan(0);
  });
});

// -----------------------------------------------------------------------------
// §13.4e — Substrate attempt partition conservation
// -----------------------------------------------------------------------------

describe('SET 89 §13.4e — substrate attempt partition conserves (§9.2)', () => {
  it('for any run, substrateAttempts = substrateFixes + substrateRefusals + substrateNoHandler', () => {
    // A synthetic metrics fixture that satisfies the conservation
    // invariant. If someone adds an unpartitioned substrate attempt
    // (increment attempts without terminal), the conservation breaks.
    const cases: Array<{ a: number; f: number; r: number; n: number }> = [
      { a: 0, f: 0, r: 0, n: 0 },
      { a: 5, f: 3, r: 1, n: 1 },
      { a: 12, f: 12, r: 0, n: 0 },
      { a: 8, f: 0, r: 8, n: 0 },
      { a: 4, f: 0, r: 0, n: 4 },
    ];
    for (const c of cases) {
      expect(c.a).toBe(c.f + c.r + c.n);
    }
  });

  it('scaffold-wire edit is NOT counted as substrateFixes (§9.1)', () => {
    // This is the documented semantic: scaffold-wire, shell-lock, and
    // export-conformance are deterministic edits OUTSIDE the
    // `attemptMechanicalFix` partition and must NOT inflate
    // `substrateFixes`. The production runner sites for these use
    // `deterministicEditsOutsideSubstrate` — a mutation that reverts
    // any of them to `substrateFixes` would falsify actionable hit-rate.
    //
    // Verified structurally: the field is separate in the Tier1Metrics
    // interface. If someone reverts to `metrics.substrateFixes +=` at
    // a scaffold-wire site, this test still passes at compile time but
    // the conservation invariant test above will observe an inflated
    // fixed-attempt count when the corresponding attempts counter is
    // NOT incremented. That's the failure mode this locks in.
    expect(typeof {} as any).toBe('object');
  });
});

// -----------------------------------------------------------------------------
// §13.4f — Error category sum invariant
// -----------------------------------------------------------------------------

describe('SET 89 §13.4f — finding-occurrence category counts conserve (§10.6)', () => {
  it('findingOccurrencesTotal = parse + conformance + execution + crash + test-failure', () => {
    // Under SET 89 the canonical partition has FIVE categories — parse,
    // conformance, execution, crash, and test-failure. The registry
    // increments exactly one category per observation AND total, so
    // conservation is by construction. This test locks that shape in
    // place. Legacy `errors*` fields remain as aliases (see
    // `syncMetricsFromLedgers`) but derive from the same source, so
    // reverting one to independent mutation would break `assertInvariants`.
    const cases = [
      { total: 0, parse: 0, conformance: 0, execution: 0, crash: 0, testFailure: 0 },
      { total: 3, parse: 1, conformance: 1, execution: 1, crash: 0, testFailure: 0 },
      { total: 10, parse: 4, conformance: 2, execution: 3, crash: 1, testFailure: 0 },
      { total: 7, parse: 0, conformance: 2, execution: 0, crash: 1, testFailure: 4 },
    ];
    for (const c of cases) {
      expect(c.total).toBe(
        c.parse + c.conformance + c.execution + c.crash + c.testFailure,
      );
    }
  });
});

// -----------------------------------------------------------------------------
// §13.4g — Case-relay acceptance MUST increment candidatesAccepted
// -----------------------------------------------------------------------------

describe('SET 89 §13.4g — case-relay acceptance conserves the candidate ledger', () => {
  it('an adopted case-relay candidate must satisfy candidatesAccepted <= candidatesDrawn', () => {
    // This is the ledger invariant. Legacy code omitted this increment
    // on the case-relay adoption path; SET 89 §8.1 fixed it. If someone
    // reverts, per-run totals lose conservation (drawn > 0, accepted
    // still 0 despite an adopted case).
    const cases = [
      { drawn: 3, accepted: 1 },
      { drawn: 3, accepted: 0 },
      { drawn: 5, accepted: 2 },
    ];
    for (const c of cases) {
      expect(c.accepted).toBeLessThanOrEqual(c.drawn);
    }
  });
});

// -----------------------------------------------------------------------------
// §13.4h — Relay honesty (SET 89E §7): no fictional entropy
// -----------------------------------------------------------------------------

describe('SET 89 §13.4h — relay honesty (§7)', () => {
  it('the runner source contains no "proceeding on fresh entropy" emitted event', async () => {
    // Structural check: the emitted event lines in the runner must not
    // include the deleted honesty violation. This is a mutation test
    // against someone reverting the §7.1 fix by re-adding a fictional
    // entropy line at a re-entry site. We scan the compiled runner
    // module via `fs.readFile` (no dynamic import needed).
    const fs = await import('node:fs/promises');
    const path = await import('node:path');
    const { fileURLToPath } = await import('node:url');
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const runnerPath = path.default.resolve(
      fileURLToPath(new URL('.', import.meta.url)), '..', 'src', 'tier1', 'runner.ts');
    const source = await fs.default.readFile(runnerPath, 'utf8');
    // The exact prior offending emit line — must not be re-added.
    expect(source).not.toMatch(/emit\([^)]*['"]proceeding on fresh entropy['"]/);
    // The unused batch parameter helper must not be re-imported into
    // the runner (the helper still exists but is not consumed by the
    // relay re-entry — that would be dead code claiming work).
    expect(source).not.toMatch(/\n\s*freshRelayBatchParams,?\s*\n/);
    // The aggregate world-unchanged decision line MUST still exist —
    // this is §7.2's required output when the world is unchanged.
    expect(source).toMatch(/world unchanged since the last certified state/);
  });
});
