import { afterAll, beforeAll, expect, test } from "bun:test";
import { eq } from "drizzle-orm";
import { assertTestDb } from "@/lib/db/assert-test-db";
import { account, member, organization, user } from "@/lib/db/auth-schema";
import { db } from "@/lib/db/client";
import { sites } from "@/lib/db/domain-schema";
import { resolveSiteGscToken } from "./site-token";

const RUN_DB = process.env.DB_TESTS === "1";
const SCOPE = "https://www.googleapis.com/auth/webmasters.readonly";
const ORG = "sgt-org";
const SOURCE = "src/lib/gsc/site-token.ts";
const yes = async () => true;
const no = async () => false;

const USERS = ["sgt-auth", "sgt-nogoogle", "sgt-noscope"];
const SITES = ["sgt-site", "sgt-nog", "sgt-nos", "sgt-gone"];

/** Nettoyage. Appelé aussi EN TÊTE de beforeAll : un run interrompu laisserait
 *  ces IDs fixes derrière lui et ferait échouer tous les runs suivants sur une
 *  violation de PK, avec un message sans rapport avec le vrai problème. */
async function cleanup() {
  for (const id of SITES) await db.delete(sites).where(eq(sites.id, id));
  for (const id of USERS)
    await db.delete(account).where(eq(account.userId, id));
  await db.delete(member).where(eq(member.organizationId, ORG));
  await db.delete(organization).where(eq(organization.id, ORG));
  for (const id of USERS) await db.delete(user).where(eq(user.id, id));
}

beforeAll(async () => {
  if (!RUN_DB) return;
  assertTestDb();
  await cleanup();
  const t0 = Date.now();
  await db.insert(user).values([
    { id: "sgt-auth", name: "a", email: "sgt-auth@example.test" },
    { id: "sgt-nogoogle", name: "n", email: "sgt-nogoogle@example.test" },
    { id: "sgt-noscope", name: "s", email: "sgt-noscope@example.test" },
  ]);
  await db
    .insert(organization)
    .values({ id: ORG, name: "sgt", slug: ORG, createdAt: new Date() });
  await db.insert(member).values({
    id: "sgt-m1",
    organizationId: ORG,
    userId: "sgt-auth",
    role: "owner",
    createdAt: new Date(),
  });
  // Deux comptes Google scopés pour la même autorité : le site épingle le plus
  // RÉCENT, donc un test qui passe ne peut pas l'avoir obtenu par hasard.
  await db.insert(account).values([
    {
      id: "sgt-a1",
      accountId: "gt-old",
      providerId: "google",
      userId: "sgt-auth",
      scope: SCOPE,
      createdAt: new Date(t0),
      updatedAt: new Date(),
    },
    {
      id: "sgt-a2",
      accountId: "gt-new",
      providerId: "google",
      userId: "sgt-auth",
      scope: SCOPE,
      createdAt: new Date(t0 + 1000),
      updatedAt: new Date(),
    },
    {
      id: "sgt-a3",
      accountId: "gt-nos",
      providerId: "google",
      userId: "sgt-noscope",
      scope: "openid email",
      createdAt: new Date(t0),
      updatedAt: new Date(),
    },
  ]);
  await db.insert(sites).values([
    {
      id: "sgt-site",
      orgId: ORG,
      name: "s",
      url: "https://s.test",
      gscProperty: "sc-domain:s.test",
      gscAuthorizedUserId: "sgt-auth",
      gscAuthorizedAccountId: "gt-new",
    },
    {
      id: "sgt-nog",
      orgId: ORG,
      name: "g",
      url: "https://g.test",
      gscProperty: "sc-domain:g.test",
      gscAuthorizedUserId: "sgt-nogoogle",
    },
    {
      id: "sgt-nos",
      orgId: ORG,
      name: "n",
      url: "https://n.test",
      gscProperty: "sc-domain:n.test",
      gscAuthorizedUserId: "sgt-noscope",
      gscAuthorizedAccountId: "gt-nos",
    },
    // Le compte épinglé a été délié, mais l'autorité en a deux autres, scopés.
    {
      id: "sgt-gone",
      orgId: ORG,
      name: "d",
      url: "https://d.test",
      gscProperty: "sc-domain:d.test",
      gscAuthorizedUserId: "sgt-auth",
      gscAuthorizedAccountId: "gt-disparu",
    },
  ]);
});

afterAll(async () => {
  if (!RUN_DB) return;
  await cleanup();
});

// LE test du design. Deux choses en une :
//  1. pas de headers — sinon Better Auth fait gagner la session de l'appelant
//     sur body.userId et on résout le jeton du MAUVAIS utilisateur, sans
//     erreur, avec des données du mauvais compte Google ;
//  2. l'accountId transmis est celui ÉPINGLÉ sur le site — sans lui,
//     account.mjs:270 refait sa propre sélection arbitraire parmi les comptes
//     Google de l'autorité.
test.skipIf(!RUN_DB)(
  "n'envoie JAMAIS de headers à getAccessToken, et cible le compte épinglé",
  async () => {
    let captured: unknown;
    await resolveSiteGscToken("sgt-site", {
      canAuthorityAccessSite: yes,
      getAccessToken: async (args) => {
        captured = args;
        return { accessToken: "tok" };
      },
    });
    expect(captured).toEqual({
      body: { providerId: "google", userId: "sgt-auth", accountId: "gt-new" },
    });
    expect(Object.keys(captured as object)).not.toContain("headers");
  },
);

// L'angle mort du test précédent : il capture à la frontière
// `resolveSiteGscToken → deps`, jamais à la frontière `→ auth.api`. Le wrapper
// par défaut n'est donc traversé par aucun test d'exécution — quelqu'un
// pourrait y ajouter `headers: await headers()` en debuggant sans qu'un seul
// test bronche. Cette lecture du source ferme le trou : commentaires retirés,
// le mot `headers` ne doit pas apparaître dans le CODE du module, et l'appel à
// Better Auth doit rester un passe-plat de `args`.
test("le wrapper par défaut ne peut pas acquérir de headers", async () => {
  const source = await Bun.file(SOURCE).text();
  const code = source
    .replace(/\/\*[\s\S]*?\*\//g, "")
    .replace(/^\s*\/\/.*$/gm, "");
  expect(code).not.toContain("headers");
  expect(code).toMatch(/auth\.api\.getAccessToken\(\s*args\s*\)/);
});

test.skipIf(!RUN_DB)(
  "chaîne complète → jeton + propriété + autorité + compte",
  async () => {
    const r = await resolveSiteGscToken("sgt-site", {
      canAuthorityAccessSite: yes,
      getAccessToken: async () => ({ accessToken: "tok" }),
    });
    expect(r).toEqual({
      ok: true,
      accessToken: "tok",
      property: "sc-domain:s.test",
      authorityUserId: "sgt-auth",
      authorityAccountId: "gt-new",
    });
  },
);

test.skipIf(!RUN_DB)(
  "autorité sans accès → authority-lost, sans appeler Google",
  async () => {
    let called = false;
    const r = await resolveSiteGscToken("sgt-site", {
      canAuthorityAccessSite: no,
      getAccessToken: async () => {
        called = true;
        return { accessToken: "tok" };
      },
    });
    expect(r).toEqual({ ok: false, reason: "authority-lost" });
    expect(called).toBe(false);
  },
);

test.skipIf(!RUN_DB)(
  "autorité sans compte Google → no-google-account, sans appeler Google",
  async () => {
    let called = false;
    const r = await resolveSiteGscToken("sgt-nog", {
      canAuthorityAccessSite: yes,
      getAccessToken: async () => {
        called = true;
        return { accessToken: "tok" };
      },
    });
    expect(r).toEqual({ ok: false, reason: "no-google-account" });
    expect(called).toBe(false);
  },
);

test.skipIf(!RUN_DB)(
  "compte sans le scope → missing-scope, sans appeler Google",
  async () => {
    let called = false;
    const r = await resolveSiteGscToken("sgt-nos", {
      canAuthorityAccessSite: yes,
      getAccessToken: async () => {
        called = true;
        return { accessToken: "tok" };
      },
    });
    expect(r).toEqual({ ok: false, reason: "missing-scope" });
    expect(called).toBe(false);
  },
);

// L'aller-retour Google évité : sans la branche « pin disparu », ce site se
// dériverait `ok`, partirait chercher un jeton valide sur un AUTRE compte, et
// mourrait en 403 opaque un cran plus loin (search-analytics.ts).
test.skipIf(!RUN_DB)(
  "compte épinglé disparu → no-google-account, sans appeler Google",
  async () => {
    let called = false;
    const r = await resolveSiteGscToken("sgt-gone", {
      canAuthorityAccessSite: yes,
      getAccessToken: async () => {
        called = true;
        return { accessToken: "tok" };
      },
    });
    expect(r).toEqual({ ok: false, reason: "no-google-account" });
    expect(called).toBe(false);
  },
);

test.skipIf(!RUN_DB)("getAccessToken qui jette → token-refused", async () => {
  const r = await resolveSiteGscToken("sgt-site", {
    canAuthorityAccessSite: yes,
    getAccessToken: async () => {
      throw new Error("refresh token révoqué");
    },
  });
  expect(r).toEqual({ ok: false, reason: "token-refused" });
});

test.skipIf(!RUN_DB)(
  "getAccessToken sans accessToken → token-refused",
  async () => {
    const r = await resolveSiteGscToken("sgt-site", {
      canAuthorityAccessSite: yes,
      getAccessToken: async () => ({}),
    });
    expect(r).toEqual({ ok: false, reason: "token-refused" });
  },
);

test.skipIf(!RUN_DB)("propage l'état dérivé sans appeler Google", async () => {
  const r = await resolveSiteGscToken("sgt-ghost", {
    canAuthorityAccessSite: yes,
  });
  expect(r).toEqual({ ok: false, reason: "no-property" });
});
