/**
 * Schedule Autonomous Dispatch Rule tests.
 *
 * Purpose: verify the schedule dispatch rule in auto-dispatch.js:
 * - consumes repo-scoped due autonomous_dispatch commands between autonomous units
 * - returns null when no matching due entries
 * - dispatches due autonomous_dispatch prompts as custom-step units
 * - non-fatal on errors
 *
 * Consumer: CI test runner (vitest).
 */
import assert from "node:assert/strict";
import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, it } from "vitest";
import { createScheduleStore } from "../schedule/schedule-store.js";
import { generateULID } from "../schedule/schedule-ulid.js";
import { DISPATCH_RULES } from "../uok/auto-dispatch.js";

describe("schedule-dispatch", () => {
	let testDir;
	let originalCwd;

	beforeEach(() => {
		testDir = join(
			tmpdir(),
			`sf-dispatch-test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
		);
		mkdirSync(testDir, { recursive: true });
		originalCwd = process.cwd();
		process.chdir(testDir);
	});

	afterEach(() => {
		process.chdir(originalCwd);
		try {
			rmSync(testDir, { recursive: true });
		} catch {
			// ignore
		}
	});

	function makeEntry(overrides = {}) {
		const now = new Date().toISOString();
		return {
			id: generateULID(),
			kind: "reminder",
			status: "pending",
			due_at: now,
			created_at: now,
			payload: { message: "test reminder" },
			created_by: "user",
			...overrides,
		};
	}

	async function runScheduleRule(state, basePath = testDir) {
		const rule = DISPATCH_RULES.find(
			(r) => r.name === "schedule autonomous dispatch",
		);
		assert.ok(rule, "schedule dispatch rule not found");
		return rule.match({ state, basePath });
	}

	it("returns null when no due entries exist", async () => {
		const state = { activeMilestone: null, phase: "idle" };
		const result = await runScheduleRule(state);
		assert.equal(result, null);
	});

	it("executes due autonomous_dispatch command from the project root", async () => {
		const store = createScheduleStore(testDir);
		store.appendEntry(
			"project",
			makeEntry({
				kind: "command",
				due_at: new Date(Date.now() - 60 * 60 * 1000).toISOString(),
				autonomous_dispatch: true,
				payload: {
					command:
						"node -e \"require('fs').writeFileSync('cron-ran.txt', process.cwd())\"",
				},
			}),
		);

		const state = {
			activeMilestone: { id: "M010" },
			phase: "executing",
		};
		const result = await runScheduleRule(state);
		assert.ok(result);
		assert.equal(result.action, "skip");
		assert.equal(existsSync(join(testDir, "cron-ran.txt")), true);
		assert.equal(readFileSync(join(testDir, "cron-ran.txt"), "utf-8"), testDir);
		const entries = store.readEntries("project");
		assert.equal(entries[0].status, "done");
		assert.equal(entries[0].payload.result_note, "command completed");
	});

	it("returns null when due entries have autonomous_dispatch=false", async () => {
		const store = createScheduleStore(testDir);
		store.appendEntry(
			"project",
			makeEntry({
				kind: "command",
				due_at: new Date(Date.now() - 60 * 60 * 1000).toISOString(),
				autonomous_dispatch: false,
				payload: {
					command:
						"node -e \"require('fs').writeFileSync('should-not-run.txt', 'bad')\"",
				},
			}),
		);

		const state = { activeMilestone: null, phase: "idle" };
		const result = await runScheduleRule(state);
		assert.equal(result, null);
		assert.equal(existsSync(join(testDir, "should-not-run.txt")), false);
	});

	it("returns null when due entries are passive reminders", async () => {
		const store = createScheduleStore(testDir);
		store.appendEntry(
			"project",
			makeEntry({
				due_at: new Date(Date.now() - 60 * 60 * 1000).toISOString(),
				autonomous_dispatch: true,
				kind: "milestone_check",
			}),
		);

		const state = { activeMilestone: null, phase: "idle" };
		const result = await runScheduleRule(state);
		assert.equal(result, null);
	});

	it("pauses when due autonomous_dispatch command fails and records evidence", async () => {
		const store = createScheduleStore(testDir);
		store.appendEntry(
			"project",
			makeEntry({
				kind: "command",
				due_at: new Date(Date.now() - 60 * 60 * 1000).toISOString(),
				autonomous_dispatch: true,
				payload: {
					command: "node -e \"process.stderr.write('boom'); process.exit(9)\"",
				},
			}),
		);

		const state = { activeMilestone: null, phase: "idle" };
		const result = await runScheduleRule(state);
		assert.ok(result);
		assert.equal(result.action, "stop");
		assert.equal(result.level, "warning");
		assert.ok(result.reason.includes("failed"));
		const entries = store.readEntries("project");
		assert.equal(entries[0].status, "cancelled");
		assert.ok(entries[0].payload.result_note.includes("boom"));
	});

	it("picks first command entry by due_at when multiple match", async () => {
		const store = createScheduleStore(testDir);
		store.appendEntry(
			"project",
			makeEntry({
				id: "SECOND",
				kind: "command",
				due_at: "2024-01-02T00:00:00.000Z",
				autonomous_dispatch: true,
				payload: {
					command:
						"node -e \"require('fs').writeFileSync('picked.txt', 'second')\"",
				},
			}),
		);
		store.appendEntry(
			"project",
			makeEntry({
				id: "FIRST",
				kind: "command",
				due_at: "2024-01-01T00:00:00.000Z",
				autonomous_dispatch: true,
				payload: {
					command:
						"node -e \"require('fs').writeFileSync('picked.txt', 'first')\"",
				},
			}),
		);

		const state = { activeMilestone: null, phase: "idle" };
		const result = await runScheduleRule(state);
		assert.ok(result);
		assert.equal(result.action, "skip");
		assert.equal(readFileSync(join(testDir, "picked.txt"), "utf-8"), "first");
		const entries = store.readEntries("project");
		assert.equal(entries.find((e) => e.id === "FIRST")?.status, "done");
		assert.equal(entries.find((e) => e.id === "SECOND")?.status, "pending");
	});

	it("dispatches due autonomous_dispatch prompt as a custom step and consumes the trigger", async () => {
		const store = createScheduleStore(testDir);
		store.appendEntry(
			"project",
			makeEntry({
				id: "PROMPT",
				kind: "prompt",
				due_at: "2024-01-01T00:00:00.000Z",
				autonomous_dispatch: true,
				payload: { prompt: "refresh schedule diagnostics" },
			}),
		);

		const state = { activeMilestone: null, phase: "idle" };
		const result = await runScheduleRule(state);
		assert.ok(result);
		assert.equal(result.action, "dispatch");
		assert.equal(result.unitType, "custom-step");
		assert.equal(result.unitId, "schedule/PROMPT");
		assert.ok(result.prompt.includes("refresh schedule diagnostics"));
		const entries = store.readEntries("project");
		assert.equal(entries[0].status, "done");
		assert.equal(entries[0].payload.result_note, "prompt dispatched");
	});

	it("is non-fatal on store errors", async () => {
		const state = { activeMilestone: null, phase: "idle" };
		// Pass an invalid basePath that will cause store errors
		const result = await runScheduleRule(state, "/dev/null/invalid-path");
		assert.equal(result, null);
	});
});
