/**
 * uok-scheduler-v2.test.mjs — ExecutionGraphSchedulerV2 contract tests.
 *
 * Purpose: verify cancellation tokens, progress streaming, worker pool
 * enforcement, and task state integration.
 */

import assert from "node:assert/strict";
import { afterEach, test } from "vitest";
import { closeDatabase, getDatabase, openDatabase } from "../sf-db.js";
import {
	getGraphProgressStream,
	getGraphStateSummary,
	queryTasksByState,
} from "../uok/execution-graph-persist.js";
import {
	CancellationToken,
	ExecutionGraphSchedulerV2,
	ProgressStream,
	WorkerPool,
} from "../uok/scheduler.js";

afterEach(() => {
	closeDatabase();
});

// ─── CancellationToken ─────────────────────────────────────────────────────

test("token_initially_not_cancelled", () => {
	const token = new CancellationToken();
	assert.equal(token.isCancelled, false);
	assert.equal(token.reason, null);
});

test("token_cancel_sets_state", () => {
	const token = new CancellationToken();
	token.cancel("timeout");
	assert.equal(token.isCancelled, true);
	assert.equal(token.reason, "timeout");
});

test("token_cancel_idempotent", () => {
	const token = new CancellationToken();
	token.cancel("first");
	token.cancel("second");
	assert.equal(token.reason, "first");
});

test("token_throwIfCancelled_throws_when_cancelled", () => {
	const token = new CancellationToken();
	token.cancel("abort");
	assert.throws(() => token.throwIfCancelled(), /abort/);
});

test("token_throwIfCancelled_does_not_throw_when_active", () => {
	const token = new CancellationToken();
	assert.doesNotThrow(() => token.throwIfCancelled());
});

test("token_onCancel_fires_immediately_if_already_cancelled", () => {
	const token = new CancellationToken();
	token.cancel("done");
	let fired = false;
	token.onCancel(() => {
		fired = true;
	});
	assert.equal(fired, true);
});

test("token_onCancel_fires_when_cancelled_later", () => {
	const token = new CancellationToken();
	let fired = false;
	token.onCancel(() => {
		fired = true;
	});
	assert.equal(fired, false);
	token.cancel("later");
	assert.equal(fired, true);
});

test("token_onCancel_unsubscribe_works", () => {
	const token = new CancellationToken();
	let fired = false;
	const unsub = token.onCancel(() => {
		fired = true;
	});
	unsub();
	token.cancel("nope");
	assert.equal(fired, false);
});

// ─── ProgressStream ────────────────────────────────────────────────────────

test("stream_emit_delivers_to_listener", () => {
	const stream = new ProgressStream();
	const events = [];
	stream.onProgress((ev) => events.push(ev));
	stream.emit({ type: "test", data: 1 });
	assert.equal(events.length, 1);
	assert.equal(events[0].type, "test");
	assert.ok(events[0].ts);
});

test("stream_replays_history_to_new_listener", () => {
	const stream = new ProgressStream();
	stream.emit({ type: "first" });
	stream.emit({ type: "second" });
	const events = [];
	stream.onProgress((ev) => events.push(ev));
	assert.equal(events.length, 2);
	assert.equal(events[0].type, "first");
	assert.equal(events[1].type, "second");
});

test("stream_history_capped_at_max", () => {
	const stream = new ProgressStream();
	// Private maxHistory = 100; emit 105 events
	for (let i = 0; i < 105; i++) {
		stream.emit({ type: `ev-${i}` });
	}
	const events = [];
	stream.onProgress((ev) => events.push(ev));
	assert.equal(events.length, 100);
	assert.equal(events[0].type, "ev-5");
});

// ─── WorkerPool ────────────────────────────────────────────────────────────

test("pool_enforces_max_workers", async () => {
	const pool = new WorkerPool(2);
	const token1 = new CancellationToken();
	const token2 = new CancellationToken();
	const token3 = new CancellationToken();

	const w1 = await pool.acquire("node-1", token1);
	const w2 = await pool.acquire("node-2", token2);

	// Third acquire should queue, not resolve immediately
	let w3Resolved = false;
	const w3Promise = pool.acquire("node-3", token3).then((w) => {
		w3Resolved = true;
		return w;
	});

	assert.equal(pool.activeCount, 2);
	assert.equal(pool.queuedCount, 1);
	assert.equal(w3Resolved, false);

	w1.release();
	const w3 = await w3Promise;
	assert.equal(w3Resolved, true);
	assert.equal(pool.activeCount, 2);

	w2.release();
	w3.release();
	assert.equal(pool.activeCount, 0);
});

test("pool_cancel_queued_rejects_queued_acquire", async () => {
	const pool = new WorkerPool(1);
	const token1 = new CancellationToken();
	const token2 = new CancellationToken();

	const w1 = await pool.acquire("node-1", token1);
	const acquire2 = pool.acquire("node-2", token2);

	pool.cancelAll("drain");
	await assert.rejects(acquire2, /drain/);
	w1.release();
});

test("pool_getActive_returns_current_workers", async () => {
	const pool = new WorkerPool(2);
	const token = new CancellationToken();
	const w = await pool.acquire("node-a", token);
	const active = pool.getActive();
	assert.equal(active.length, 1);
	assert.equal(active[0].nodeId, "node-a");
	assert.ok(active[0].durationMs >= 0);
	w.release();
});

// ─── ExecutionGraphSchedulerV2 — Serial ────────────────────────────────────

test("scheduler_serial_runs_all_nodes_in_order", async () => {
	const scheduler = new ExecutionGraphSchedulerV2();
	const order = [];
	scheduler.registerHandler("unit", async (node) => {
		order.push(node.id);
		return { gateResults: [{ outcome: "pass", gateId: "test" }] };
	});

	const nodes = [
		{ id: "a", kind: "unit", dependsOn: [], metadata: {} },
		{ id: "b", kind: "unit", dependsOn: ["a"], metadata: {} },
		{ id: "c", kind: "unit", dependsOn: ["b"], metadata: {} },
	];

	const result = await scheduler.run(nodes, { parallel: false });
	assert.deepEqual(order, ["a", "b", "c"]);
	assert.deepEqual(result.order, ["a", "b", "c"]);
});

test("scheduler_serial_respects_dependencies", async () => {
	const scheduler = new ExecutionGraphSchedulerV2();
	const order = [];
	scheduler.registerHandler("unit", async (node) => {
		order.push(node.id);
		return { gateResults: [{ outcome: "pass", gateId: "test" }] };
	});

	const nodes = [
		{ id: "a", kind: "unit", dependsOn: [], metadata: {} },
		{ id: "b", kind: "unit", dependsOn: [], metadata: {} },
		{ id: "c", kind: "unit", dependsOn: ["a", "b"], metadata: {} },
	];

	await scheduler.run(nodes, { parallel: false });
	assert.ok(order.indexOf("c") > order.indexOf("a"));
	assert.ok(order.indexOf("c") > order.indexOf("b"));
});

test("scheduler_serial_cancellation_stops_execution", async () => {
	const scheduler = new ExecutionGraphSchedulerV2();
	const order = [];
	const globalToken = new CancellationToken();
	scheduler.registerHandler("unit", async (node, _token) => {
		if (node.id === "b") globalToken.cancel("stop");
		globalToken.throwIfCancelled();
		order.push(node.id);
		return { gateResults: [{ outcome: "pass", gateId: "test" }] };
	});

	const nodes = [
		{ id: "a", kind: "unit", dependsOn: [], metadata: {} },
		{ id: "b", kind: "unit", dependsOn: ["a"], metadata: {} },
		{ id: "c", kind: "unit", dependsOn: ["b"], metadata: {} },
	];

	await assert.rejects(
		scheduler.run(nodes, { parallel: false, globalToken }),
		/stop/,
	);
	assert.deepEqual(order, ["a"]);
});

// ─── ExecutionGraphSchedulerV2 — Parallel ──────────────────────────────────

test("scheduler_parallel_runs_independent_nodes_concurrently", async () => {
	const scheduler = new ExecutionGraphSchedulerV2();
	const starts = [];
	scheduler.registerHandler("unit", async (node, _token, _progress) => {
		starts.push(node.id);
		await new Promise((r) => setTimeout(r, 50));
		return { gateResults: [{ outcome: "pass", gateId: "test" }] };
	});

	const nodes = [
		{ id: "a", kind: "unit", dependsOn: [], metadata: {} },
		{ id: "b", kind: "unit", dependsOn: [], metadata: {} },
	];

	const result = await scheduler.run(nodes, { parallel: true, maxWorkers: 2 });
	assert.equal(result.order.length, 2);
	assert.ok(result.order.includes("a"));
	assert.ok(result.order.includes("b"));
});

test("scheduler_parallel_enforces_max_workers", async () => {
	const scheduler = new ExecutionGraphSchedulerV2();
	let concurrent = 0;
	let maxConcurrent = 0;
	scheduler.registerHandler("unit", async (_node) => {
		concurrent++;
		maxConcurrent = Math.max(maxConcurrent, concurrent);
		await new Promise((r) => setTimeout(r, 50));
		concurrent--;
		return { gateResults: [{ outcome: "pass", gateId: "test" }] };
	});

	const nodes = [
		{ id: "a", kind: "unit", dependsOn: [], metadata: {} },
		{ id: "b", kind: "unit", dependsOn: [], metadata: {} },
		{ id: "c", kind: "unit", dependsOn: [], metadata: {} },
		{ id: "d", kind: "unit", dependsOn: [], metadata: {} },
	];

	await scheduler.run(nodes, { parallel: true, maxWorkers: 2 });
	assert.equal(maxConcurrent, 2);
});

test("scheduler_parallel_reports_progress_events", async () => {
	const scheduler = new ExecutionGraphSchedulerV2();
	scheduler.registerHandler("unit", async (_node) => {
		return { gateResults: [{ outcome: "pass", gateId: "test" }] };
	});

	const nodes = [{ id: "a", kind: "unit", dependsOn: [], metadata: {} }];

	const events = [];
	scheduler.progress.onProgress((ev) => events.push(ev));
	await scheduler.run(nodes, { parallel: true, maxWorkers: 1 });

	const startEvent = events.find((e) => e.type === "node-start");
	const completeEvent = events.find((e) => e.type === "node-complete");
	assert.ok(startEvent);
	assert.ok(completeEvent);
	assert.equal(completeEvent.nodeId, "a");
	assert.equal(completeEvent.state, "done");
});

test("scheduler_parallel_task_records_have_correct_state", async () => {
	const scheduler = new ExecutionGraphSchedulerV2();
	scheduler.registerHandler("unit", async (_node) => {
		return {
			gateResults: [
				{ outcome: "pass", gateId: "security" },
				{ outcome: "pass", gateId: "cost" },
			],
		};
	});

	const nodes = [
		{ id: "task-1", kind: "unit", dependsOn: [], metadata: { unitId: "U1" } },
	];

	await scheduler.run(nodes, { parallel: true, maxWorkers: 1 });
	const task = scheduler.getTask("task-1");
	assert.equal(task.state, "done");
	assert.equal(task.unitId, "U1");
	assert.ok(task.startedAt);
	assert.ok(task.endedAt);
});

test("scheduler_parallel_detects_cyclic_dependencies", async () => {
	const scheduler = new ExecutionGraphSchedulerV2();
	scheduler.registerHandler("unit", async (_node) => ({
		gateResults: [{ outcome: "pass", gateId: "test" }],
	}));

	const nodes = [
		{ id: "a", kind: "unit", dependsOn: ["b"], metadata: {} },
		{ id: "b", kind: "unit", dependsOn: ["a"], metadata: {} },
	];

	await assert.rejects(scheduler.run(nodes, { parallel: true }), /cyclic/);
});

test("scheduler_parallel_detects_deadlock", async () => {
	const scheduler = new ExecutionGraphSchedulerV2();
	scheduler.registerHandler("unit", async (_node) => ({
		gateResults: [{ outcome: "pass", gateId: "test" }],
	}));

	// a depends on b, b depends on a — cyclic, but also no nodes with in-degree 0
	const nodes = [
		{ id: "a", kind: "unit", dependsOn: ["b"], metadata: {} },
		{ id: "b", kind: "unit", dependsOn: ["a"], metadata: {} },
	];

	await assert.rejects(scheduler.run(nodes, { parallel: true }), /cyclic/);
});

// ─── Progress stream wiring ────────────────────────────────────────────────

test("scheduler_wires_progress_to_parent_stream", async () => {
	const parent = new ProgressStream();
	const scheduler = new ExecutionGraphSchedulerV2();
	scheduler.registerHandler("unit", async () => ({
		gateResults: [{ outcome: "pass", gateId: "test" }],
	}));

	const parentEvents = [];
	parent.onProgress((ev) => parentEvents.push(ev));

	const nodes = [{ id: "x", kind: "unit", dependsOn: [], metadata: {} }];

	await scheduler.run(nodes, { parallel: false, parentStream: parent });
	assert.ok(parentEvents.some((e) => e.type === "node-complete"));
});

// ─── SQLite persistence ───────────────────────────────────────────────────

test("scheduler_when_db_given_persists_graph_tasks_and_progress", async () => {
	assert.equal(openDatabase(":memory:"), true);
	const db = getDatabase();
	const scheduler = new ExecutionGraphSchedulerV2();
	scheduler.registerHandler("unit", async () => ({
		gateResults: [{ outcome: "pass", gateId: "test" }],
	}));

	const nodes = [
		{
			id: "task-1",
			kind: "unit",
			dependsOn: [],
			writes: ["src/a.js"],
			metadata: {
				unitType: "execute-task",
				unitId: "M001/S01/T01",
				milestoneId: "M001",
				sliceId: "S01",
				taskId: "T01",
				title: "Implement A",
			},
		},
	];

	await scheduler.run(nodes, {
		parallel: false,
		db,
		graphId: "graph-persisted",
		graphMeta: { milestoneId: "M001", phase: "executing" },
	});

	const tasks = queryTasksByState(db, { graphId: "graph-persisted" });
	assert.equal(tasks.length, 1);
	assert.equal(tasks[0].id, "task-1");
	assert.equal(tasks[0].status, "done");
	assert.deepEqual(tasks[0].writes, ["src/a.js"]);

	const summary = getGraphStateSummary(db, "graph-persisted");
	assert.equal(summary.total, 1);
	assert.equal(summary.progress, 100);
	assert.equal(summary.isComplete, true);

	const events = getGraphProgressStream(db, "graph-persisted", 10);
	assert.ok(events.some((e) => e.type === "node-start"));
	assert.ok(events.some((e) => e.type === "node-complete"));
});
