// src/lib/backlink/actions.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 actions — addSourcesToSiteAction (postgres)",
  () => {
    let db: typeof import("@/lib/db/client").db;
    let schema: typeof import("@/lib/db/domain-schema");
    let authSchema: typeof import("@/lib/db/auth-schema");
    const ORG = "demo-org"; // matches the @/lib/active-org stub
    const OTHER_ORG = "bl-actions-other-org";
    const SITE = "wt-actions-site";

    async function insertSource(
      id: string,
      orgId: string,
      overrides: Record<string, unknown> = {},
    ) {
      await db.insert(schema.linkSources).values({
        id,
        orgId,
        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");
      authSchema = await import("@/lib/db/auth-schema");
      const { assertTestDb } = await import("@/lib/db/assert-test-db");
      const { eq } = await import("drizzle-orm");
      await assertTestDb();
      await db.delete(schema.backlinks);
      await db.delete(schema.linkSources);
      await db.delete(schema.sites).where(eq(schema.sites.id, SITE));
      // 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: "BL Actions Other Org",
        slug: OTHER_ORG,
        createdAt: new Date(),
      });
      await db.insert(schema.sites).values({
        id: SITE,
        orgId: ORG,
        ownerUserId: "seed-owner",
        name: "WT Actions Site",
        url: "https://wt-actions-site.com",
        primaryLanguage: "fr",
        languages: [],
        targetCountries: ["FR"],
      });
    });

    test("addSourcesToSiteAction — insère une source de l'org active", async () => {
      await insertSource("S1", ORG);
      const { addSourcesToSiteAction } = await import("./actions");
      await addSourcesToSiteAction(["S1"], SITE);
      const { eq } = await import("drizzle-orm");
      const rows = await db
        .select({ linkSourceId: schema.backlinks.linkSourceId })
        .from(schema.backlinks)
        .where(eq(schema.backlinks.siteId, SITE));
      expect(rows.map((r) => r.linkSourceId)).toEqual(["S1"]);
    });

    test("addSourcesToSiteAction — un linkSourceId d'une autre org est filtré (IDOR cross-org, audit B1)", async () => {
      await insertSource("S1", ORG);
      await insertSource("S-OTHER", OTHER_ORG);
      const { addSourcesToSiteAction } = await import("./actions");
      // Sans le guard, ceci créerait 2 lignes — la seconde exposerait S-OTHER
      // (domaine/URL/DA de l'org B) au travers des vues backlink de l'org A.
      await addSourcesToSiteAction(["S1", "S-OTHER"], SITE);
      const { eq } = await import("drizzle-orm");
      const rows = await db
        .select({ linkSourceId: schema.backlinks.linkSourceId })
        .from(schema.backlinks)
        .where(eq(schema.backlinks.siteId, SITE));
      expect(rows.map((r) => r.linkSourceId)).toEqual(["S1"]);
    });

    test("addSourcesToSiteAction — n'écrit rien si tous les ids sont hors org", async () => {
      await insertSource("S-OTHER", OTHER_ORG);
      const { addSourcesToSiteAction } = await import("./actions");
      await addSourcesToSiteAction(["S-OTHER"], SITE);
      const { eq } = await import("drizzle-orm");
      const rows = await db
        .select({ linkSourceId: schema.backlinks.linkSourceId })
        .from(schema.backlinks)
        .where(eq(schema.backlinks.siteId, SITE));
      expect(rows).toEqual([]);
    });
  },
);
