/**
 * Schedule E2E — full add→list→done round-trip.
 *
 * Purpose: verify the schedule system end-to-end through the store API:
 * appendEntry with a past due_at, findDue returns it, list shows it,
 * updateEntryStatus to done, findDue returns 0.
 *
 * Consumer: CI test runner (vitest).
 */
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 { createScheduleStore } from "../schedule/schedule-store.js";
import { generateULID } from "../schedule/schedule-ulid.js";
import { closeDatabase } from "../sf-db.js";

describe("schedule-e2e round-trip", () => {
	/** @type {string} */
	let testDir;
	/** @type {ReturnType<typeof createScheduleStore>} */
	let store;

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

	afterEach(() => {
		closeDatabase();
		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" },
			created_by: "user",
			...overrides,
		};
	}

	it("appends an entry with due_at in the past and findDue returns it", () => {
		const entry = makeEntry({
			due_at: "2020-01-01T00:00:00.000Z",
			status: "pending",
		});
		store.appendEntry("project", entry);

		const due = store.findDue("project", "2024-06-01T00:00:00.000Z");
		assert.equal(due.length, 1);
		assert.equal(due[0].id, entry.id);
	});

	it("list shows the pending entry", () => {
		const entry = makeEntry({
			due_at: "2020-01-01T00:00:00.000Z",
			status: "pending",
			payload: { message: "e2e test item" },
		});
		store.appendEntry("project", entry);

		const entries = store
			.readEntries("project")
			.filter((e) => e.status === "pending" || e.status === "snoozed");
		assert.equal(entries.length, 1);
		assert.equal(entries[0].id, entry.id);
		assert.equal(entries[0].payload.message, "e2e test item");
	});

	it("marking done via appendEntry update removes it from findDue", () => {
		const entry = makeEntry({
			due_at: "2020-01-01T00:00:00.000Z",
			status: "pending",
		});
		store.appendEntry("project", entry);

		// Verify it's due
		let due = store.findDue("project", "2024-06-01T00:00:00.000Z");
		assert.equal(due.length, 1);

		// Append a "done" update for the same ID
		const updated = {
			...entry,
			status: "done",
			created_at: new Date().toISOString(),
		};
		store.appendEntry("project", updated);

		// Now findDue should return 0
		due = store.findDue("project", "2024-06-01T00:00:00.000Z");
		assert.equal(due.length, 0);
	});

	it("full round-trip: add past → findDue → list → done → findDue empty", () => {
		const entry = makeEntry({
			due_at: "2020-01-01T00:00:00.000Z",
			status: "pending",
			payload: { message: "round-trip item" },
		});

		// 1) add
		store.appendEntry("project", entry);

		// 2) findDue returns it
		let due = store.findDue("project", "2024-06-01T00:00:00.000Z");
		assert.equal(due.length, 1);
		assert.equal(due[0].id, entry.id);

		// 3) list shows it
		const list = store
			.readEntries("project")
			.filter((e) => e.status === "pending" || e.status === "snoozed");
		assert.equal(list.length, 1);
		assert.equal(list[0].payload.message, "round-trip item");

		// 4) mark done
		const updated = {
			...entry,
			status: "done",
			created_at: new Date().toISOString(),
		};
		store.appendEntry("project", updated);

		// 5) findDue returns 0
		due = store.findDue("project", "2024-06-01T00:00:00.000Z");
		assert.equal(due.length, 0);
	});

	it("isolates scopes: two project stores do not see each other’s entries", () => {
		const testDir2 = join(
			tmpdir(),
			`sf-schedule-e2e-2-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
		);
		mkdirSync(testDir2, { recursive: true });
		const store2 = createScheduleStore(testDir2);

		const entry1 = makeEntry({
			due_at: "2020-01-01T00:00:00.000Z",
			status: "pending",
			payload: { message: "store 1" },
		});
		const entry2 = makeEntry({
			due_at: "2020-01-01T00:00:00.000Z",
			status: "pending",
			payload: { message: "store 2" },
		});

		store.appendEntry("project", entry1);
		store2.appendEntry("project", entry2);

		const due1 = store.findDue("project", "2024-06-01T00:00:00.000Z");
		const due2 = store2.findDue("project", "2024-06-01T00:00:00.000Z");

		assert.equal(due1.length, 1);
		assert.equal(due1[0].payload.message, "store 1");
		assert.equal(due2.length, 1);
		assert.equal(due2[0].payload.message, "store 2");

		try {
			rmSync(testDir2, { recursive: true });
		} catch {
			// ignore
		}
	});

	it("1000-entry loadEntries completes within threshold", () => {
		const count = 1000;
		for (let i = 0; i < count; i++) {
			const entry = makeEntry({
				due_at: "2020-01-01T00:00:00.000Z",
				status: "pending",
				payload: { message: `entry ${i}` },
			});
			store.appendEntry("project", entry);
		}

		const start = performance.now();
		const entries = store.readEntries("project");
		const elapsed = performance.now() - start;

		assert.equal(entries.length, count);

		// Catastrophic-regression guard, not a microbenchmark. Anything under a
		// few hundred ms means we are still doing one-shot read+parse rather than
		// O(N) per-entry scans; full-suite parallelism on dev machines routinely
		// adds >100ms of scheduler/filesystem jitter, so we deliberately use the
		// same generous threshold everywhere.
		const thresholdMs = 500;
		assert.ok(
			elapsed < thresholdMs,
			`Expected readEntries(${count}) to complete in <${thresholdMs}ms, took ${elapsed.toFixed(2)}ms`,
		);
	});

	it("2 appends produce 2 DB-backed entries with unique IDs", () => {
		const first = makeEntry({ due_at: "2020-01-01T00:00:00.000Z" });
		const second = makeEntry({ due_at: "2020-01-01T00:00:00.000Z" });
		store.appendEntry("project", first);
		store.appendEntry("project", second);

		const entries = store.readEntries("project");
		assert.equal(entries.length, 2);
		const ids = entries.map((e) => e.id);
		assert.notEqual(ids[0], ids[1], "Expected two unique IDs");
	});
});
