// src/lib/seranking/balance.test.ts — accès SE-Ranking (unlimited/byok/metered)
// + consommation atomique du solde de syncs (postgres, section gated DB_TESTS=1,
// même pattern que sync.test.ts).
import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test";

// isAddonEnabled (entitlements.ts) est unstable_cache'd — le vrai next/cache
// jette hors d'un scope requête Next (pas d'incrementalCache), stub
// pass-through (même pattern que sync.test.ts).
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 {
  consumeSync,
  creditSyncs,
  getSerankingAccess,
  SERANKING_BYOK_ADDON,
} from "./balance";

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

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

  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: {
    serankingUnlimited?: boolean;
    serankingSyncBalance?: number;
    serankingApiKey?: string | null;
  }): Promise<void> {
    await cleanup();
    await db.insert(organization).values({
      id: ORG,
      name: "srk-balance-test",
      slug: "srk-balance-test",
      createdAt: new Date(),
      serankingUnlimited: overrides.serankingUnlimited ?? false,
      serankingSyncBalance: overrides.serankingSyncBalance ?? 0,
      serankingApiKey: overrides.serankingApiKey ?? null,
    });
  }

  async function readBalance(): Promise<number> {
    const [row] = await db
      .select({ b: organization.serankingSyncBalance })
      .from(organization)
      .where(eq(organization.id, ORG));
    return row.b;
  }

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

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

  describe("getSerankingAccess", () => {
    test("org unlimited → mode unlimited même avec balance 0", async () => {
      await resetOrg({ serankingUnlimited: true, serankingSyncBalance: 0 });
      const access = await getSerankingAccess(ORG);
      expect(access).toEqual({ mode: "unlimited" });
    });

    test("add-on actif + clé renseignée → byok", async () => {
      await resetOrg({ serankingApiKey: "srk-key-abc" });
      const { orgAddons } = await import("@/lib/db/domain-schema");
      await db
        .insert(orgAddons)
        .values({ orgId: ORG, addon: SERANKING_BYOK_ADDON });
      const access = await getSerankingAccess(ORG);
      expect(access).toEqual({ mode: "byok", apiKey: "srk-key-abc" });
    });

    test("add-on actif SANS clé → metered", async () => {
      await resetOrg({ serankingSyncBalance: 7 });
      const { orgAddons } = await import("@/lib/db/domain-schema");
      await db
        .insert(orgAddons)
        .values({ orgId: ORG, addon: SERANKING_BYOK_ADDON });
      const access = await getSerankingAccess(ORG);
      expect(access).toEqual({ mode: "metered", balance: 7 });
    });

    test("clé renseignée sans add-on actif → metered", async () => {
      await resetOrg({
        serankingApiKey: "srk-key-abc",
        serankingSyncBalance: 3,
      });
      const access = await getSerankingAccess(ORG);
      expect(access).toEqual({ mode: "metered", balance: 3 });
    });
  });

  describe("consumeSync", () => {
    test("décrémente 1 et renvoie true", async () => {
      await resetOrg({ serankingSyncBalance: 5 });
      const ok = await consumeSync(ORG);
      expect(ok).toBe(true);
      expect(await readBalance()).toBe(4);
    });

    test("à 0 renvoie false et ne passe pas en négatif", async () => {
      await resetOrg({ serankingSyncBalance: 0 });
      const ok = await consumeSync(ORG);
      expect(ok).toBe(false);
      expect(await readBalance()).toBe(0);
    });

    // Même régression que src/lib/mcp/quota.test.ts et
    // src/lib/billing/quotas.test.ts : un read-then-write fire-and-forget
    // laisserait N racers lire le même solde pré-écriture et tous passer.
    // L'UPDATE...WHERE...RETURNING conditionnel sérialise sur le row lock
    // Postgres — exactement 5 des 12 doivent passer sur un solde de 5.
    test("concurrence : 12 consumeSync sur balance=5 → exactement 5 true", async () => {
      await resetOrg({ serankingSyncBalance: 5 });
      const RACERS = 12;
      const results = await Promise.all(
        Array.from({ length: RACERS }, () => consumeSync(ORG)),
      );
      expect(results.filter(Boolean).length).toBe(5);
      expect(results.filter((r) => !r).length).toBe(RACERS - 5);
      expect(await readBalance()).toBe(0);
    });
  });

  describe("creditSyncs", () => {
    test("additionne au solde existant", async () => {
      await resetOrg({ serankingSyncBalance: 3 });
      await creditSyncs(ORG, 100);
      expect(await readBalance()).toBe(103);
    });
  });
});
