/**
 * Schedule Milestone Integration — unit tests.
 *
 * Purpose: verify that appendScheduleSpec / appendScheduleSpecs correctly
 * converts ScheduleSpec objects into ScheduleEntry records and appends them
 * to the store. Covers all valid kinds, invalid-kind rejection, duration
 * parsing, entry shape, and non-fatal error handling.
 *
 * Consumer: CI test runner (vitest). Run via `npm test`.
 */
import assert from "node:assert/strict";
import { mkdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, it } from "vitest";
import {
	appendScheduleSpec,
	appendScheduleSpecs,
} from "../schedule/schedule-milestone.js";
import { createScheduleStore } from "../schedule/schedule-store.js";

/** @type {string} */
let testDir;

beforeEach(() => {
	testDir = join(
		tmpdir(),
		`sf-schedule-milestone-test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
	);
	mkdirSync(join(testDir, ".sf"), { recursive: true });
});

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

describe("appendScheduleSpec", () => {
	describe("kind validation", () => {
		for (const kind of [
			"review",
			"audit",
			"reminder",
			"milestone_check",
			"review_due",
			"recurring",
		]) {
			it(`kind "${kind}" creates an entry (not silently dropped)`, () => {
				const spec = {
					in: "1d",
					kind,
					title: `Test ${kind} entry`,
				};
				appendScheduleSpec(testDir, spec, "M001");

				const store = createScheduleStore(testDir);
				const entries = store.readEntries("project");

				assert.ok(
					entries.some((e) => e.kind === kind && e.status === "pending"),
					`no pending entry with kind="${kind}" found. Entries: ${JSON.stringify(entries)}`,
				);
			});
		}

		it("invalid kind is skipped (no throw, no entry created)", () => {
			const spec = {
				in: "1d",
				kind: "not_a_real_kind",
				title: "Should be silently dropped",
			};
			// Should not throw
			appendScheduleSpec(testDir, spec, "M001");

			const store = createScheduleStore(testDir);
			const entries = store.readEntries("project");
			assert.equal(entries.length, 0, "invalid kind should create no entry");
		});
	});

	describe("entry shape", () => {
		it("creates entry with required fields", () => {
			const spec = {
				in: "2w",
				kind: "review",
				title: "Check adoption",
				payload: { extra: "data" },
			};
			appendScheduleSpec(testDir, spec, "M042");

			const store = createScheduleStore(testDir);
			const entries = store.readEntries("project");

			assert.equal(entries.length, 1);
			const entry = entries[0];
			assert.equal(entry.kind, "review");
			assert.equal(entry.status, "pending");
			assert.equal(entry.created_by, "milestone");
			assert.equal(entry.payload.milestoneId, "M042");
			assert.equal(entry.payload.message, "Check adoption");
			assert.equal(entry.payload.extra, "data");
			assert.ok(typeof entry.id === "string" && entry.id.length === 28);
			assert.ok(typeof entry.due_at === "string");
			assert.ok(typeof entry.created_at === "string");
		});

		it("sets due_at correctly for in: duration", () => {
			const before = Date.now();
			const spec = { in: "1d", kind: "reminder", title: "Daily check" };
			appendScheduleSpec(testDir, spec);

			const store = createScheduleStore(testDir);
			const entries = store.readEntries("project");
			const dueMs = new Date(entries[0].due_at).getTime();
			const expectedMs = before + 24 * 60 * 60 * 1000;

			assert.ok(
				Math.abs(dueMs - expectedMs) < 5000,
				`due_at ${dueMs} is more than 5s from expected ${expectedMs}`,
			);
		});

		it("sets due_at correctly for on_complete.in duration", () => {
			const before = Date.now();
			const spec = {
				on_complete: { in: "3d" },
				kind: "audit",
				title: "Post-completion audit",
			};
			appendScheduleSpec(testDir, spec, "M001");

			const store = createScheduleStore(testDir);
			const entries = store.readEntries("project");
			const dueMs = new Date(entries[0].due_at).getTime();
			const expectedMs = before + 3 * 24 * 60 * 60 * 1000;

			assert.ok(
				Math.abs(dueMs - expectedMs) < 5000,
				`due_at ${dueMs} is more than 5s from expected ${expectedMs}`,
			);
		});

		it("skips spec with no in or on_complete.in (no entry, no throw)", () => {
			const spec = { kind: "review", title: "No duration" };
			appendScheduleSpec(testDir, spec);

			const store = createScheduleStore(testDir);
			const entries = store.readEntries("project");
			assert.equal(entries.length, 0);
		});
	});

	describe("non-fatal error handling", () => {
		it("throws nothing for null spec", () => {
			assert.doesNotThrow(() => {
				// @ts-expect-error — intentionally passing bad input
				appendScheduleSpec(testDir, null);
			});
		});

		it("throws nothing for non-object spec", () => {
			assert.doesNotThrow(() => {
				// @ts-expect-error
				appendScheduleSpec(testDir, "not an object");
			});
		});

		it("skips spec without milestoneId when milestoneId is undefined", () => {
			const spec = { in: "1h", kind: "audit", title: "Ad-hoc audit" };
			appendScheduleSpec(testDir, spec); // no milestoneId arg

			const store = createScheduleStore(testDir);
			const entries = store.readEntries("project");
			assert.equal(entries.length, 1);
			assert.equal(entries[0].payload.milestoneId, undefined);
		});
	});
});

describe("appendScheduleSpecs", () => {
	it("processes an array of specs", () => {
		const specs = [
			{ in: "1d", kind: "review", title: "First review" },
			{ in: "2d", kind: "audit", title: "Second audit" },
			{ in: "3d", kind: "reminder", title: "Third reminder" },
		];
		appendScheduleSpecs(testDir, specs, "M001");

		const store = createScheduleStore(testDir);
		const entries = store.readEntries("project");

		assert.equal(entries.length, 3);
		assert.ok(entries.some((e) => e.kind === "review"));
		assert.ok(entries.some((e) => e.kind === "audit"));
		assert.ok(entries.some((e) => e.kind === "reminder"));
	});

	it("one bad spec does not block others", () => {
		const specs = [
			{ in: "1d", kind: "review", title: "Good one" },
			{ in: "2d", kind: "bad_kind", title: "Bad kind" },
			{ in: "3d", kind: "audit", title: "Good two" },
		];
		appendScheduleSpecs(testDir, specs, "M001");

		const store = createScheduleStore(testDir);
		const entries = store.readEntries("project");

		assert.equal(
			entries.length,
			2,
			"bad kind should be skipped, others preserved",
		);
		assert.ok(entries.every((e) => e.kind !== "bad_kind"));
	});

	it("handles empty array without throw", () => {
		assert.doesNotThrow(() => {
			appendScheduleSpecs(testDir, [], "M001");
		});
	});

	it("handles non-array input without throw", () => {
		assert.doesNotThrow(() => {
			// @ts-expect-error
			appendScheduleSpecs(testDir, "not an array", "M001");
		});
	});
});
