// src/lib/keywords/serp-actions.test.ts — integration, gated on DB_TESTS=1, *_test DB only.
import { beforeEach, describe, expect, mock, test } from "bun:test";
import { AnalyticsJobError } from "@/lib/analytics/enqueue";

mock.module("server-only", () => ({}));
mock.module("next/cache", () => ({
  revalidatePath: () => {},
  revalidateTag: () => {},
  updateTag: () => {},
  unstable_cache: (fn: unknown) => fn,
}));
// The tenancy guard (assertKeywordInActiveOrg) resolves the org from the
// session — stub the session primitive; "demo-org" matches the seeded org
// of the fixtures below, so the REAL guard runs against the test DB.
mock.module("@/lib/active-org", () => ({
  getActiveOrgId: async () => "demo-org",
  getActiveUserId: async () => "seed-owner",
}));
// crawlSerpForKeywordAction (Task 5) now only enqueues a `crawl-onpage`
// analytics job — the Firecrawl batch-scrape + WASM on-page parsing moved
// server-side (services/analytics/src/crawl-onpage.ts), so there's nothing
// left to fetch/mock at that level. Stub the enqueue call itself so these
// tests never hit the real ANALYTICS_URL; `AnalyticsJobError` is re-exported
// for real so the action's `err instanceof AnalyticsJobError` check still
// works against the class the test throws.
let enqueueImpl: (input: unknown) => Promise<string> = async () => "job-1";
const enqueueAnalyticsJob = mock((input: unknown) => enqueueImpl(input));
mock.module("@/lib/analytics/enqueue", () => ({
  AnalyticsJobError,
  enqueueAnalyticsJob,
}));

const RUN_DB = process.env.DB_TESTS === "1";

describe.skipIf(!RUN_DB)("serp actions (postgres)", () => {
  let db: typeof import("@/lib/db/client").db;
  let schema: typeof import("@/lib/db/domain-schema");
  let actions: typeof import("./serp-actions");

  beforeEach(async () => {
    ({ db } = await import("@/lib/db/client"));
    schema = await import("@/lib/db/domain-schema");
    actions = await import("./serp-actions");
    // Real assert path (no leaked assert-site mock when run standalone):
    // inject the caller the tenancy guard resolves, matching the fixtures.
    const { __setTestCaller } = await import("@/lib/sites/access");
    __setTestCaller({ userId: "seed-owner", orgId: "demo-org", role: "owner" });
    const { assertTestDb } = await import("@/lib/db/assert-test-db");
    await assertTestDb();

    enqueueAnalyticsJob.mockClear();
    enqueueImpl = async () => "job-1";

    await db.delete(schema.keywords);
    await db.delete(schema.sites);
    await db.insert(schema.sites).values([
      {
        id: "site-a",
        orgId: "demo-org",
        ownerUserId: "seed-owner",
        name: "Site A",
        primaryLanguage: "fr",
        languages: [],
        targetCountries: ["FR"],
      },
      {
        id: "site-other",
        orgId: "other-org",
        ownerUserId: "seed-owner",
        name: "Site Other",
        primaryLanguage: "fr",
        languages: [],
        targetCountries: ["FR"],
      },
    ]);
    await db.insert(schema.keywords).values([
      {
        id: "kw-1",
        siteId: "site-a",
        term: "comparateur electricite pro",
        lang: "fr",
        country: "FR",
        status: "active",
        intent: ["commercial"],
        primarySource: "mangools",
        sources: ["mangools"],
        metrics: {},
        trend: [],
      },
      {
        id: "kw-other-org",
        siteId: "site-other",
        term: "mot cle autre org",
        lang: "fr",
        country: "FR",
        status: "active",
        intent: ["commercial"],
        primarySource: "mangools",
        sources: ["mangools"],
        metrics: {},
        trend: [],
      },
    ]);
  });

  test("crawlSerpForKeywordAction enqueues a crawl-onpage job and returns its id", async () => {
    const res = await actions.crawlSerpForKeywordAction("kw-1");
    expect(res).toEqual({ ok: true, jobId: "job-1" });
    expect(enqueueAnalyticsJob).toHaveBeenCalledTimes(1);
    expect(enqueueAnalyticsJob).toHaveBeenCalledWith({
      type: "crawl-onpage",
      siteId: "site-a",
      payload: { keywordId: "kw-1" },
    });
  });

  test("crawlSerpForKeywordAction: 409 (job already queued/running) → ok:true, jobId:null", async () => {
    enqueueImpl = async () => {
      throw new AnalyticsJobError(
        "Analytics service rejected the job (409 Conflict)",
        409,
      );
    };
    const res = await actions.crawlSerpForKeywordAction("kw-1");
    expect(res).toEqual({ ok: true, jobId: null });
  });

  test("crawlSerpForKeywordAction: throws on a keyword outside the active org (tenancy unchanged)", async () => {
    await expect(
      actions.crawlSerpForKeywordAction("kw-other-org"),
    ).rejects.toThrow();
    expect(enqueueAnalyticsJob).not.toHaveBeenCalled();
  });
});
