// src/lib/seranking/actions.test.ts
import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test";

// actions.ts calls updateTag on a successful sync — pass-through stub (même
// pattern que sync.test.ts/balance.test.ts).
mock.module("next/cache", () => ({
  revalidatePath: () => {},
  revalidateTag: () => {},
  updateTag: () => {},
  unstable_cache: (fn: unknown) => fn,
}));

// La garde tenancy réelle (assertSiteInActiveOrg) a besoin d'une session —
// mockée pour piloter le gate "site étranger" sans session/headers (même
// approche que src/lib/keywords/actions.test.ts). ALLOWED_SITES : liste des
// ids qui passent le gate tenancy — tout id absent lève, comme le ferait la
// vraie garde pour un site hors org active.
const ALLOWED_SITES = new Set<string>();
mock.module("@/lib/sites/assert-site", () => ({
  assertSiteInActiveOrg: async (siteId: string) => {
    if (!ALLOWED_SITES.has(siteId)) {
      throw new Error("Site does not belong to the active organization");
    }
  },
}));

import { eq } from "drizzle-orm";
import { env } from "@/env";
import { assertTestDb } from "@/lib/db/assert-test-db";
import { organization } from "@/lib/db/auth-schema";
import { db } from "@/lib/db/client";
import { keywords, sites } from "@/lib/db/domain-schema";
import { estimateSerankingSyncAction, syncSerankingAction } from "./actions";
import { SerankingError, serankingErrorMessage } from "./client";

describe("serankingErrorMessage", () => {
  test("maps every kind to a French user message", () => {
    expect(
      serankingErrorMessage(new SerankingError("insufficient-credits", "x")),
    ).toContain("Crédits");
    expect(
      serankingErrorMessage(new SerankingError("rate-limited", "x")),
    ).toContain("débit");
    expect(serankingErrorMessage(new SerankingError("auth", "x"))).toContain(
      "refusée",
    );
    expect(
      serankingErrorMessage(new SerankingError("invalid-source", "x")),
    ).toContain("régionale");
    expect(serankingErrorMessage(new SerankingError("http", "x"))).toContain(
      "Échec",
    );
    expect(serankingErrorMessage(new SerankingError("network", "x"))).toContain(
      "Échec",
    );
    expect(
      serankingErrorMessage(new SerankingError("no-syncs", "x")),
    ).toContain("Solde de syncs épuisé");
  });
});

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

describe.skipIf(!RUN)("seranking actions — gates (postgres)", () => {
  const ORG = "org-srk-actions-test";
  const SITE = "site-srk-actions-test";
  const KW = `${SITE}-kw1`;
  const KW_SKIPPED = `${SITE}-kw2`;
  const FOREIGN_SITE = "site-srk-actions-foreign";
  const GHOST_SITE = "site-srk-actions-ghost"; // passe le gate tenancy, absent de `sites`

  async function cleanup() {
    await db.delete(keywords).where(eq(keywords.siteId, SITE));
    await db.delete(sites).where(eq(sites.id, SITE));
    await db.delete(organization).where(eq(organization.id, ORG));
  }

  async function setBalance(balance: number): Promise<void> {
    await db
      .update(organization)
      .set({ serankingSyncBalance: balance })
      .where(eq(organization.id, ORG));
  }

  beforeAll(async () => {
    await assertTestDb();
    await cleanup();
    ALLOWED_SITES.add(SITE);
    ALLOWED_SITES.add(GHOST_SITE);
    await db.insert(organization).values({
      id: ORG,
      name: "srk-actions-test",
      slug: "srk-actions-test",
      createdAt: new Date(),
      serankingSyncBalance: 0,
    });
    await db.insert(sites).values({
      id: SITE,
      orgId: ORG,
      name: "srk-actions",
      primaryLanguage: "fr",
      targetCountries: ["FR"],
    });
    await db.insert(keywords).values([
      {
        id: KW,
        siteId: SITE,
        term: "plombier paris",
        lang: "fr",
        country: "FR",
        status: "idea",
        intent: [],
        primarySource: "manual",
        sources: ["manual"],
        metrics: {},
        trend: [],
      },
      {
        id: KW_SKIPPED,
        siteId: SITE,
        term: "foo",
        lang: "fr",
        country: "XK", // pas de base SE-Ranking → exclu de l'estimation
        status: "idea",
        intent: [],
        primarySource: "manual",
        sources: ["manual"],
        metrics: {},
        trend: [],
      },
    ]);
  });

  afterAll(async () => {
    await cleanup();
    mock.restore();
  });

  test("env off → les deux actions refusent AVANT tout gate tenancy", async () => {
    expect(env.serankingEnabled).toBe(true); // précondition : SERANKING_API_KEY configurée
    env.serankingEnabled = false;
    try {
      const est = await estimateSerankingSyncAction(SITE, {
        mode: "ids",
        ids: [KW],
      });
      expect(est).toEqual({
        ok: false,
        error: "SE-Ranking n'est pas configuré.",
      });
      const sync = await syncSerankingAction(SITE, { mode: "ids", ids: [KW] });
      expect(sync).toEqual({
        ok: false,
        error: "SE-Ranking n'est pas configuré.",
      });
    } finally {
      env.serankingEnabled = true;
    }
  });

  test("site étranger → le gate tenancy propage (les deux actions)", async () => {
    await expect(
      estimateSerankingSyncAction(FOREIGN_SITE, { mode: "ids", ids: [KW] }),
    ).rejects.toThrow();
    await expect(
      syncSerankingAction(FOREIGN_SITE, { mode: "ids", ids: [KW] }),
    ).rejects.toThrow();
  });

  test("sélection vide → 'Sélection vide.' (les deux actions)", async () => {
    const est = await estimateSerankingSyncAction(SITE, {
      mode: "ids",
      ids: [],
    });
    expect(est).toEqual({ ok: false, error: "Sélection vide." });
    const sync = await syncSerankingAction(SITE, { mode: "ids", ids: [] });
    expect(sync).toEqual({ ok: false, error: "Sélection vide." });
  });

  test("site introuvable (tenancy passe le mock, absent de `sites`) → 'Site introuvable.'", async () => {
    const est = await estimateSerankingSyncAction(GHOST_SITE, {
      mode: "ids",
      ids: [KW],
    });
    expect(est).toEqual({ ok: false, error: "Site introuvable." });
    const sync = await syncSerankingAction(GHOST_SITE, {
      mode: "ids",
      ids: [KW],
    });
    expect(sync).toEqual({ ok: false, error: "Site introuvable." });
  });

  test("estimateSerankingSyncAction : happy path metered — keywords/requests/pays sautés + accès", async () => {
    await setBalance(7);
    const res = await estimateSerankingSyncAction(SITE, {
      mode: "ids",
      ids: [KW, KW_SKIPPED],
    });
    expect(res).toEqual({
      ok: true,
      keywords: 1,
      requests: 1,
      countries: 1,
      skippedCountries: ["XK"],
      access: { mode: "metered", balance: 7 },
    });
  });

  test("syncSerankingAction : metered balance 0 → refus AVANT tout appel", async () => {
    await setBalance(0);
    const res = await syncSerankingAction(SITE, { mode: "ids", ids: [KW] });
    expect(res).toEqual({
      ok: false,
      error: "Solde de syncs épuisé — recharge depuis Facturation.",
    });
    // aucune consommation n'a eu lieu (refus avant tout appel).
    const [org] = await db
      .select({ b: organization.serankingSyncBalance })
      .from(organization)
      .where(eq(organization.id, ORG));
    expect(org.b).toBe(0);
  });
});
