/**
 * last-green.test.mjs — Last-green ledger module (M048/S02/T01).
 *
 * Purpose: verify the ledger storage/comparison API that S03 and S04 depend on.
 * Tests cover the write/read/compare/clear round-trip, edge cases, and the
 * smoke_gate experimental flag guard.
 *
 * Consumer: autonomous regression harness; S03/S04 integration points.
 * Fails when: ledger format drifts, API behaviour changes, or experimental flag
 *   wiring breaks.
 */
import { afterEach, describe, expect, it, vi } from "vitest";
import {
	constants as fsConstants,
	existsSync,
	mkdtempSync,
	mkdirSync,
	readFileSync,
	rmSync,
	writeFileSync,
} from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";

// Mock experimental.js so we control getExperimentalFlag before last-green.js
// resolves its static import of it.
import { getExperimentalFlag } from "../experimental.js";
vi.mock("../experimental.js", () => ({
	// Default: smoke_gate on so most tests pass without per-test setup.
	getExperimentalFlag: vi.fn(() => true),
}));

// ---------------------------------------------------------------------------
// Cleanup
// ---------------------------------------------------------------------------

const tmpDirs = [];

afterEach(() => {
	// Reset mock so per-test mockReturnValue calls don't bleed into next test.
	vi.restoreAllMocks();
	// Clean up temp directories.
	while (tmpDirs.length > 0) {
		const d = tmpDirs.pop();
		if (d) rmSync(d, { recursive: true, force: true });
	}
});

function tmpDir() {
	const d = mkdtempSync(join(tmpdir(), "sf-last-green-"));
	tmpDirs.push(d);
	return d;
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/** Build a synthetic last-green entry for testing. */
function makeEntry(overrides = {}) {
	return {
		version: 1,
		fixture: {
			milestoneId: "M049",
			unitType: "complete-milestone",
			unitId: "M049",
			runId: "run-001",
			traceId: null,
			...overrides.fixture,
		},
		result: {
			ok: true,
			exitCode: 0,
			outputLength: 42,
			outputPreview: "smoke test complete",
			elapsedMs: 1234,
			dispatchCount: 1,
			...overrides.result,
		},
		checksum: "a1b2c3d4e5f6a1b2",
		timestamp: "2026-01-01T00:00:00.000Z",
		...overrides,
	};
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

import {
	clearLastGreen,
	compareToLastGreen,
	getLastGreenEntry,
	readLastGreenRecord,
	writeLastGreenRecord,
} from "../last-green.js";

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe("last-green ledger", () => {
	describe("writeLastGreenRecord", () => {
		it("write/read roundtrip — smoke_gate=true writes entry and returns true", () => {
			vi.mocked(getExperimentalFlag).mockReturnValue(true);
			const dir = mkdtempSync(join(tmpdir(), "sf-last-green-wr-"));
			const result = {
				ok: true,
				exitCode: 0,
				output: "smoke test output",
				elapsedMs: 567,
				dispatchCount: 3,
			};
			const wrote = writeLastGreenRecord(dir, result, {
				milestoneId: "M049",
				unitType: "complete-milestone",
				unitId: "M049",
			});
			expect(wrote).toBe(true);

			const ledger = readLastGreenRecord(dir);
			expect(ledger).not.toBeNull();
			expect(Array.isArray(ledger)).toBe(true);
			expect(ledger.length).toBe(1);
			expect(ledger[0].fixture.milestoneId).toBe("M049");
			expect(ledger[0].result.ok).toBe(true);
			expect(ledger[0].result.exitCode).toBe(0);
			expect(ledger[0].result.outputLength).toBe(17);
			expect(ledger[0].result.outputPreview).toBe("smoke test output");
			expect(ledger[0].result.elapsedMs).toBe(567);
			expect(ledger[0].result.dispatchCount).toBe(3);
			expect(typeof ledger[0].checksum).toBe("string");
			expect(ledger[0].checksum.length).toBeGreaterThan(0);
			expect(ledger[0].timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T/);

			rmSync(dir, { recursive: true, force: true });
		});

		it("smoke_gate=false suppresses write and returns false", () => {
			vi.mocked(getExperimentalFlag).mockReturnValue(false);
			const dir = mkdtempSync(join(tmpdir(), "sf-last-green-gate-"));
			const result = { ok: true, exitCode: 0 };
			const wrote = writeLastGreenRecord(dir, result);
			expect(wrote).toBe(false);
			expect(existsSync(join(dir, ".sf", "runtime", "last-green.json"))).toBe(
				false,
			);
			rmSync(dir, { recursive: true, force: true });
		});

		it("multiple writes append newest first and truncate to 50", () => {
			vi.mocked(getExperimentalFlag).mockReturnValue(true);
			const dir = mkdtempSync(join(tmpdir(), "sf-last-green-trunc-"));

			// Write 55 entries (more than MAX_ENTRIES=50).
			for (let i = 0; i < 55; i++) {
				writeLastGreenRecord(
					dir,
					{ ok: true, exitCode: i, output: `output ${i}`, elapsedMs: i * 10 },
					{ milestoneId: "M049", unitId: "M049" },
				);
			}

			const ledger = readLastGreenRecord(dir);
			expect(ledger.length).toBe(50);
			// Newest entry (index 0) should be the last written.
			expect(ledger[0].result.exitCode).toBe(54);
			// Oldest entry (index 49) should be write number 5 (55 - 50 + 4 = 9... wait:
			// writes 0-54 = 55 writes, keeping newest 50 means we keep writes 5-54)
			// Write 5 (index 5) = exitCode 5, so index 49 = write 54.
			expect(ledger[49].result.exitCode).toBe(5);

			rmSync(dir, { recursive: true, force: true });
		});

		it("metadata fields are written correctly", () => {
			vi.mocked(getExperimentalFlag).mockReturnValue(true);
			const dir = mkdtempSync(join(tmpdir(), "sf-last-green-meta-"));
			writeLastGreenRecord(
				dir,
				{ ok: true, exitCode: 0, output: "ok" },
				{
					milestoneId: "M049",
					unitType: "complete-milestone",
					unitId: "M049",
					runId: "run-abc",
					traceId: "trace-xyz",
				},
			);
			const entry = getLastGreenEntry(dir);
			expect(entry.fixture.milestoneId).toBe("M049");
			expect(entry.fixture.unitType).toBe("complete-milestone");
			expect(entry.fixture.unitId).toBe("M049");
			expect(entry.fixture.runId).toBe("run-abc");
			expect(entry.fixture.traceId).toBe("trace-xyz");
			rmSync(dir, { recursive: true, force: true });
		});

		it("missing optional metadata fields default to null", () => {
			vi.mocked(getExperimentalFlag).mockReturnValue(true);
			const dir = mkdtempSync(join(tmpdir(), "sf-last-green-null-meta-"));
			writeLastGreenRecord(dir, { ok: false, exitCode: 1, output: "" });
			const entry = getLastGreenEntry(dir);
			expect(entry.fixture.milestoneId).toBeNull();
			expect(entry.fixture.runId).toBeNull();
			expect(entry.fixture.traceId).toBeNull();
			rmSync(dir, { recursive: true, force: true });
		});
	});

	describe("readLastGreenRecord", () => {
		it("missing file returns null (not throw)", () => {
			const dir = mkdtempSync(join(tmpdir(), "sf-last-green-missing-"));
			const ledger = readLastGreenRecord(dir);
			expect(ledger).toBeNull();
			rmSync(dir, { recursive: true, force: true });
		});

		it("malformed JSON returns null (not throw)", () => {
			const dir = mkdtempSync(join(tmpdir(), "sf-last-green-badjson-"));
			mkdirSync(join(dir, ".sf", "runtime"), { recursive: true });
			writeFileSync(
				join(dir, ".sf", "runtime", "last-green.json"),
				"{ not valid json",
			);
			const ledger = readLastGreenRecord(dir);
			expect(ledger).toBeNull();
			rmSync(dir, { recursive: true, force: true });
		});

		it("valid ledger returns parsed array", () => {
			vi.mocked(getExperimentalFlag).mockReturnValue(true);
			const dir = mkdtempSync(join(tmpdir(), "sf-last-green-valid-"));
			writeLastGreenRecord(
				dir,
				{ ok: true, exitCode: 0, output: "x" },
				{ milestoneId: "M049", unitId: "M049" },
			);
			const ledger = readLastGreenRecord(dir);
			expect(Array.isArray(ledger)).toBe(true);
			expect(ledger.length).toBeGreaterThan(0);
			rmSync(dir, { recursive: true, force: true });
		});
	});

	describe("getLastGreenEntry", () => {
		it("empty ledger returns null", () => {
			const dir = mkdtempSync(join(tmpdir(), "sf-last-green-empty-"));
			const entry = getLastGreenEntry(dir);
			expect(entry).toBeNull();
			rmSync(dir, { recursive: true, force: true });
		});

		it("single entry returns that entry", () => {
			vi.mocked(getExperimentalFlag).mockReturnValue(true);
			const dir = mkdtempSync(join(tmpdir(), "sf-last-green-single-"));
			writeLastGreenRecord(
				dir,
				{ ok: true, exitCode: 0, output: "test" },
				{ milestoneId: "M049", unitId: "M049" },
			);
			const entry = getLastGreenEntry(dir);
			expect(entry).not.toBeNull();
			expect(entry.fixture.milestoneId).toBe("M049");
			rmSync(dir, { recursive: true, force: true });
		});

		it("multiple entries returns newest (index 0)", () => {
			vi.mocked(getExperimentalFlag).mockReturnValue(true);
			const dir = mkdtempSync(join(tmpdir(), "sf-last-green-multi-"));
			writeLastGreenRecord(
				dir,
				{ ok: true, exitCode: 0, output: "first" },
				{ milestoneId: "M049", unitId: "M049" },
			);
			writeLastGreenRecord(
				dir,
				{ ok: true, exitCode: 99, output: "second" },
				{ milestoneId: "M049", unitId: "M049" },
			);
			const entry = getLastGreenEntry(dir);
			expect(entry.result.exitCode).toBe(99); // newest entry
			rmSync(dir, { recursive: true, force: true });
		});
	});

	describe("compareToLastGreen", () => {
		it("identical entries match (all deltas true)", () => {
			const current = makeEntry();
			const last = makeEntry();
			const { match, delta } = compareToLastGreen(current, last);
			expect(match).toBe(true);
			expect(delta.ok_delta).toBe(true);
			expect(delta.exitcode_delta).toBe(true);
			expect(delta.output_delta).toBe(true);
			expect(delta.elapsed_delta).toBe(true);
		});

		it("ok mismatch — match=false", () => {
			const current = makeEntry({ result: { ok: false, exitCode: 1 } });
			const last = makeEntry({ result: { ok: true, exitCode: 0 } });
			const { match, delta } = compareToLastGreen(current, last);
			expect(match).toBe(false);
			expect(delta.ok_delta).toBe(false);
		});

		it("exitCode mismatch — match=false", () => {
			const current = makeEntry({ result: { ok: true, exitCode: 1 } });
			const last = makeEntry({ result: { ok: true, exitCode: 0 } });
			const { match, delta } = compareToLastGreen(current, last);
			expect(match).toBe(false);
			expect(delta.exitcode_delta).toBe(false);
		});

		it("outputLength within ±10% tolerance — match=true", () => {
			// 100 → 110 is exactly 10%, within tolerance.
			const current = makeEntry({
				result: { outputLength: 110, outputPreview: "x".repeat(110) },
			});
			const last = makeEntry({ result: { outputLength: 100 } });
			const { match, delta } = compareToLastGreen(current, last);
			expect(match).toBe(true);
			expect(delta.output_delta).toBe(true);
		});

		it("outputLength outside ±10% tolerance — match=false", () => {
			// 100 → 120 is 20%, outside tolerance.
			const current = makeEntry({
				result: { outputLength: 120, outputPreview: "x".repeat(120) },
			});
			const last = makeEntry({ result: { outputLength: 100 } });
			const { match, delta } = compareToLastGreen(current, last);
			expect(match).toBe(false);
			expect(delta.output_delta).toBe(false);
		});

		it("elapsedMs within ±20% tolerance — match=true", () => {
			// 1000 → 1200 is 20%, within tolerance.
			const current = makeEntry({ result: { elapsedMs: 1200 } });
			const last = makeEntry({ result: { elapsedMs: 1000 } });
			const { match, delta } = compareToLastGreen(current, last);
			expect(match).toBe(true);
			expect(delta.elapsed_delta).toBe(true);
		});

		it("elapsedMs outside ±20% tolerance — match=false", () => {
			// 1000 → 1300 is 30%, outside tolerance.
			const current = makeEntry({ result: { elapsedMs: 1300 } });
			const last = makeEntry({ result: { elapsedMs: 1000 } });
			const { match, delta } = compareToLastGreen(current, last);
			expect(match).toBe(false);
			expect(delta.elapsed_delta).toBe(false);
		});

		it("null current returns match=false", () => {
			const { match } = compareToLastGreen(null, makeEntry());
			expect(match).toBe(false);
		});

		it("null last returns match=false", () => {
			const { match } = compareToLastGreen(makeEntry(), null);
			expect(match).toBe(false);
		});

		it("both null returns match=false", () => {
			const { match } = compareToLastGreen(null, null);
			expect(match).toBe(false);
		});

		it("outputPreview prefix comparison — first 100 chars only", () => {
			const longStr = "a".repeat(200);
			const current = makeEntry({ result: { outputLength: 200, outputPreview: longStr } });
			const last = makeEntry({
				result: { outputLength: 200, outputPreview: "a".repeat(200) },
			});
			// The compare function only checks outputLength, not preview content,
			// so this should match.
			const { match } = compareToLastGreen(current, last);
			expect(match).toBe(true);
		});
	});

	describe("clearLastGreen", () => {
		it("deletes the ledger file and returns true", () => {
			vi.mocked(getExperimentalFlag).mockReturnValue(true);
			const dir = mkdtempSync(join(tmpdir(), "sf-last-green-clear-"));
			writeLastGreenRecord(
				dir,
				{ ok: true, exitCode: 0, output: "x" },
				{ milestoneId: "M049", unitId: "M049" },
			);
			expect(existsSync(join(dir, ".sf", "runtime", "last-green.json"))).toBe(true);
			const cleared = clearLastGreen(dir);
			expect(cleared).toBe(true);
			expect(existsSync(join(dir, ".sf", "runtime", "last-green.json"))).toBe(false);
			rmSync(dir, { recursive: true, force: true });
		});

		it("file not present returns true (idempotent)", () => {
			const dir = mkdtempSync(join(tmpdir(), "sf-last-green-noop-"));
			const cleared = clearLastGreen(dir);
			expect(cleared).toBe(true);
			rmSync(dir, { recursive: true, force: true });
		});
	});
});