// test/serp-semantics.test.ts — job serp-semantics : garde anti-doublon
// (POST /jobs) + dispatch handler (runner mocké par DI, pas de mock.module —
// cf. mémoire projet sur la fuite globale de mock.module entre fichiers de
// test). Mirror de enqueue.test.ts (garde) et worker.test.ts (dispatch).
import { beforeAll, expect, test } from "bun:test";
import { randomUUID } from "node:crypto";
import { eq } from "drizzle-orm";
import { analyticsJobs } from "@/lib/db/domain-schema";
import { db } from "../src/db";
import { app } from "../src/index";
import { processSerpSemantics } from "../src/serp-semantics";
import { ensureTestSite, TEST_SITE as SITE } from "./fixtures/site";

// A real key exported in the developer's shell would turn these unauthenticated
// requests into 401s — the auth path has its own suite (auth.test.ts).
delete process.env.ANALYTICS_API_KEY;

beforeAll(ensureTestSite);

const payload = {
  keywordId: "kw-serp-semantics-test",
  country: "FR",
  urls: [{ position: 1, url: "https://example.com/a", included: true }],
};

test("POST /jobs accepts a serp-semantics job, then 409 while active for the same keywordId", async () => {
  const headers = { "content-type": "application/json" };
  const body = JSON.stringify({
    type: "serp-semantics",
    siteId: SITE,
    payload,
  });

  const res = await app.request("/jobs", { method: "POST", headers, body });
  expect(res.status).toBe(200);
  const { id } = (await res.json()) as { id: string };
  const row = (
    await db.select().from(analyticsJobs).where(eq(analyticsJobs.id, id))
  )[0];
  expect(row.type).toBe("serp-semantics");
  expect(row.status).toBe("queued");
  expect(row.payload?.keywordId).toBe(payload.keywordId);

  // A second job for the SAME keywordId is refused while the first is active.
  const dup = await app.request("/jobs", { method: "POST", headers, body });
  expect(dup.status).toBe(409);

  // A DIFFERENT keywordId is not blocked by the first job's guard.
  const other = await app.request("/jobs", {
    method: "POST",
    headers,
    body: JSON.stringify({
      type: "serp-semantics",
      siteId: SITE,
      payload: { ...payload, keywordId: "kw-serp-semantics-other" },
    }),
  });
  expect(other.status).toBe(200);
  const { id: otherId } = (await other.json()) as { id: string };

  await db.delete(analyticsJobs).where(eq(analyticsJobs.id, id));
  await db.delete(analyticsJobs).where(eq(analyticsJobs.id, otherId));
});

test("POST /jobs rejects an invalid serp-semantics payload (country not 2 chars)", async () => {
  const res = await app.request("/jobs", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({
      type: "serp-semantics",
      siteId: SITE,
      payload: { ...payload, country: "FRA" },
    }),
  });
  expect(res.status).toBe(400);
});

test("processSerpSemantics: succès du runner → completeJob (done)", async () => {
  const id = randomUUID();
  await db.insert(analyticsJobs).values({
    id,
    siteId: SITE,
    type: "serp-semantics",
    payload,
    status: "running",
  });
  const job = (
    await db.select().from(analyticsJobs).where(eq(analyticsJobs.id, id))
  )[0];
  await processSerpSemantics(job, async () => ({ ok: true }));
  const done = (
    await db.select().from(analyticsJobs).where(eq(analyticsJobs.id, id))
  )[0];
  expect(done.status).toBe("done");
  await db.delete(analyticsJobs).where(eq(analyticsJobs.id, id));
});

test("processSerpSemantics: échec du runner → failJob (failed) avec le message", async () => {
  const id = randomUUID();
  await db.insert(analyticsJobs).values({
    id,
    siteId: SITE,
    type: "serp-semantics",
    payload,
    status: "running",
  });
  const job = (
    await db.select().from(analyticsJobs).where(eq(analyticsJobs.id, id))
  )[0];
  await processSerpSemantics(job, async () => ({ ok: false, error: "boom" }));
  const failed = (
    await db.select().from(analyticsJobs).where(eq(analyticsJobs.id, id))
  )[0];
  expect(failed.status).toBe("failed");
  expect(failed.error).toBe("boom");
  await db.delete(analyticsJobs).where(eq(analyticsJobs.id, id));
});

test("processSerpSemantics: payload manquant → failJob sans appeler le runner", async () => {
  const id = randomUUID();
  await db.insert(analyticsJobs).values({
    id,
    siteId: SITE,
    type: "serp-semantics",
    status: "running",
  });
  const job = (
    await db.select().from(analyticsJobs).where(eq(analyticsJobs.id, id))
  )[0];
  let called = false;
  await processSerpSemantics(job, async () => {
    called = true;
    return { ok: true };
  });
  expect(called).toBe(false);
  const failed = (
    await db.select().from(analyticsJobs).where(eq(analyticsJobs.id, id))
  )[0];
  expect(failed.status).toBe("failed");
  await db.delete(analyticsJobs).where(eq(analyticsJobs.id, id));
});
