// src/lib/semantic/actions.test.ts — buildSerpSemanticsPayload est pure
// (no_serp/curation/clamp testés sans DB ni mock). Les chemins action
// (tenancy) sont mirror du harnais DB de serp-actions.test.ts, gated
// DB_TESTS=1 sur la *_test DB. enqueueAnalyticsJob n'est volontairement pas
// mocké : aucun test ici n'atteint le chemin "ok" qui l'appellerait (mémoire
// projet : pas de `mock.module` global pour la DI — on préfère court-circuiter
// avant l'appel réseau plutôt que stubber le module).
import { beforeEach, describe, expect, mock, test } from "bun:test";
import { eq } from "drizzle-orm";
import { AnalyticsJobError } from "@/lib/analytics/enqueue";
import type { SerpSnapshot } from "@/lib/keywords/serp-types";
import type { StoredSemanticProfile } from "./profile-types";

// Same mocks as serp-actions.test.ts: `server-only` throws unless the
// `react-server` condition resolves it to a no-op (the `test` script passes
// `--conditions=react-server`, but keep the mock as a defense when this file
// runs via a bare `bun test`). `getActiveOrgId` backs both the tenancy guard
// (assertKeywordInActiveOrg) and the D6 hook read in startSerpSemanticsAction;
// "demo-org" matches the seeded org below, so the real guard runs against the
// test DB.
mock.module("server-only", () => ({}));
mock.module("@/lib/active-org", () => ({
  getActiveOrgId: async () => "demo-org",
  getActiveUserId: async () => "seed-owner",
}));

const baseCompetitor = {
  domain: "example.com",
  backlinks: null,
  refDomains: null,
  pageAuthority: null,
  searchTraffic: null,
  urlKeywords: null,
  title: null,
  metaDescription: null,
  lang: null,
  images: null,
  imagesWithoutAlt: null,
  imageAlts: null,
  missingAltSrcs: null,
  hasJsonLd: null,
  jsonLdTypes: null,
  crawledAt: null,
};

function snapshot(
  competitors: { position: number; url: string }[],
): SerpSnapshot {
  return {
    source: "se-ranking",
    importedAt: "2026-07-01T00:00:00.000Z",
    features: [],
    competitors: competitors.map((c) => ({
      ...baseCompetitor,
      position: c.position,
      url: c.url,
    })),
  };
}

describe("buildSerpSemanticsPayload (pure)", () => {
  test("no_serp : aucun snapshot pour le pays de référence → null", async () => {
    const { buildSerpSemanticsPayload } = await import("./serp-payload");
    expect(
      buildSerpSemanticsPayload("kw-1", { serp: {}, country: "FR" }),
    ).toBeNull();
    expect(
      buildSerpSemanticsPayload("kw-1", {
        serp: { US: snapshot([{ position: 1, url: "https://a.com" }]) },
        country: "FR",
      }),
    ).toBeNull();
  });

  test("curation : excludedPositions marque included:false, le reste included:true", async () => {
    const { buildSerpSemanticsPayload } = await import("./serp-payload");
    const row = {
      serp: {
        FR: snapshot([
          { position: 1, url: "https://a.com" },
          { position: 2, url: "https://b.com" },
          { position: 3, url: "https://c.com" },
        ]),
      },
      country: "FR",
    };
    const payload = buildSerpSemanticsPayload("kw-1", row, {
      excludedPositions: [2],
    });
    expect(payload).not.toBeNull();
    expect(payload?.urls).toEqual([
      { position: 1, url: "https://a.com", included: true },
      { position: 2, url: "https://b.com", included: false },
      { position: 3, url: "https://c.com", included: true },
    ]);
  });

  test("clamp : les positions hors [1,10] sont filtrées", async () => {
    const { buildSerpSemanticsPayload } = await import("./serp-payload");
    const row = {
      serp: {
        FR: snapshot([
          { position: 1, url: "https://a.com" },
          { position: 10, url: "https://j.com" },
          { position: 11, url: "https://k.com" },
        ]),
      },
      country: "FR",
    };
    const payload = buildSerpSemanticsPayload("kw-1", row);
    expect(payload?.urls.map((u) => u.position)).toEqual([1, 10]);
  });

  test("clientUrl est propagé quand fourni, absent sinon", async () => {
    const { buildSerpSemanticsPayload } = await import("./serp-payload");
    const row = {
      serp: { FR: snapshot([{ position: 1, url: "https://a.com" }]) },
      country: "FR",
    };
    expect(
      buildSerpSemanticsPayload("kw-1", row, { clientUrl: "https://mine.com" })
        ?.clientUrl,
    ).toBe("https://mine.com");
    expect(buildSerpSemanticsPayload("kw-1", row)?.clientUrl).toBeUndefined();
  });
});

describe("mapEnqueueError (pure)", () => {
  test("AnalyticsJobError status 409 → already_running", async () => {
    const { mapEnqueueError } = await import("./serp-payload");
    expect(
      mapEnqueueError(
        new AnalyticsJobError(
          "Analytics service rejected the job (409 Conflict)",
          409,
        ),
      ),
    ).toBe("already_running");
  });

  test("AnalyticsJobError autre status → enqueue_failed", async () => {
    const { mapEnqueueError } = await import("./serp-payload");
    expect(
      mapEnqueueError(
        new AnalyticsJobError(
          "Analytics service rejected the job (500 Internal Server Error)",
          500,
        ),
      ),
    ).toBe("enqueue_failed");
  });

  test("erreur non-AnalyticsJobError (réseau, throw arbitraire) → enqueue_failed", async () => {
    const { mapEnqueueError } = await import("./serp-payload");
    expect(mapEnqueueError(new Error("fetch failed"))).toBe("enqueue_failed");
    expect(mapEnqueueError("boom")).toBe("enqueue_failed");
  });
});

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

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

    beforeEach(async () => {
      ({ db } = await import("@/lib/db/client"));
      schema = await import("@/lib/db/domain-schema");
      actions = await import("./actions");
      const { assertTestDb } = await import("@/lib/db/assert-test-db");
      await assertTestDb();
      const { __setTestCaller } = await import("@/lib/sites/access");
      __setTestCaller({
        userId: "seed-owner",
        orgId: "demo-org",
        role: "owner",
      });

      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("no_serp : mot-clé sans snapshot SERP renvoie ok:false", async () => {
      const res = await actions.startSerpSemanticsAction("kw-1");
      expect(res).toEqual({ ok: false, error: "no_serp" });
    });

    test("no_serp : snapshot présent mais 0 URL après clamp (toutes positions > 10)", async () => {
      await db
        .update(schema.keywords)
        .set({
          serp: {
            FR: {
              source: "se-ranking",
              importedAt: "2026-07-01T00:00:00.000Z",
              features: [],
              competitors: [
                { ...baseCompetitor, position: 11, url: "https://k.com" },
                { ...baseCompetitor, position: 12, url: "https://l.com" },
              ],
            },
          },
        })
        .where(eq(schema.keywords.id, "kw-1"));

      const res = await actions.startSerpSemanticsAction("kw-1");
      expect(res).toEqual({ ok: false, error: "no_serp" });
    });

    test("tenancy : startSerpSemanticsAction throw sur un mot-clé d'une autre org", async () => {
      await expect(
        actions.startSerpSemanticsAction("kw-other-org"),
      ).rejects.toThrow();
    });

    test("tenancy : getSemanticProfileAction throw sur un mot-clé d'une autre org", async () => {
      await expect(
        actions.getSemanticProfileAction("kw-other-org"),
      ).rejects.toThrow();
    });

    test("getSemanticProfileAction renvoie null sans profil stocké, puis le profil mergé", async () => {
      expect(await actions.getSemanticProfileAction("kw-1")).toBeNull();

      const stored: StoredSemanticProfile = {
        intent: { curve: "standard", mode: "inferred", mixedIntent: false },
        coverage: {
          analyzed: 1,
          total: 1,
          excludedByUser: [],
          missing: [],
          flags: [],
        },
        terms: [],
        ngrams: { bigrams: [], trigrams: [] },
        questions: [],
        structure: {
          wordCount: { p25: 0, median: 0, p75: 0 },
          h2Count: 0,
          lists: 0,
          tables: 0,
          schemaTypes: {},
          archetypes: {},
          top3VsTop10: {
            wordCountTop3: 0,
            wordCountTop10: 0,
            h2CountTop3: 0,
            h2CountTop10: 0,
          },
        },
        guide: {
          terms: [],
          ngrams: [],
          questions: [],
          objectives: {
            wordCount: { p25: 0, median: 0, p75: 0 },
            h2Count: 0,
            lists: 0,
            tables: 0,
          },
        },
        gap: null,
        analyzedAt: "2026-07-21T00:00:00.000Z",
        jobId: "job-1",
        exports: {
          pagesKey:
            "serp-semantics-results/site-a/kw-1/FR/job-1/pages.jsonl.gz",
          termsKey:
            "serp-semantics-results/site-a/kw-1/FR/job-1/terms.jsonl.gz",
        },
      };
      await db
        .update(schema.keywords)
        .set({ semantic: { FR: stored } })
        .where(eq(schema.keywords.id, "kw-1"));

      const profile = await actions.getSemanticProfileAction("kw-1");
      expect(profile?.jobId).toBe("job-1");
      expect(profile?.analyzedAt).toBe("2026-07-21T00:00:00.000Z");
    });
  },
);
