/**
 * Schedule CLI command tests.
 *
 * Purpose: verify the /schedule subcommands: add, list, done, cancel,
 * snooze, run, and the duration parser.
 *
 * Consumer: CI test runner (vitest).
 */
import assert from "node:assert/strict";
import { mkdirSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, it } from "vitest";
import { handleSchedule, parseDuration } from "../commands-schedule.js";
import { createScheduleStore } from "../schedule/schedule-store.js";

function mockCtx() {
	const notifications = [];
	return {
		ui: {
			notify: (msg, type = "info") => {
				notifications.push({ msg, type });
			},
		},
		notifications,
	};
}

describe("parseDuration", () => {
	it("parses weeks", () => {
		assert.equal(parseDuration("2w"), 2 * 7 * 24 * 60 * 60 * 1000);
	});

	it("parses days", () => {
		assert.equal(parseDuration("1d"), 24 * 60 * 60 * 1000);
	});

	it("parses hours", () => {
		assert.equal(parseDuration("1h"), 60 * 60 * 1000);
	});

	it("parses minutes", () => {
		assert.equal(parseDuration("30m"), 30 * 60 * 1000);
	});

	it("throws on invalid format", () => {
		assert.throws(() => parseDuration("foo"), /Invalid duration/);
	});
});

describe("handleSchedule", () => {
	let testDir;
	let originalCwd;

	beforeEach(() => {
		testDir = join(
			tmpdir(),
			`sf-schedule-cmd-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
		}
	});

	describe("add", () => {
		it("adds a scheduled item with --in", async () => {
			const ctx = mockCtx();
			await handleSchedule("add --in 1d test item", ctx);
			assert.equal(ctx.notifications.length, 1);
			assert.equal(ctx.notifications[0].type, "success");
			assert.ok(ctx.notifications[0].msg.includes("Scheduled:"));
		});

		it("rejects missing title", async () => {
			const ctx = mockCtx();
			await handleSchedule("add --in 1d", ctx);
			assert.equal(ctx.notifications[0].type, "warning");
			assert.ok(ctx.notifications[0].msg.includes("Missing title"));
		});

		it("rejects missing --in or --at", async () => {
			const ctx = mockCtx();
			await handleSchedule("add test item", ctx);
			assert.equal(ctx.notifications[0].type, "warning");
			assert.ok(ctx.notifications[0].msg.includes("Usage:"));
		});

		it("preserves argv-tokenized command payloads from top-level CLI", async () => {
			const ctx = mockCtx();
			await handleSchedule(
				[
					"add",
					"--in",
					"0m",
					"--kind",
					"command",
					"--autonomous-dispatch",
					"--",
					"node",
					"-e",
					"require('fs').writeFileSync('argv-cron.txt','ok')",
				],
				ctx,
			);

			const entries = createScheduleStore(testDir).readEntries("project");
			assert.equal(entries.length, 1);
			assert.equal(entries[0].kind, "command");
			assert.equal(entries[0].autonomous_dispatch, true);
			assert.equal(
				entries[0].payload.command,
				"node -e 'require('\\''fs'\\'').writeFileSync('\\''argv-cron.txt'\\'','\\''ok'\\'')'",
			);

			await handleSchedule(["run", entries[0].id.slice(0, 8)], mockCtx());
			assert.equal(readFileSync(join(testDir, "argv-cron.txt"), "utf-8"), "ok");
		});

		it("rejects removed autonomous dispatch shorthand", async () => {
			const ctx = mockCtx();
			await handleSchedule(
				"add --in 0m --kind command --auto-dispatch -- echo old",
				ctx,
			);

			assert.equal(ctx.notifications[0].type, "warning");
			assert.match(ctx.notifications[0].msg, /--autonomous-dispatch/);
			assert.equal(
				createScheduleStore(testDir).readEntries("project").length,
				0,
			);
		});
	});

	describe("list", () => {
		it("lists items after adding", async () => {
			const ctx1 = mockCtx();
			await handleSchedule("add --in 1d first item", ctx1);

			const ctx2 = mockCtx();
			await handleSchedule("list", ctx2);
			assert.equal(ctx2.notifications.length, 1);
			assert.equal(ctx2.notifications[0].type, "info");
			assert.ok(ctx2.notifications[0].msg.includes("first item"));
		});

		it("shows empty message when no items", async () => {
			const ctx = mockCtx();
			await handleSchedule("list", ctx);
			assert.equal(ctx.notifications[0].type, "info");
			assert.ok(ctx.notifications[0].msg.includes("No scheduled items"));
		});

		it("list --json preserves the exact stored command payload", async () => {
			const addCtx = mockCtx();
			await handleSchedule(
				[
					"add",
					"--in",
					"1h",
					"--kind",
					"command",
					"--autonomous-dispatch",
					"--",
					"node",
					"-e",
					"require('fs').writeFileSync('json-cron.txt','ok')",
				],
				addCtx,
			);
			const stored = createScheduleStore(testDir).readEntries("project")[0];

			const listCtx = mockCtx();
			await handleSchedule("list --json", listCtx);
			const listed = JSON.parse(listCtx.notifications[0].msg);

			assert.equal(listed.length, 1);
			assert.equal(listed[0].kind, "command");
			assert.equal(listed[0].autonomous_dispatch, true);
			assert.equal(listed[0].payload.command, stored.payload.command);
		});
	});

	describe("done", () => {
		it("marks an item done", async () => {
			const ctx1 = mockCtx();
			await handleSchedule("add --in 1d item to complete", ctx1);
			const id = ctx1.notifications[0].msg.match(/Scheduled: (\S+)/)?.[1];
			assert.ok(id);

			const ctx2 = mockCtx();
			await handleSchedule(`done ${id.slice(0, 8)}`, ctx2);
			assert.equal(ctx2.notifications[0].type, "success");
			assert.ok(ctx2.notifications[0].msg.includes("Marked done"));
		});
	});

	describe("cancel", () => {
		it("cancels an item", async () => {
			const ctx1 = mockCtx();
			await handleSchedule("add --in 1d item to cancel", ctx1);
			const id = ctx1.notifications[0].msg.match(/Scheduled: (\S+)/)?.[1];
			assert.ok(id);

			const ctx2 = mockCtx();
			await handleSchedule(`cancel ${id.slice(0, 8)}`, ctx2);
			assert.equal(ctx2.notifications[0].type, "success");
			assert.ok(ctx2.notifications[0].msg.includes("Cancelled"));
		});
	});

	describe("snooze", () => {
		it("snoozes an item", async () => {
			const ctx1 = mockCtx();
			await handleSchedule("add --in 1d item to snooze", ctx1);
			const id = ctx1.notifications[0].msg.match(/Scheduled: (\S+)/)?.[1];
			assert.ok(id);

			const ctx2 = mockCtx();
			await handleSchedule(`snooze ${id.slice(0, 8)} --by 2d`, ctx2);
			assert.equal(ctx2.notifications[0].type, "success");
			assert.ok(ctx2.notifications[0].msg.includes("Snoozed"));
		});

		it("snoozed item still appears in list", async () => {
			const ctx1 = mockCtx();
			await handleSchedule("add --in 1d item that will be snoozed", ctx1);
			const id = ctx1.notifications[0].msg.match(/Scheduled: (\S+)/)?.[1];
			assert.ok(id);

			await handleSchedule(`snooze ${id.slice(0, 8)} --by 2d`, mockCtx());

			const ctx3 = mockCtx();
			await handleSchedule("list", ctx3);
			assert.ok(
				ctx3.notifications[0].msg.includes("item that will be snoozed"),
				"snoozed item must still appear in default list",
			);
		});
	});

	describe("run", () => {
		it("runs a reminder item and marks done", async () => {
			const ctx1 = mockCtx();
			await handleSchedule("add --in 1d --kind reminder reminder item", ctx1);
			const id = ctx1.notifications[0].msg.match(/Scheduled: (\S+)/)?.[1];
			assert.ok(id);

			const ctx2 = mockCtx();
			await handleSchedule(`run ${id.slice(0, 8)}`, ctx2);
			assert.ok(ctx2.notifications.some((n) => n.msg.includes("Reminder:")));
			assert.ok(ctx2.notifications.some((n) => n.type === "success"));
		});

		it("dry-runs a command item without executing or marking done", async () => {
			const ctx1 = mockCtx();
			await handleSchedule(
				[
					"add",
					"--in",
					"0m",
					"--kind",
					"command",
					"--autonomous-dispatch",
					"--",
					"node",
					"-e",
					"require('fs').writeFileSync('dry-run-cron.txt','bad')",
				],
				ctx1,
			);
			const id = ctx1.notifications[0].msg.match(/Scheduled: (\S+)/)?.[1];
			assert.ok(id);

			const ctx2 = mockCtx();
			await handleSchedule(["run", "--dry-run", id.slice(0, 8)], ctx2);

			const preview = JSON.parse(ctx2.notifications[0].msg);
			assert.equal(preview.id, id);
			assert.equal(preview.kind, "command");
			assert.equal(preview.would_execute, true);
			assert.ok(preview.command.includes("dry-run-cron.txt"));
			assert.throws(() => readFileSync(join(testDir, "dry-run-cron.txt")));
			const entries = createScheduleStore(testDir).readEntries("project");
			assert.equal(entries[0].status, "pending");
		});
	});

	describe("usage", () => {
		it("shows usage when called with no subcommand", async () => {
			const ctx = mockCtx();
			await handleSchedule("", ctx);
			assert.equal(ctx.notifications[0].type, "info");
			assert.ok(ctx.notifications[0].msg.includes("Usage:"));
		});
	});
});
