// Integration test — gated on DB_TESTS=1, run against the *_test DB only.
import { beforeEach, describe, expect, mock, test } from "bun:test";
import type { SerpSnapshot } from "../serp-types";
import type { KeywordBrief } from "../ytg-types";

// actions/queries import "server-only" (throws outside Next) — stub it.
mock.module("server-only", () => ({}));
// next/cache primitives throw outside a Next request scope — stub them.
mock.module("next/cache", () => ({
  revalidatePath: () => {},
  revalidateTag: () => {},
  updateTag: () => {},
  unstable_cache:
    (fn: (...args: unknown[]) => unknown) =>
    (...args: unknown[]) =>
      fn(...args),
}));

// The org guard resolves the active org's visible sites via getSitesForActiveOrg
// (needs a session/headers()). Stub it to a fixed set so we can exercise the
// site-membership check deterministically: SITE_A is visible, SITE_B is not.
mock.module("@/lib/sites/access", () => ({
  getSitesForActiveOrg: async () => [{ id: "lustre-lab" }],
}));

// gscConnected est dérivé de l'état GSC DU SITE — hors de portée de ce test
// d'intégration DB (il faudrait une autorité et un compte Google en base) : on
// stubbe « non connecté » pour un bundle stable.
mock.module("@/lib/gsc/site-state", () => ({
  deriveSiteGscState: async () => ({
    state: "no-authority",
    property: null,
    authorityUserId: null,
    authorityAccountId: null,
  }),
}));

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

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

  const SITE_A = "lustre-lab";
  const SITE_B = "atelier-forge";

  const brief: KeywordBrief = {
    source: "ytg",
    query: "alpha",
    language: "French (France)",
    lang: "fr",
    country: "FR",
    importedAt: "2026-06-18T00:00:00.000Z",
    primaryIntent: "Informational",
    objectives: ["Educate readers"],
    keySubjects: ["alpha basics"],
    questions: ["What is alpha?"],
    tone: [],
    style: [],
    valueProps: [],
    topTerms: ["alpha"],
    twoGrams: [],
    threeGrams: [],
    entities: ["Alpha Co"],
    rawBrief: "",
    rawGuide: "",
  };

  const serp: Record<string, SerpSnapshot> = {
    FR: {
      source: "se-ranking",
      importedAt: "2026-06-18T00:00:00.000Z",
      features: ["paa", "video"],
      // crawledAt present → on-page state "ok".
      crawledAt: "2026-06-18T00:00:00.000Z",
      competitors: Array.from({ length: 10 }, (_, i) => ({
        position: i + 1,
        domain: `comp${i + 1}.example`,
        url: `https://comp${i + 1}.example/page`,
        backlinks: 100 + i,
        refDomains: 10 + i,
        pageAuthority: 30 + i,
        searchTraffic: 1000 + i,
        urlKeywords: 50 + i,
        title: `Title ${i + 1}`,
        metaDescription: null,
        lang: "fr",
        images: 5,
        imagesWithoutAlt: 1,
        imageAlts: null,
        missingAltSrcs: null,
        hasJsonLd: true,
        jsonLdTypes: ["Article"],
        crawledAt: "2026-06-18T00:00:00.000Z",
      })),
    },
  };

  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();
    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: "Lustre Lab",
        url: "https://lustrelab.com",
        primaryLanguage: "fr",
        languages: [],
        targetCountries: ["FR"],
      },
      {
        id: SITE_B,
        orgId: "other-org",
        ownerUserId: "seed-owner",
        name: "Atelier Forge",
        url: "https://atelierforge.com",
        primaryLanguage: "fr",
        languages: [],
        targetCountries: ["FR"],
      },
    ]);
  });

  test("happy path: returns the full bundle for a visible keyword", async () => {
    await db.insert(schema.keywords).values({
      id: "k1",
      siteId: SITE_A,
      term: "alpha",
      lang: "fr",
      country: "FR",
      theme: "lab-diamonds",
      status: "idea",
      intent: ["informational"],
      primarySource: "mangools",
      sources: ["mangools", "gsc"],
      metrics: {
        mangools: {
          volume: 1200,
          kd: 42,
          cpc: 1.5,
          growthPct: 5,
          updatedAt: "",
        },
      },
      trend: [],
      briefs: { FR: brief },
      serp,
      gscByCountry: {
        FR: { clicks: 1, impressions: 1, ctr: 1, position: 1, updatedAt: "" },
      },
    });

    const bundle = await actions.getKeywordPromptBundle("k1");

    expect(bundle.id).toBe("k1");
    expect(bundle.term).toBe("alpha");
    expect(bundle.country).toBe("FR");
    expect(bundle.lang).toBe("fr");
    expect(bundle.primarySource).toBe("mangools");
    expect(bundle.sources).toEqual(["mangools", "gsc"]);
    // primary-source metrics
    expect(bundle.vol).toBe(1200);
    expect(bundle.kd).toBe(42);
    expect(bundle.cpc).toBe(1.5);
    // SERP
    expect(bundle.competitors).toHaveLength(10);
    expect(bundle.serpFeatures).toEqual(["paa", "video"]);
    // brief non-null for the keyword's country
    expect(bundle.brief).not.toBeNull();
    expect(bundle.brief?.query).toBe("alpha");
    // GSC overlay present (per-country)
    expect(bundle.hasGsc).toBe(true);
    // no serp-semantics analysis stored for this keyword
    expect(bundle.semanticProfile).toBeNull();
    // mapped readiness: serp ok→ready, onpage ok (crawledAt)→ready, brief ok→ready
    expect(bundle.readiness).toEqual({
      serp: "ready",
      onpage: "ready",
      brief: "ready",
    });
  });

  test("readiness maps partial/other-locale/none states", async () => {
    // No crawledAt → onpage "partial"; brief only in another country → "other-locale".
    const noCrawl: Record<string, SerpSnapshot> = {
      FR: { ...serp.FR, crawledAt: undefined },
    };
    await db.insert(schema.keywords).values({
      id: "k2",
      siteId: SITE_A,
      term: "bravo",
      lang: "fr",
      country: "FR",
      status: "idea",
      intent: ["commercial"],
      primarySource: "manual",
      sources: ["manual"],
      metrics: {},
      trend: [],
      briefs: { US: { ...brief, country: "US" } },
      serp: noCrawl,
    });

    const bundle = await actions.getKeywordPromptBundle("k2");
    expect(bundle.vol).toBeNull();
    expect(bundle.kd).toBeNull();
    expect(bundle.cpc).toBeNull();
    expect(bundle.brief).toBeNull(); // no brief for FR
    expect(bundle.hasGsc).toBe(false);
    expect(bundle.readiness).toEqual({
      serp: "ready", // competitors present
      onpage: "progress", // snapshot but no crawledAt
      brief: "progress", // brief exists for another locale
    });
  });

  test("missing readiness when no serp/brief at all", async () => {
    await db.insert(schema.keywords).values({
      id: "k3",
      siteId: SITE_A,
      term: "charlie",
      lang: "fr",
      country: "FR",
      status: "idea",
      intent: ["informational"],
      primarySource: "manual",
      sources: ["manual"],
      metrics: {},
      trend: [],
      briefs: {},
      serp: {},
    });
    const bundle = await actions.getKeywordPromptBundle("k3");
    expect(bundle.competitors).toEqual([]);
    expect(bundle.serpFeatures).toEqual([]);
    expect(bundle.readiness).toEqual({
      serp: "missing",
      onpage: "missing",
      brief: "missing",
    });
  });

  test("org isolation: a keyword on another org's site throws", async () => {
    await db.insert(schema.keywords).values({
      id: "kx",
      siteId: SITE_B, // not in getSitesForActiveOrg() stub
      term: "foreign",
      lang: "fr",
      country: "FR",
      status: "idea",
      intent: ["informational"],
      primarySource: "manual",
      sources: ["manual"],
      metrics: {},
      trend: [],
    });
    await expect(actions.getKeywordPromptBundle("kx")).rejects.toThrow();
  });

  test("missing keyword throws", async () => {
    await expect(actions.getKeywordPromptBundle("nope")).rejects.toThrow();
  });
});
