// node --experimental-strip-types --test
import { test } from "node:test";
import assert from "node:assert/strict";
import {
  parseApplication,
  signAction,
  verifyAction,
  storeApplication,
  getApplication,
  decideApplication,
  handleApply,
  handleReview,
  HONEYPOT_FIELD,
  type Application,
  type TesterEnv,
} from "../src/testers.ts";

/** Minimal KV stub — get/put(+ttl opts, ignored) is all the module touches. */
function kvStub() {
  const store = new Map<string, string>();
  const kv = {
    get: async (key: string) => store.get(key) ?? null,
    put: async (key: string, value: string) => {
      store.set(key, value);
    },
  } as unknown as KVNamespace;
  return { store, kv };
}

/** A complete, valid raw form body. */
function validBody(overrides: Record<string, unknown> = {}): Record<string, unknown> {
  return {
    firstName: "Ada",
    lastName: "Lovelace",
    email: "ada@example.com",
    jobTitle: "Staff Engineer",
    employer: "Analytical Engines Ltd",
    experienceYears: "12",
    techBackground: "Swift, TypeScript, Rust",
    agentExperience: "Daily Claude Code + Codex user",
    applePlatform: "8 years iOS/macOS",
    testDevices: "iPhone 15 Pro on iOS 18.1",
    motivation: "I ship developer tools and give sharp, reproducible bug reports.",
    ...overrides,
  };
}

function testEnv(secret = "test-secret"): { env: TesterEnv; store: Map<string, string>; sent: any[] } {
  const { kv, store } = kvStub();
  const sent: any[] = [];
  const env: TesterEnv = {
    NUCLEIC_TESTER_APPS: kv,
    OWNER_EMAIL: { send: async (m: unknown) => { sent.push(m); return {} as any; } } as unknown as SendEmail,
    TESTER_APP_SECRET: secret,
  };
  return { env, store, sent };
}

function applyRequest(body: unknown, headers: Record<string, string> = {}): Request {
  return new Request("https://api.nucleic.blakeslee.xyz/v1/tester/apply", {
    method: "POST",
    headers: { "content-type": "application/json", ...headers },
    body: JSON.stringify(body),
  });
}

// MARK: - parseApplication

test("parse accepts a complete body and trims fields", () => {
  const r = parseApplication(validBody({ firstName: "  Ada  " }));
  assert.equal(r.ok, true);
  if (r.ok) {
    assert.equal(r.value.firstName, "Ada");
    assert.equal(r.value.email, "ada@example.com");
    assert.equal(r.value.links, undefined); // optional, absent
  }
});

test("parse rejects each missing required field", () => {
  for (const k of ["firstName", "email", "employer", "motivation"]) {
    const r = parseApplication(validBody({ [k]: "" }));
    assert.equal(r.ok, false, `expected ${k} to be required`);
  }
});

test("parse rejects a bad email and an over-long field", () => {
  assert.equal(parseApplication(validBody({ email: "not-an-email" })).ok, false);
  assert.equal(parseApplication(validBody({ motivation: "x".repeat(6000) })).ok, false);
});

test("parse flags a filled honeypot distinctly", () => {
  const r = parseApplication(validBody({ [HONEYPOT_FIELD]: "http://spam.example" }));
  assert.equal(r.ok, false);
  if (!r.ok) assert.equal(r.honeypot, true);
});

// MARK: - signed links

test("action tokens round-trip and reject tampering", async () => {
  const tok = await signAction("s", "id-1", "approve");
  assert.equal(await verifyAction("s", "id-1", "approve", tok), true);
  assert.equal(await verifyAction("s", "id-1", "reject", tok), false); // wrong action
  assert.equal(await verifyAction("s", "id-2", "approve", tok), false); // wrong id
  assert.equal(await verifyAction("other", "id-1", "approve", tok), false); // wrong secret
  assert.equal(await verifyAction("s", "id-1", "approve", ""), false); // empty
});

// MARK: - storage

function pendingApp(id: string, email = "ada@example.com"): Application {
  return {
    id,
    status: "pending",
    createdAt: "2026-07-04T00:00:00.000Z",
    firstName: "Ada",
    lastName: "Lovelace",
    email,
    jobTitle: "Staff Engineer",
    employer: "AE Ltd",
    experienceYears: "12",
    techBackground: "Swift",
    agentExperience: "Claude Code",
    applePlatform: "iOS",
    testDevices: "iPhone 15",
    motivation: "good bugs",
  };
}

test("store persists a pending record and dedupes non-rejected re-applies", async () => {
  const { kv } = kvStub();
  const first = await storeApplication(kv, pendingApp("a1"));
  assert.equal(first.duplicate, false);
  assert.deepEqual((await getApplication(kv, "a1"))?.status, "pending");

  const dup = await storeApplication(kv, pendingApp("a2"));
  assert.equal(dup.duplicate, true); // same email, still pending
});

test("a rejected applicant may re-apply", async () => {
  const { kv } = kvStub();
  await storeApplication(kv, pendingApp("a1"));
  await decideApplication(kv, "a1", "reject");
  const again = await storeApplication(kv, pendingApp("a3"));
  assert.equal(again.duplicate, false);
});

test("decide flips pending → decided and is idempotent", async () => {
  const { kv } = kvStub();
  await storeApplication(kv, pendingApp("a1"));
  assert.equal((await decideApplication(kv, "a1", "approve"))?.status, "approved");
  // Re-click keeps the first decision.
  assert.equal((await decideApplication(kv, "a1", "reject"))?.status, "approved");
  assert.equal(await decideApplication(kv, "missing", "approve"), null);
});

// MARK: - handlers

test("apply stores pending, emails the owner, and 200s", async () => {
  const { env, store, sent } = testEnv();
  const res = await handleApply(applyRequest(validBody()), env);
  assert.equal(res.status, 200);
  assert.deepEqual(await res.json(), { ok: true });
  assert.equal(sent.length, 1);
  assert.equal(sent[0].to, "accounts@abkslm.com");
  // exactly one app record written
  const appKeys = [...store.keys()].filter((k) => k.startsWith("app:"));
  assert.equal(appKeys.length, 1);
});

test("apply drops a honeypot silently (200, no store, no email)", async () => {
  const { env, store, sent } = testEnv();
  const res = await handleApply(applyRequest(validBody({ [HONEYPOT_FIELD]: "x" })), env);
  assert.equal(res.status, 200);
  assert.equal(sent.length, 0);
  assert.equal([...store.keys()].filter((k) => k.startsWith("app:")).length, 0);
});

test("apply 422s an invalid body and is idempotent on duplicates", async () => {
  const { env, sent } = testEnv();
  assert.equal((await handleApply(applyRequest(validBody({ email: "nope" })), env)).status, 422);

  await handleApply(applyRequest(validBody()), env);
  const dup = await handleApply(applyRequest(validBody()), env);
  assert.equal(dup.status, 200);
  assert.deepEqual(await dup.json(), { ok: true, duplicate: true });
  assert.equal(sent.length, 1); // duplicate did not re-notify
});

test("apply preflight (OPTIONS) returns CORS headers", async () => {
  const { env } = testEnv();
  const res = await handleApply(
    new Request("https://api.nucleic.blakeslee.xyz/v1/tester/apply", {
      method: "OPTIONS",
      headers: { origin: "https://nucleic.blakeslee.xyz" },
    }),
    env,
  );
  assert.equal(res.status, 204);
  assert.equal(res.headers.get("access-control-allow-origin"), "https://nucleic.blakeslee.xyz");
});

/** Build the GET review request + parsed URL the way index.ts routes it. */
function reviewCall(env: TesterEnv, id: string, action: string, token: string) {
  const q = new URLSearchParams({ id, action, token });
  const url = new URL(`https://api.nucleic.blakeslee.xyz/v1/tester/review?${q}`);
  return handleReview(new Request(url, { method: "GET" }), env, url);
}

test("review flips status with a valid link and rejects a forged one", async () => {
  const { env, store } = testEnv();
  await handleApply(applyRequest(validBody()), env);
  const id = [...store.keys()].find((k) => k.startsWith("app:"))!.slice(4);
  const token = await signAction("test-secret", id, "approve");

  const ok = await reviewCall(env, id, "approve", token);
  assert.equal(ok.status, 200);
  assert.equal((await getApplication(env.NUCLEIC_TESTER_APPS, id))?.status, "approved");
  assert.match(await ok.text(), /Approved/);

  assert.equal((await reviewCall(env, id, "reject", "deadbeef")).status, 401);
  assert.equal((await reviewCall(env, id, "sideways", token)).status, 400);
  const missTok = await signAction("test-secret", "no-such-id", "approve");
  assert.equal((await reviewCall(env, "no-such-id", "approve", missTok)).status, 404);
});
