/**
 * update-task-status-revert-safety.test.mjs — Layer B revert guard.
 *
 * Purpose: prove that updateTaskStatus refuses to revert a task whose
 * (milestone, slice, task) triple appears in iterations.jsonl with
 * outcome=complete within SF_REVERT_BLOCK_WINDOW_MS (default 30 min).
 *
 * Consumer: safety harness that prevented T02 from being clobbered by a
 * "pending" revert after iterations.jsonl already recorded completion.
 */

import assert from "node:assert/strict";
import {
	existsSync,
	mkdirSync,
	mkdtempSync,
	readFileSync,
	rmSync,
	writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, test } from "vitest";
import {
	closeDatabase,
	insertMilestone,
	insertSlice,
	insertTask,
	openDatabase,
	updateTaskStatus,
} from "../sf-db.js";

const tmpDirs = [];

afterEach(() => {
	closeDatabase();
	delete process.env.SF_REVERT_BLOCK_WINDOW_MS;
	while (tmpDirs.length > 0) {
		const dir = tmpDirs.pop();
		if (dir) rmSync(dir, { recursive: true, force: true });
	}
});

/**
 * Create a minimal project with a DB, optional iterations.jsonl, and
 * a task seeded in "complete" status.
 */
function makeProject({ iterLines = null } = {}) {
	const dir = mkdtempSync(join(tmpdir(), "sf-revert-safety-"));
	tmpDirs.push(dir);

	// Create required dirs
	mkdirSync(join(dir, ".sf", "runtime", "autonomous-solver"), {
		recursive: true,
	});

	// Write iterations.jsonl if provided
	if (iterLines !== null) {
		writeFileSync(
			join(dir, ".sf", "runtime", "autonomous-solver", "iterations.jsonl"),
			iterLines,
			"utf-8",
		);
	}

	// Open DB and seed data
	openDatabase(join(dir, ".sf", "sf.db"));
	insertMilestone({ id: "M001", title: "Test milestone", status: "active" });
	insertSlice({
		milestoneId: "M001",
		id: "S01",
		title: "Test slice",
		status: "pending",
	});
	insertTask({
		milestoneId: "M001",
		sliceId: "S01",
		id: "T02",
		title: "Test task",
		status: "complete",
	});

	return dir;
}

function makeIterLine(unitId, outcome, ts) {
	return JSON.stringify({ schemaVersion: 1, unitId, outcome, ts }) + "\n";
}

// ── Test 1: Revert to pending with NO iter entry → succeeds ──────────────────
test("updateTaskStatus_revert_to_pending_with_no_iter_entry_succeeds", () => {
	const dir = makeProject({ iterLines: null });
	// Should not throw
	updateTaskStatus("M001", "S01", "T02", "pending", null, undefined, {
		basePath: dir,
	});
});

// ── Test 2: Revert with iter outcome=complete but older than window → succeeds
test("updateTaskStatus_revert_with_stale_complete_iter_entry_succeeds", () => {
	// 1h old — well outside the 30min window
	const oldTs = new Date(Date.now() - 60 * 60 * 1000).toISOString();
	const iterLines = makeIterLine("M001/S01/T02", "complete", oldTs);
	const dir = makeProject({ iterLines });
	// Should not throw
	updateTaskStatus("M001", "S01", "T02", "pending", null, undefined, {
		basePath: dir,
	});
});

// ── Test 3: Revert with iter outcome=complete 5min ago → BLOCKED ─────────────
test("updateTaskStatus_revert_with_recent_complete_iter_entry_is_blocked", () => {
	const recentTs = new Date(Date.now() - 5 * 60 * 1000).toISOString(); // 5min ago
	const iterLines = makeIterLine("M001/S01/T02", "complete", recentTs);
	const dir = makeProject({ iterLines });

	assert.throws(
		() =>
			updateTaskStatus("M001", "S01", "T02", "pending", null, undefined, {
				basePath: dir,
			}),
		(err) => {
			assert.ok(
				err.message.includes("revert-blocked-by-iter-truth"),
				`Expected revert-blocked-by-iter-truth in: ${err.message}`,
			);
			return true;
		},
	);

	// Self-feedback entry must have been appended
	const sfJsonlPath = join(dir, ".sf", "self-feedback.jsonl");
	assert.ok(existsSync(sfJsonlPath), "self-feedback.jsonl should be created");
	const content = readFileSync(sfJsonlPath, "utf-8");
	const entry = JSON.parse(content.trim().split("\n")[0]);
	assert.equal(entry.kind, "revert-blocked-by-iter-truth");
	assert.equal(entry.severity, "medium");
	assert.equal(entry.unitId, "M001/S01/T02");
	assert.equal(entry.attemptedRevert, "pending");
	assert.equal(entry.iterCompleteTs, recentTs);
});

// ── Test 4: SF_REVERT_BLOCK_WINDOW_MS=0 disables check → succeeds ────────────
test("updateTaskStatus_revert_with_window_ms_zero_env_succeeds", () => {
	process.env.SF_REVERT_BLOCK_WINDOW_MS = "0";
	const recentTs = new Date(Date.now() - 5 * 60 * 1000).toISOString();
	const iterLines = makeIterLine("M001/S01/T02", "complete", recentTs);
	const dir = makeProject({ iterLines });
	// Should not throw (window disabled)
	updateTaskStatus("M001", "S01", "T02", "pending", null, undefined, {
		basePath: dir,
	});
});

// ── Test 5: Iter outcome=continue (not complete) → revert succeeds ────────────
test("updateTaskStatus_revert_with_continue_iter_outcome_succeeds", () => {
	const recentTs = new Date(Date.now() - 5 * 60 * 1000).toISOString();
	const iterLines = makeIterLine("M001/S01/T02", "continue", recentTs);
	const dir = makeProject({ iterLines });
	// Should not throw — outcome is not "complete"
	updateTaskStatus("M001", "S01", "T02", "pending", null, undefined, {
		basePath: dir,
	});
});

// ── Test 6: newStatus = "in_progress" → never blocked ────────────────────────
test("updateTaskStatus_in_progress_never_blocked_even_with_recent_complete_iter", () => {
	const recentTs = new Date(Date.now() - 5 * 60 * 1000).toISOString();
	const iterLines = makeIterLine("M001/S01/T02", "complete", recentTs);
	const dir = makeProject({ iterLines });
	// "in_progress" is explicitly excluded from the revert guard
	updateTaskStatus("M001", "S01", "T02", "in_progress", null, undefined, {
		basePath: dir,
	});
});

// ── Test 7: newStatus = "complete" → never blocked (forward transition) ───────
test("updateTaskStatus_complete_forward_transition_never_blocked", () => {
	const recentTs = new Date(Date.now() - 5 * 60 * 1000).toISOString();
	const iterLines = makeIterLine("M001/S01/T02", "complete", recentTs);
	// Seed the task as pending so a "complete" update makes sense
	const dir = makeProject({ iterLines });
	// updateTaskStatus to "complete" should never be blocked
	updateTaskStatus(
		"M001",
		"S01",
		"T02",
		"complete",
		new Date().toISOString(),
		undefined,
		{
			basePath: dir,
		},
	);
});
