import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
import { safeExec, WindtunnelLockSchema } from '@mmnto/totem';
import { cleanTmpDir } from '../test-utils.js';
import { assertCorpusCompleteness, buildReadStrategy, computeFixtureSha, isCommitAncestor, verifyControlIntegrity, verifyFreezeProof, } from './spine-windtunnel.js';
// ─── Fixtures ────────────────────────────────────────────
const HEX40 = /^[0-9a-f]{40}$/;
const FAKE_SHA = 'a'.repeat(40);
const createdDirs = [];
afterEach(() => {
    while (createdDirs.length > 0) {
        cleanTmpDir(createdDirs.pop());
    }
});
/** Create an isolated temp dir tracked for afterEach cleanup. */
function makeTmpDir() {
    const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'totem-spine-wt-'));
    createdDirs.push(dir);
    return dir;
}
/** Initialize a throwaway git repo with a deterministic identity. */
function makeTmpRepo() {
    const dir = makeTmpDir();
    safeExec('git', ['init'], { cwd: dir });
    safeExec('git', ['config', 'user.email', 'test@example.com'], { cwd: dir });
    safeExec('git', ['config', 'user.name', 'Totem Test'], { cwd: dir });
    safeExec('git', ['config', 'commit.gpgsign', 'false'], { cwd: dir });
    return dir;
}
/** Stage everything and commit; returns the new commit SHA. */
function commitAll(dir, message) {
    safeExec('git', ['add', '-A'], { cwd: dir });
    safeExec('git', ['commit', '-m', message], { cwd: dir });
    return safeExec('git', ['rev-parse', 'HEAD'], { cwd: dir });
}
/** Build a schema-valid lock pinned to `asOfCommit`. */
function makeLock(asOfCommit) {
    return WindtunnelLockSchema.parse({
        schema: 'windtunnel.lock.v1',
        canonicalPath: '.totem/spine/gate-1/windtunnel.lock.json',
        gate: 'gate-1',
        phase: 'harness',
        corpus: {
            repo: 'mmnto-ai/liquid-city',
            selectionRule: {
                state: 'merged',
                predicate: 'code-touching',
                window: { type: 'all' },
                asOfCommit,
            },
            resolvedPrs: [
                { pr: 1, mergeCommit: 'b'.repeat(40), baseSha: 'c'.repeat(40), headSha: 'd'.repeat(40) },
            ],
        },
        fpDefinition: {
            rubricRef: '.totem/spine/gate-1/fp-rubric.md',
            groundTruthRef: '.totem/spine/gate-1/ground-truth-labels.json',
            adjudicator: 'frozen-ground-truth+operator-tiebreak',
            precisionFloor: 1.0,
        },
        controls: {
            positiveRef: '.totem/spine/gate-1/controls/positive/',
            negativeRef: '.totem/spine/gate-1/controls/negative/',
            integrity: { mechanism: '.wind-tunnel-sha', fixtureSha: 'e'.repeat(40) },
        },
        cullRateThreshold: 0.5,
        exposureDenominator: {
            activeRulesEvaluated: { floor: 2 },
            filesTouchedInWindow: { floor: 0 },
            positiveControlsExercised: { floor: 0 },
        },
    });
}
// ─── verifyFreezeProof (C3) ──────────────────────────────
describe('verifyFreezeProof (C3 — git-derived freeze proof)', () => {
    it('throws when the lock has never been committed (repo has history, lock does not)', () => {
        const repo = makeTmpRepo();
        // The repo has commits, but the lock file itself was never committed —
        // git log succeeds and returns zero commits for the lock path.
        fs.writeFileSync(path.join(repo, 'README.md'), '# repo\n');
        commitAll(repo, 'initial');
        const lockPath = path.join(repo, 'windtunnel.lock.json');
        fs.writeFileSync(lockPath, '{"a":1}\n');
        expect(() => verifyFreezeProof(lockPath, repo, safeExec)).toThrow(/never been committed/);
    });
    it('passes when the lock is committed and unchanged (CRLF/newline immune)', () => {
        const repo = makeTmpRepo();
        const lockPath = path.join(repo, 'windtunnel.lock.json');
        // Trailing newline present — a raw string compare against `git show` would
        // spuriously fail here; the hash-based proof must pass.
        fs.writeFileSync(lockPath, '{"a":1}\n');
        commitAll(repo, 'freeze lock');
        expect(() => verifyFreezeProof(lockPath, repo, safeExec)).not.toThrow();
    });
    it('throws when the lock was modified after freeze (tamper)', () => {
        const repo = makeTmpRepo();
        const lockPath = path.join(repo, 'windtunnel.lock.json');
        fs.writeFileSync(lockPath, '{"a":1}\n');
        commitAll(repo, 'freeze lock');
        fs.writeFileSync(lockPath, '{"a":2}\n'); // post-freeze tamper
        expect(() => verifyFreezeProof(lockPath, repo, safeExec)).toThrow(/differs from the committed blob/);
    });
});
// ─── isCommitAncestor ────────────────────────────────────
describe('isCommitAncestor', () => {
    it('returns true for a genuine ancestor', () => {
        const repo = makeTmpRepo();
        fs.writeFileSync(path.join(repo, 'a.txt'), '1');
        const c1 = commitAll(repo, 'c1');
        fs.writeFileSync(path.join(repo, 'b.txt'), '2');
        const c2 = commitAll(repo, 'c2');
        expect(isCommitAncestor(c1, c2, repo, safeExec)).toBe(true);
    });
    it('returns false for a non-ancestor (exit 1 is a clean boolean, not an error)', () => {
        const repo = makeTmpRepo();
        fs.writeFileSync(path.join(repo, 'a.txt'), '1');
        const c1 = commitAll(repo, 'c1');
        fs.writeFileSync(path.join(repo, 'b.txt'), '2');
        const c2 = commitAll(repo, 'c2');
        expect(isCommitAncestor(c2, c1, repo, safeExec)).toBe(false);
    });
    it('re-throws on a bad ref rather than masking it as false', () => {
        const repo = makeTmpRepo();
        fs.writeFileSync(path.join(repo, 'a.txt'), '1');
        commitAll(repo, 'c1');
        expect(() => isCommitAncestor(FAKE_SHA, 'HEAD', repo, safeExec)).toThrow();
    });
});
// ─── buildReadStrategy (C2) ──────────────────────────────
describe('buildReadStrategy (C2 — post-image blob, throw on missing)', () => {
    it('returns null for every file when no lc clone is provided', async () => {
        const readStrategy = buildReadStrategy(undefined, FAKE_SHA, safeExec);
        expect(await readStrategy('any/file.ts')).toBeNull();
    });
    it('resolves the post-image blob from the lc clone at asOfCommit', async () => {
        const repo = makeTmpRepo();
        fs.writeFileSync(path.join(repo, 'src.ts'), 'export const x = 1;\n');
        const sha = commitAll(repo, 'add src');
        const readStrategy = buildReadStrategy(repo, sha, safeExec);
        expect(await readStrategy('src.ts')).toContain('export const x = 1;');
    });
    it('throws on an unresolvable blob — never a silent no-match (corpus shrinkage)', async () => {
        const repo = makeTmpRepo();
        fs.writeFileSync(path.join(repo, 'src.ts'), 'x\n');
        const sha = commitAll(repo, 'add src');
        const readStrategy = buildReadStrategy(repo, sha, safeExec);
        await expect(readStrategy('does-not-exist.ts')).rejects.toThrow(/unresolvable/);
    });
});
// ─── computeFixtureSha (integrity) ───────────────────────
/** Create populated positive + negative control dirs; returns [positive, negative]. */
function makeControlDirs(repo) {
    const positive = path.join(repo, 'controls', 'positive');
    const negative = path.join(repo, 'controls', 'negative');
    fs.mkdirSync(positive, { recursive: true });
    fs.mkdirSync(negative, { recursive: true });
    fs.writeFileSync(path.join(positive, 'p1.diff'), 'positive control\n');
    fs.writeFileSync(path.join(negative, 'n1.diff'), 'negative control\n');
    return [positive, negative];
}
describe('computeFixtureSha (gate-1-scoped integrity hash)', () => {
    it('returns null when no control dir has files', () => {
        const repo = makeTmpRepo();
        const ctrl = path.join(repo, 'controls');
        fs.mkdirSync(ctrl, { recursive: true });
        expect(computeFixtureSha([ctrl], repo, safeExec)).toBeNull();
    });
    it('returns a stable aggregate hash across positive + negative dirs', () => {
        const repo = makeTmpRepo();
        const dirs = makeControlDirs(repo);
        const first = computeFixtureSha(dirs, repo, safeExec);
        const second = computeFixtureSha(dirs, repo, safeExec);
        expect(first).toMatch(HEX40);
        expect(second).toBe(first);
    });
    it('changes when a POSITIVE control file changes (tamper-evident)', () => {
        const repo = makeTmpRepo();
        const dirs = makeControlDirs(repo);
        const before = computeFixtureSha(dirs, repo, safeExec);
        fs.writeFileSync(path.join(dirs[0], 'p1.diff'), 'CHANGED\n');
        expect(computeFixtureSha(dirs, repo, safeExec)).not.toBe(before);
    });
    it('changes when a NEGATIVE control file changes (negative controls are protected too)', () => {
        const repo = makeTmpRepo();
        const dirs = makeControlDirs(repo);
        const before = computeFixtureSha(dirs, repo, safeExec);
        fs.writeFileSync(path.join(dirs[1], 'n1.diff'), 'CHANGED\n');
        expect(computeFixtureSha(dirs, repo, safeExec)).not.toBe(before);
    });
});
// ─── verifyControlIntegrity (C6 / §5) ────────────────────
describe('verifyControlIntegrity (C6 / §5 — mandatory, fail-loud)', () => {
    it('throws when a control dir is missing (no silent skip)', () => {
        const repo = makeTmpRepo();
        const [positive] = makeControlDirs(repo);
        const missingNegative = path.join(repo, 'controls', 'gone');
        expect(() => verifyControlIntegrity([positive, missingNegative], FAKE_SHA, repo, safeExec)).toThrow(/missing/);
    });
    it('throws when the control dirs are empty', () => {
        const repo = makeTmpRepo();
        const positive = path.join(repo, 'controls', 'positive');
        const negative = path.join(repo, 'controls', 'negative');
        fs.mkdirSync(positive, { recursive: true });
        fs.mkdirSync(negative, { recursive: true });
        expect(() => verifyControlIntegrity([positive, negative], FAKE_SHA, repo, safeExec)).toThrow(/empty/);
    });
    it('throws on a fixtureSha mismatch (tamper)', () => {
        const repo = makeTmpRepo();
        const dirs = makeControlDirs(repo);
        expect(() => verifyControlIntegrity(dirs, 'f'.repeat(40), repo, safeExec)).toThrow(/expected/);
    });
    it('passes when the aggregate hash matches the declared fixtureSha', () => {
        const repo = makeTmpRepo();
        const dirs = makeControlDirs(repo);
        const sha = computeFixtureSha(dirs, repo, safeExec);
        expect(() => verifyControlIntegrity(dirs, sha, repo, safeExec)).not.toThrow();
    });
});
// ─── assertCorpusCompleteness (S4) ───────────────────────
describe('assertCorpusCompleteness (S4 — loud on inaccessible clone)', () => {
    it('throws when the lc clone is inaccessible', async () => {
        const repo = makeTmpRepo();
        const missing = path.join(repo, 'no-such-clone');
        const lock = makeLock(FAKE_SHA);
        await expect(assertCorpusCompleteness(lock, missing, repo, safeExec)).rejects.toThrow(/cannot access lc clone/);
    });
    it('resolves when the lc clone is accessible and includes asOfCommit', async () => {
        const lcRepo = makeTmpRepo();
        fs.writeFileSync(path.join(lcRepo, 'x.ts'), '1');
        const sha = commitAll(lcRepo, 'lc c1');
        const repo = makeTmpRepo();
        const lock = makeLock(sha);
        await expect(assertCorpusCompleteness(lock, lcRepo, repo, safeExec)).resolves.toBeUndefined();
    });
});
//# sourceMappingURL=spine-windtunnel.test.js.map