/**
 * Bootstrap + workflow HIGH-severity bug fix contract tests.
 *
 * Purpose: prevent regression on the three HIGH bugs in the bootstrap+workflow
 * cluster: advisory-lock scope, silent corruption threshold, and milestone guard.
 *
 * Consumer: CI gate via `npx vitest run bootstrap-workflow-high`.
 */

import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test, vi } from "vitest";

// ─── Hoisted mocks ─────────────────────────────────────────────────────────

const dbMock = vi.hoisted(() => ({
	milestone: null,
	slices: [],
	tasks: [],
}));

const lockMock = vi.hoisted(() => ({
	acquired: true,
	acquireCallCount: 0,
	releaseCallCount: 0,
}));

vi.mock("../sf-db.js", () => ({
	getMilestone: vi.fn((id) =>
		dbMock.milestone?.id === id ? dbMock.milestone : null,
	),
	getMilestoneSlices: vi.fn(() => dbMock.slices),
	getSliceTasks: vi.fn((_mId, sId) =>
		dbMock.tasks.filter((t) => t.slice_id === sId),
	),
	updateSliceStatus: vi.fn(),
	updateMilestoneStatus: vi.fn(),
	updateTaskStatus: vi.fn(),
	insertVerificationEvidence: vi.fn(),
	insertMilestone: vi.fn(),
	insertOrIgnoreSlice: vi.fn(),
	insertOrIgnoreTask: vi.fn(),
	setTaskBlockerDiscovered: vi.fn(),
	upsertDecision: vi.fn(),
	openDatabase: vi.fn(),
	transaction: vi.fn((fn) => fn()),
}));

vi.mock("../sync-lock.js", () => ({
	acquireSyncLock: vi.fn((_basePath) => {
		lockMock.acquireCallCount++;
		return { acquired: lockMock.acquired };
	}),
	releaseSyncLock: vi.fn((_basePath) => {
		lockMock.releaseCallCount++;
	}),
}));

vi.mock("../workflow-logger.js", () => ({
	logWarning: vi.fn(),
	logError: vi.fn(),
}));

vi.mock("../workflow-manifest.js", () => ({
	writeManifest: vi.fn(),
}));

vi.mock("../state.js", () => ({
	invalidateStateCache: vi.fn(),
}));

vi.mock("../paths.js", () => ({
	clearPathCache: vi.fn(),
	sfRuntimeRoot: vi.fn((base) => join(base, ".sf")),
}));

vi.mock("../files.js", () => ({
	clearParseCache: vi.fn(),
}));

// ─── Helpers ───────────────────────────────────────────────────────────────

function makeTempDir(prefix) {
	return mkdtempSync(join(tmpdir(), prefix));
}

function cleanup(dir) {
	try {
		rmSync(dir, { recursive: true, force: true });
	} catch {}
}

function resetMocks() {
	dbMock.milestone = null;
	dbMock.slices = [];
	dbMock.tasks = [];
	lockMock.acquired = true;
	lockMock.acquireCallCount = 0;
	lockMock.releaseCallCount = 0;
	vi.clearAllMocks();
}

// ─── Bug 1: Advisory lock scope ────────────────────────────────────────────

describe("reconcileWorktreeLogs lock scope", () => {
	test("acquires lock BEFORE first readEvents call", async () => {
		resetMocks();
		const mainBase = makeTempDir("sf-reconcile-main-");
		const wtBase = makeTempDir("sf-reconcile-wt-");
		mkdirSync(join(mainBase, ".sf"), { recursive: true });
		mkdirSync(join(wtBase, ".sf"), { recursive: true });
		writeFileSync(join(mainBase, ".sf", "event-log.jsonl"), "\n");
		writeFileSync(join(wtBase, ".sf", "event-log.jsonl"), "\n");

		const { reconcileWorktreeLogs } = await import("../workflow-reconcile.js");
		reconcileWorktreeLogs(mainBase, wtBase);

		expect(lockMock.acquireCallCount).toBe(1);
		expect(lockMock.releaseCallCount).toBe(1);

		cleanup(mainBase);
		cleanup(wtBase);
	});

	test("returns early with warning when lock cannot be acquired", async () => {
		resetMocks();
		lockMock.acquired = false;
		const mainBase = makeTempDir("sf-reconcile-main-");
		const wtBase = makeTempDir("sf-reconcile-wt-");

		const { reconcileWorktreeLogs } = await import("../workflow-reconcile.js");
		const { logWarning } = await import("../workflow-logger.js");
		const result = reconcileWorktreeLogs(mainBase, wtBase);

		expect(result.autoMerged).toBe(0);
		expect(result.conflicts).toEqual([]);
		expect(logWarning).toHaveBeenCalledWith(
			"reconcile",
			expect.stringContaining("could not acquire sync lock"),
		);

		cleanup(mainBase);
		cleanup(wtBase);
	});
});

// ─── Bug 2: Corruption threshold ───────────────────────────────────────────

describe("readEvents corruption threshold", () => {
	test("warns when corruption ratio reaches 1%", async () => {
		resetMocks();
		const dir = makeTempDir("sf-events-");
		const lines = [];
		// 100 lines total, 1 corrupt = 1%
		for (let i = 0; i < 99; i++) {
			lines.push(
				JSON.stringify({
					cmd: "test",
					params: { i },
					ts: new Date().toISOString(),
				}),
			);
		}
		lines.push("this is not json");
		writeFileSync(join(dir, "event-log.jsonl"), lines.join("\n") + "\n");

		const { readEvents } = await import("../workflow-events.js");
		const { logWarning } = await import("../workflow-logger.js");
		const events = readEvents(join(dir, "event-log.jsonl"));

		expect(events.length).toBe(99);
		expect(logWarning).toHaveBeenCalledWith(
			"event-log",
			expect.stringContaining("1.0%"),
		);

		cleanup(dir);
	});

	test("throws when corruption ratio reaches 10%", async () => {
		resetMocks();
		const dir = makeTempDir("sf-events-");
		const lines = [];
		// 100 lines total, 10 corrupt = 10%
		for (let i = 0; i < 90; i++) {
			lines.push(
				JSON.stringify({
					cmd: "test",
					params: { i },
					ts: new Date().toISOString(),
				}),
			);
		}
		for (let i = 0; i < 10; i++) {
			lines.push("corrupted line " + i);
		}
		writeFileSync(join(dir, "event-log.jsonl"), lines.join("\n") + "\n");

		const { readEvents } = await import("../workflow-events.js");
		expect(() => readEvents(join(dir, "event-log.jsonl"))).toThrow(
			"exceeds fatal threshold",
		);

		cleanup(dir);
	});

	test("silent when corruption is below 1%", async () => {
		resetMocks();
		const dir = makeTempDir("sf-events-");
		const lines = [];
		// 200 lines total, 1 corrupt = 0.5%
		for (let i = 0; i < 199; i++) {
			lines.push(
				JSON.stringify({
					cmd: "test",
					params: { i },
					ts: new Date().toISOString(),
				}),
			);
		}
		lines.push("bad json");
		writeFileSync(join(dir, "event-log.jsonl"), lines.join("\n") + "\n");

		const { readEvents } = await import("../workflow-events.js");
		const { logWarning } = await import("../workflow-logger.js");
		const events = readEvents(join(dir, "event-log.jsonl"));

		expect(events.length).toBe(199);
		// logWarning is called once per corrupted line, but NOT for the threshold
		expect(logWarning).toHaveBeenCalledTimes(1);
		expect(logWarning).toHaveBeenCalledWith(
			"event-log",
			expect.stringContaining("skipping corrupted event"),
		);

		cleanup(dir);
	});
});

// ─── Bug 3: replaySliceComplete milestone guard ────────────────────────────

describe("replaySliceComplete milestone guard", () => {
	test("skips when milestone is already complete", async () => {
		resetMocks();
		dbMock.milestone = {
			id: "M001",
			status: "complete",
			depends_on: [],
		};
		dbMock.tasks = [
			{ id: "T01", slice_id: "S01", status: "done" },
			{ id: "T02", slice_id: "S01", status: "done" },
		];

		const { replaySliceComplete } = await import("../workflow-reconcile.js");
		const { updateSliceStatus } = await import("../sf-db.js");
		replaySliceComplete("M001", "S01", new Date().toISOString());

		expect(updateSliceStatus).not.toHaveBeenCalled();
	});

	test("skips when depends_on milestones are incomplete", async () => {
		resetMocks();
		dbMock.milestone = {
			id: "M002",
			status: "active",
			depends_on: ["M001"],
		};
		// M001 is not in dbMock.milestone, so it's treated as incomplete
		dbMock.tasks = [{ id: "T01", slice_id: "S01", status: "done" }];

		const { replaySliceComplete } = await import("../workflow-reconcile.js");
		const { updateSliceStatus } = await import("../sf-db.js");
		replaySliceComplete("M002", "S01", new Date().toISOString());

		expect(updateSliceStatus).not.toHaveBeenCalled();
	});

	test("skips when tasks are still pending", async () => {
		resetMocks();
		dbMock.milestone = {
			id: "M001",
			status: "active",
			depends_on: [],
		};
		dbMock.tasks = [
			{ id: "T01", slice_id: "S01", status: "done" },
			{ id: "T02", slice_id: "S01", status: "in-progress" },
		];

		const { replaySliceComplete } = await import("../workflow-reconcile.js");
		const { updateSliceStatus } = await import("../sf-db.js");
		replaySliceComplete("M001", "S01", new Date().toISOString());

		expect(updateSliceStatus).not.toHaveBeenCalled();
	});

	test("marks slice done when all guards pass", async () => {
		resetMocks();
		dbMock.milestone = {
			id: "M001",
			status: "active",
			depends_on: [],
		};
		dbMock.tasks = [
			{ id: "T01", slice_id: "S01", status: "done" },
			{ id: "T02", slice_id: "S01", status: "skipped" },
		];

		const { replaySliceComplete } = await import("../workflow-reconcile.js");
		const { updateSliceStatus } = await import("../sf-db.js");
		const ts = new Date().toISOString();
		replaySliceComplete("M001", "S01", ts);

		expect(updateSliceStatus).toHaveBeenCalledWith("M001", "S01", "done", ts);
	});

	test("allows completion when milestone has no tasks (empty slice)", async () => {
		resetMocks();
		dbMock.milestone = {
			id: "M001",
			status: "active",
			depends_on: [],
		};
		dbMock.tasks = [];

		const { replaySliceComplete } = await import("../workflow-reconcile.js");
		const { updateSliceStatus } = await import("../sf-db.js");
		const ts = new Date().toISOString();
		replaySliceComplete("M001", "S01", ts);

		expect(updateSliceStatus).toHaveBeenCalledWith("M001", "S01", "done", ts);
	});
});
