import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { mkdtemp, rm, writeFile, readdir } from "fs/promises";
import { tmpdir } from "os";
import * as path from "path";

import type { PublishRequest } from "./types";

// jobStore.ts reads JOBS_DIR once at module load, so each test gets a fresh
// module (and a fresh temp directory) instead of sharing state -- this is
// what actually lets these tests prove persistence works, rather than just
// exercising the same in-memory object every time.
async function freshJobStore() {
  const dir = await mkdtemp(path.join(tmpdir(), "jobstore-test-"));
  process.env.JOBS_DIR = dir;
  vi.resetModules();
  const jobStore = await import("./jobStore");
  return { dir, ...jobStore };
}

function request(requestId: string, overrides: Partial<PublishRequest> = {}): PublishRequest {
  return {
    requestId,
    submittedByEmail: "a@uwaterloo.ca",
    event: {
      name: "Test",
      short: "short",
      startDate: "August 05 2026 10:00",
      online: false,
      location: "DC 1351",
      descriptionMarkdown: "desc",
      year: 2026,
      term: "spring",
      slug: "test",
      poster: null,
    },
    ...overrides,
  };
}

describe("jobStore (file-based)", () => {
  let dir: string;

  afterEach(async () => {
    delete process.env.JOBS_DIR;
    if (dir) await rm(dir, { recursive: true, force: true });
  });

  it("creates a job and can read it back", async () => {
    const store = await freshJobStore();
    dir = store.dir;

    const { job, isNew } = await store.getOrCreateJob(request("r1"));
    expect(isNew).toBe(true);
    expect(job.status).toBe("processing");

    const fetched = await store.getJob("r1");
    expect(fetched?.requestId).toBe("r1");
  });

  it("actually persists to disk -- surviving a fresh module import (simulating a restart)", async () => {
    const store = await freshJobStore();
    dir = store.dir;
    await store.getOrCreateJob(request("r2"));

    // Re-import with the same JOBS_DIR, exactly like a process restart would
    // reload jobStore.ts against the same on-disk directory.
    vi.resetModules();
    const restarted = await import("./jobStore");
    const fetched = await restarted.getJob("r2");
    expect(fetched?.requestId).toBe("r2");
  });

  it("is idempotent for a retry with an identical body", async () => {
    const store = await freshJobStore();
    dir = store.dir;
    const first = await store.getOrCreateJob(request("r3"));
    const second = await store.getOrCreateJob(request("r3"));
    expect(second.isNew).toBe(false);
    expect(second.job.createdAt).toBe(first.job.createdAt);
  });

  it("throws RequestConflictError for the same requestId with a different body", async () => {
    const store = await freshJobStore();
    dir = store.dir;
    await store.getOrCreateJob(request("r4"));
    await expect(store.getOrCreateJob(request("r4", { submittedByEmail: "b@uwaterloo.ca" }))).rejects.toThrow(
      store.RequestConflictError
    );
  });

  it("updateJob patches fields and bumps updatedAt", async () => {
    const store = await freshJobStore();
    dir = store.dir;
    const { job } = await store.getOrCreateJob(request("r5"));
    await new Promise((resolve) => setTimeout(resolve, 5));
    const updated = await store.updateJob("r5", { status: "pr_created", pullRequestUrl: "https://example/pr/1" });
    expect(updated.status).toBe("pr_created");
    expect(updated.pullRequestUrl).toBe("https://example/pr/1");
    expect(updated.updatedAt).toBeGreaterThan(job.updatedAt);
  });

  it("evicts jobs older than the TTL on the next getOrCreateJob call", async () => {
    const store = await freshJobStore();
    dir = store.dir;
    await store.getOrCreateJob(request("stale"));

    // Directly overwrite the file with an updatedAt far in the past --
    // exercising the real eviction sweep rather than waiting 24h.
    const stalePath = path.join(dir, "stale.json");
    const stale = JSON.parse(await (await import("fs/promises")).readFile(stalePath, "utf-8"));
    stale.updatedAt = Date.now() - 25 * 60 * 60 * 1000;
    await writeFile(stalePath, JSON.stringify(stale));

    await store.getOrCreateJob(request("trigger-eviction"));

    const remaining = await readdir(dir);
    expect(remaining.some((name) => name.startsWith("stale"))).toBe(false);
    expect(remaining.some((name) => name.startsWith("trigger-eviction"))).toBe(true);
  });

  it("getJob returns undefined for an unknown requestId", async () => {
    const store = await freshJobStore();
    dir = store.dir;
    expect(await store.getJob("does-not-exist")).toBeUndefined();
  });
});
