// src/lib/backlink/research.test.ts — integration, gated on DB_TESTS=1.
import { beforeEach, describe, expect, mock, test } from "bun:test";

// server-only throws outside Next.js; stub it for the test runner.
mock.module("server-only", () => ({}));
// actions call revalidatePath, which throws outside a Next request scope.
mock.module("next/cache", () => ({
  revalidatePath: () => {},
  revalidateTag: () => {},
  updateTag: () => {},
  unstable_cache: (fn: unknown) => fn,
}));
// the tenancy guard needs a session (headers()) — stub it; site-scoping is
// covered at the SQL level by these tests and by assert-site.test.ts.
mock.module("@/lib/sites/assert-site", () => ({
  siteBelongsToOrg: async () => true,
  assertSiteInActiveOrg: async () => {},
}));
// linkSourceId tenancy guard resolves the active org from the session — stub
// it to the demo org (source-scoping is covered at the SQL level below and
// by assert-link-source.test.ts).
mock.module("@/lib/active-org", () => ({
  getActiveOrgId: async () => "demo-org",
  getActiveUserId: async () => "seed-owner",
}));

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

describe.skipIf(!RUN_DB)("backlink research pool + actions (postgres)", () => {
  let db: typeof import("@/lib/db/client").db;
  let schema: typeof import("@/lib/db/domain-schema");
  const ORG = "demo-org"; // guaranteed by db:seed / test DB setup
  const OTHER_ORG = "wt-research-other-org";
  const SITE_A = "wt-research-a";
  const SITE_B = "wt-research-b";

  async function insertSource(
    id: string,
    overrides: Record<string, unknown> = {},
  ) {
    await db.insert(schema.linkSources).values({
      id,
      orgId: ORG,
      platform: id,
      url: `https://${id.toLowerCase()}.com`,
      domainNorm: `${id.toLowerCase()}.com`,
      kind: "blog",
      da: 40,
      effort: "low",
      linkType: "dofollow",
      language: "en",
      ...overrides,
    } as never);
  }

  beforeEach(async () => {
    ({ db } = await import("@/lib/db/client"));
    schema = await import("@/lib/db/domain-schema");
    const authSchema = await import("@/lib/db/auth-schema");
    const { assertTestDb } = await import("@/lib/db/assert-test-db");
    const { inArray, eq } = await import("drizzle-orm");
    await assertTestDb();
    await db.delete(schema.backlinks);
    await db.delete(schema.linkSources);
    await db.delete(schema.domainRatings);
    await db
      .delete(schema.sites)
      .where(inArray(schema.sites.id, [SITE_A, SITE_B]));
    // Foreign org for the cross-org linkSourceId tenancy test (FK target of
    // link_sources.org_id) — cascades to link_sources on cleanup.
    await db
      .delete(authSchema.organization)
      .where(eq(authSchema.organization.id, OTHER_ORG));
    await db.insert(authSchema.organization).values({
      id: OTHER_ORG,
      name: "WT Research Other Org",
      slug: OTHER_ORG,
      createdAt: new Date(),
    });
    await db.insert(schema.sites).values([
      {
        id: SITE_A,
        orgId: ORG,
        ownerUserId: "seed-owner",
        name: "WT Research A",
        url: "https://wt-research-a.com",
        primaryLanguage: "fr",
        languages: [],
        targetCountries: ["FR"],
      },
      {
        id: SITE_B,
        orgId: ORG,
        ownerUserId: "seed-owner",
        name: "WT Research B",
        url: "https://wt-research-b.com",
        primaryLanguage: "fr",
        languages: [],
        targetCountries: ["FR"],
      },
    ]);
  });

  test("pool scoré : footprint, targetedByCurrent, currentStatus, score", async () => {
    await insertSource("S1", { domainNorm: "s1.com" });
    await db
      .insert(schema.domainRatings)
      .values({ domainNorm: "s1.com", dr: 90 });
    await db.insert(schema.backlinks).values([
      { id: "b1", linkSourceId: "S1", siteId: SITE_A, status: "prospect" },
      { id: "b2", linkSourceId: "S1", siteId: SITE_B, status: "live" },
    ]);

    const { getResearchPool } = await import("./research");
    const rows = await getResearchPool(ORG, SITE_A);
    const s1 = rows.find((r) => r.id === "S1");
    expect(s1).toBeDefined();
    expect(s1?.footprint).toBe(2);
    expect(s1?.alert).toBe(false);
    expect(s1?.targetedByCurrent).toBe(true);
    expect(s1?.currentStatus).toBe("prospect");
    expect(s1?.score).toBe(27); // en vs fr → 0.3 × dr 90 = 27
  });

  test("assignProspectsAction N×M avec dédup", async () => {
    await insertSource("S1");
    await insertSource("S2");
    const { assignProspectsAction } = await import("./research-actions");
    const r1 = await assignProspectsAction(["S1", "S2"], [SITE_A, SITE_B]);
    expect(r1).toEqual({ created: 4, skipped: 0 });
    const r2 = await assignProspectsAction(["S1", "S2"], [SITE_A, SITE_B]);
    expect(r2).toEqual({ created: 0, skipped: 4 });
  });

  test("assignProspectsAction — un linkSourceId d'une autre org est filtré (IDOR cross-org, audit B1)", async () => {
    await insertSource("S1"); // ORG (demo-org)
    await insertSource("S-OTHER", { orgId: OTHER_ORG });
    const { assignProspectsAction } = await import("./research-actions");
    const r = await assignProspectsAction(["S1", "S-OTHER"], [SITE_A]);
    // Seul S1 (org active) donne lieu à un insert ; S-OTHER est écarté avant
    // toute écriture — sans le guard, ce test créerait 2 lignes.
    expect(r).toEqual({ created: 1, skipped: 0 });
    const { eq } = await import("drizzle-orm");
    const rows = await db
      .select({ linkSourceId: schema.backlinks.linkSourceId })
      .from(schema.backlinks)
      .where(eq(schema.backlinks.siteId, SITE_A));
    expect(rows.map((r) => r.linkSourceId)).toEqual(["S1"]);
  });

  test("pushProspectsToPlanningAction ne touche que les prospects", async () => {
    await insertSource("S1");
    await insertSource("S2");
    await db.insert(schema.backlinks).values([
      { id: "b1", linkSourceId: "S1", siteId: SITE_A, status: "prospect" },
      { id: "b2", linkSourceId: "S2", siteId: SITE_A, status: "live" },
    ]);
    const { pushProspectsToPlanningAction } = await import(
      "./research-actions"
    );
    const r = await pushProspectsToPlanningAction(SITE_A, ["S1", "S2"]);
    expect(r).toEqual({ updated: 1 });

    const { eq } = await import("drizzle-orm");
    const rows = await db
      .select({
        linkSourceId: schema.backlinks.linkSourceId,
        status: schema.backlinks.status,
      })
      .from(schema.backlinks)
      .where(eq(schema.backlinks.siteId, SITE_A));
    const byId = new Map(rows.map((r) => [r.linkSourceId, r.status]));
    expect(byId.get("S1")).toBe("planned");
    expect(byId.get("S2")).toBe("live"); // intact
  });
});
