// src/lib/seranking/integration-actions.test.ts — status + save/disconnect de
// la clé BYO SE-Ranking (postgres, section gated DB_TESTS=1, même pattern que
// balance.test.ts). Les 9 tests DB tournent contre les fonctions core
// (integration-core.ts, `server-only`, DI sûre) — gates (canManage) et orgId
// injectés via `deps`, pas de mock.module auth ; seul next/cache est stubbé
// (isAddonEnabled est unstable_cache'd, jette hors d'un scope requête Next).
// Un test statique séparé, hors du gate DB_TESTS, vérifie que les wrappers
// "use server" exportés par integration-actions.ts n'exposent aucun `deps`
// injectable (surface réseau — voir integration-actions.ts).
import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test";

mock.module("next/cache", () => ({
  revalidatePath: () => {},
  revalidateTag: () => {},
  updateTag: () => {},
  unstable_cache: (fn: unknown) => fn,
}));

import { eq } from "drizzle-orm";
import { assertTestDb } from "@/lib/db/assert-test-db";
import { organization } from "@/lib/db/auth-schema";
import { db } from "@/lib/db/client";
import { orgAddons } from "@/lib/db/domain-schema";
import {
  getSerankingIntegrationStatusAction,
  saveSerankingApiKeyAction,
} from "./integration-actions";
import {
  getSerankingIntegrationStatus,
  saveSerankingApiKey,
} from "./integration-core";

describe("integration-actions wrappers — pas de deps injectable (surface réseau)", () => {
  test("getSerankingIntegrationStatusAction n'accepte aucun paramètre", () => {
    expect(getSerankingIntegrationStatusAction.length).toBe(0);
  });

  test("saveSerankingApiKeyAction n'accepte que `key` (pas de deps)", () => {
    expect(saveSerankingApiKeyAction.length).toBe(1);
  });
});

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

describe.skipIf(!RUN)("seranking integration-actions (postgres)", () => {
  const ORG = "org-srk-integration-test";
  const orgId = async () => ORG;

  async function cleanup() {
    // FK org_addons.orgId → organization.id ON DELETE CASCADE : supprimer
    // l'org suffit à faire disparaître ses add-ons.
    await db.delete(organization).where(eq(organization.id, ORG));
  }

  async function resetOrg(overrides: {
    serankingApiKey?: string | null;
    addonActive?: boolean;
  }): Promise<void> {
    await cleanup();
    await db.insert(organization).values({
      id: ORG,
      name: "srk-integration-test",
      slug: "srk-integration-test",
      createdAt: new Date(),
      serankingApiKey: overrides.serankingApiKey ?? null,
    });
    if (overrides.addonActive) {
      await db
        .insert(orgAddons)
        .values({ orgId: ORG, addon: "seranking_byok" });
    }
  }

  async function readApiKey(): Promise<string | null> {
    const [row] = await db
      .select({ apiKey: organization.serankingApiKey })
      .from(organization)
      .where(eq(organization.id, ORG));
    return row?.apiKey ?? null;
  }

  beforeAll(async () => {
    await assertTestDb();
    await cleanup(); // reste d'un run précédent crashé
  });

  afterAll(async () => {
    await cleanup();
  });

  describe("getSerankingIntegrationStatusAction", () => {
    test("expose canManage, addonActive, hasKey — jamais la clé, unitsLeft toujours null", async () => {
      await resetOrg({ serankingApiKey: "srk-key-abc", addonActive: true });
      const status = await getSerankingIntegrationStatus({
        canManage: async () => true,
        orgId,
      });
      expect(status).toEqual({
        canManage: true,
        addonActive: true,
        hasKey: true,
        unitsLeft: null,
      });
      expect(JSON.stringify(status)).not.toContain("srk-key-abc");
    });

    test("sans clé ni add-on → hasKey/addonActive false", async () => {
      await resetOrg({});
      const status = await getSerankingIntegrationStatus({
        canManage: async () => false,
        orgId,
      });
      expect(status).toEqual({
        canManage: false,
        addonActive: false,
        hasKey: false,
        unitsLeft: null,
      });
    });
  });

  describe("saveSerankingApiKeyAction", () => {
    test("gate refusée (member) → erreur, rien n'est écrit", async () => {
      await resetOrg({ addonActive: true });
      const res = await saveSerankingApiKey("srk-new-key", {
        canManage: async () => false,
        orgId,
      });
      expect(res).toEqual({
        ok: false,
        error: "Action réservée au propriétaire ou à un administrateur.",
      });
      expect(await readApiKey()).toBeNull();
    });

    test("add-on inactif → « Active d'abord l'add-on SE-Ranking. »", async () => {
      await resetOrg({ addonActive: false });
      const res = await saveSerankingApiKey("srk-new-key", {
        canManage: async () => true,
        orgId,
      });
      expect(res).toEqual({
        ok: false,
        error: "Active d'abord l'add-on SE-Ranking.",
      });
      expect(await readApiKey()).toBeNull();
    });

    test("clé refusée par le ping (403) → « Clé SE-Ranking refusée. », rien n'est stocké", async () => {
      await resetOrg({ addonActive: true });
      const res = await saveSerankingApiKey("srk-bad-key", {
        canManage: async () => true,
        orgId,
        ping: async () => {
          throw new Error("SE-Ranking ping HTTP 403");
        },
      });
      expect(res).toEqual({ ok: false, error: "Clé SE-Ranking refusée." });
      expect(await readApiKey()).toBeNull();
    });

    test("clé valide → stockée + unitsLeft retourné", async () => {
      await resetOrg({ addonActive: true });
      const res = await saveSerankingApiKey("srk-good-key", {
        canManage: async () => true,
        orgId,
        ping: async () => 4200,
      });
      expect(res).toEqual({ ok: true, unitsLeft: 4200 });
      expect(await readApiKey()).toBe("srk-good-key");
    });

    test("clé TRIMÉE avant stockage — espaces autour ignorés", async () => {
      await resetOrg({ addonActive: true });
      const res = await saveSerankingApiKey("  srk-trim-key  ", {
        canManage: async () => true,
        orgId,
        ping: async () => 10,
      });
      expect(res).toEqual({ ok: true, unitsLeft: 10 });
      expect(await readApiKey()).toBe("srk-trim-key");
    });

    test("null → colonne vidée (déconnexion), autorisée même add-on inactif", async () => {
      await resetOrg({ serankingApiKey: "srk-old-key", addonActive: false });
      const res = await saveSerankingApiKey(null, {
        canManage: async () => true,
        orgId,
      });
      expect(res).toEqual({ ok: true, unitsLeft: null });
      expect(await readApiKey()).toBeNull();
    });

    test("chaîne vide/espaces-seulement → traitée comme null (déconnexion)", async () => {
      await resetOrg({ serankingApiKey: "srk-old-key", addonActive: true });
      const res = await saveSerankingApiKey("   ", {
        canManage: async () => true,
        orgId,
      });
      expect(res).toEqual({ ok: true, unitsLeft: null });
      expect(await readApiKey()).toBeNull();
    });
  });
});
