import { afterEach, beforeEach, expect, test } from "bun:test";
import { eq, sql } 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";

const RUN_DB = process.env.DB_TESTS === "1";
const SCOPE = "https://www.googleapis.com/auth/webmasters.readonly";
const ORG = "af-org";
const SITE = "af-site";
const AUTH = "af-auth";
const MIGRATION = "drizzle/0071_flowery_jetstream.sql";

// Doit rester byte-identique à la requête de drizzle/0071_flowery_jetstream.sql
// — c'est vérifié mécaniquement plus bas, pas seulement promis en commentaire.
// Chaîne brute (pas de template `sql`) précisément pour rendre cette
// comparaison possible ; aucun paramètre n'est interpolé, `sql.raw` est donc
// sans risque d'injection ici.
const BACKFILL_SQL = String.raw`UPDATE sites s SET gsc_authorized_account_id = (
  SELECT a.account_id FROM account a
  WHERE a.user_id = s.gsc_authorized_user_id
    AND a.provider_id = 'google'
    AND a.scope ~ '(^|[[:space:],])https://www\.googleapis\.com/auth/webmasters\.readonly([[:space:],]|$)'
  ORDER BY a.created_at, a.account_id
  LIMIT 1
) WHERE s.gsc_authorized_user_id IS NOT NULL
  AND s.gsc_authorized_account_id IS NULL`;

const BACKFILL = sql.raw(BACKFILL_SQL);

type GoogleAccount = {
  id: string;
  accountId: string;
  scope: string | null;
  /** Décalage en ms sur created_at : pilote l'ordre total du backfill. */
  createdAtOffsetMs?: number;
  providerId?: string;
};

type SeedOpts = {
  accounts: GoogleAccount[];
  /** null = site sans autorité, donc hors périmètre du backfill. */
  authorityUserId?: string | null;
};

/** Nettoyage. Appelé aussi EN TÊTE de seed() : un run interrompu laisserait des
 *  lignes 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() {
  await db.delete(sites).where(eq(sites.id, SITE));
  await db.delete(account).where(eq(account.userId, AUTH));
  await db.delete(member).where(eq(member.organizationId, ORG));
  await db.delete(organization).where(eq(organization.id, ORG));
  await db.delete(user).where(eq(user.id, AUTH));
}

async function seed(opts: SeedOpts) {
  await cleanup();
  const base = Date.now();
  await db
    .insert(user)
    .values({ id: AUTH, name: "a", email: "af-auth@example.test" });
  await db
    .insert(organization)
    .values({ id: ORG, name: "af", slug: ORG, createdAt: new Date() });
  await db.insert(member).values({
    id: "af-m1",
    organizationId: ORG,
    userId: AUTH,
    role: "owner",
    createdAt: new Date(),
  });
  for (const a of opts.accounts) {
    await db.insert(account).values({
      id: a.id,
      accountId: a.accountId,
      providerId: a.providerId ?? "google",
      userId: AUTH,
      scope: a.scope,
      createdAt: new Date(base + (a.createdAtOffsetMs ?? 0)),
      updatedAt: new Date(),
    });
  }
  await db.insert(sites).values({
    id: SITE,
    orgId: ORG,
    name: "s",
    url: "https://s.test",
    gscProperty: "sc-domain:s.test",
    gscAuthorizedUserId:
      opts.authorityUserId === undefined ? AUTH : opts.authorityUserId,
  });
}

async function pinnedAccount(): Promise<string | null> {
  const [row] = await db
    .select({ a: sites.gscAuthorizedAccountId })
    .from(sites)
    .where(eq(sites.id, SITE));
  return row?.a ?? null;
}

beforeEach(() => {
  if (RUN_DB) assertTestDb();
});

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

// La garantie qui rend tous les tests suivants pertinents : ils exercent le
// texte EXACT du fichier de migration, pas une paraphrase qui aurait divergé.
test("la requête testée est byte-identique à celle de la migration", async () => {
  const content = await Bun.file(MIGRATION).text();
  expect(content).toContain(BACKFILL_SQL);
});

test.skipIf(!RUN_DB)("compte scopé unique → épinglé", async () => {
  await seed({ accounts: [{ id: "af-a1", accountId: "g1", scope: SCOPE }] });
  await db.execute(BACKFILL);
  expect(await pinnedAccount()).toBe("g1");
});

test.skipIf(!RUN_DB)(
  "deux comptes dont un seul scopé → le scopé gagne, même créé après",
  async () => {
    await seed({
      accounts: [
        {
          id: "af-a1",
          accountId: "g1",
          scope: "openid email",
          createdAtOffsetMs: 0,
        },
        {
          id: "af-a2",
          accountId: "g2",
          scope: SCOPE,
          createdAtOffsetMs: 1000,
        },
      ],
    });
    await db.execute(BACKFILL);
    expect(await pinnedAccount()).toBe("g2");
  },
);

test.skipIf(!RUN_DB)(
  "deux comptes scopés → le plus ancien (ordre total)",
  async () => {
    await seed({
      accounts: [
        { id: "af-a1", accountId: "g2", scope: SCOPE, createdAtOffsetMs: 1000 },
        { id: "af-a2", accountId: "g1", scope: SCOPE, createdAtOffsetMs: 0 },
      ],
    });
    await db.execute(BACKFILL);
    expect(await pinnedAccount()).toBe("g1");
  },
);

test.skipIf(!RUN_DB)("aucun compte scopé → reste NULL", async () => {
  await seed({
    accounts: [{ id: "af-a1", accountId: "g1", scope: "openid email" }],
  });
  await db.execute(BACKFILL);
  expect(await pinnedAccount()).toBeNull();
});

test.skipIf(!RUN_DB)("aucun compte du tout → reste NULL", async () => {
  await seed({ accounts: [] });
  await db.execute(BACKFILL);
  expect(await pinnedAccount()).toBeNull();
});

test.skipIf(!RUN_DB)("autorité NULL → reste NULL", async () => {
  await seed({
    accounts: [{ id: "af-a1", accountId: "g1", scope: SCOPE }],
    authorityUserId: null,
  });
  await db.execute(BACKFILL);
  expect(await pinnedAccount()).toBeNull();
});

test.skipIf(!RUN_DB)("compte non-google avec le scope → exclu", async () => {
  await seed({
    accounts: [
      { id: "af-a1", accountId: "g1", scope: SCOPE, providerId: "github" },
    ],
  });
  await db.execute(BACKFILL);
  expect(await pinnedAccount()).toBeNull();
});

// Le format de PRODUCTION : Better Auth stocke les scopes séparés par des
// virgules (cf. src/lib/gsc/scope.ts).
test.skipIf(!RUN_DB)("scopes séparés par des virgules → reconnus", async () => {
  await seed({
    accounts: [
      { id: "af-a1", accountId: "g1", scope: `openid,email,${SCOPE}` },
    ],
  });
  await db.execute(BACKFILL);
  expect(await pinnedAccount()).toBe("g1");
});

test.skipIf(!RUN_DB)("scope webmasters SANS readonly → exclu", async () => {
  await seed({
    accounts: [
      {
        id: "af-a1",
        accountId: "g1",
        scope: "https://www.googleapis.com/auth/webmasters",
      },
    ],
  });
  await db.execute(BACKFILL);
  expect(await pinnedAccount()).toBeNull();
});

// Garde de ré-exécution : ne doit pas écraser un compte déjà épinglé depuis
// l'UI (ni le remettre à NULL s'il a perdu le scope entre-temps).
test.skipIf(!RUN_DB)("compte déjà épinglé → non écrasé", async () => {
  await seed({
    accounts: [
      { id: "af-a1", accountId: "g1", scope: SCOPE, createdAtOffsetMs: 0 },
      { id: "af-a2", accountId: "g2", scope: SCOPE, createdAtOffsetMs: 1000 },
    ],
  });
  await db
    .update(sites)
    .set({ gscAuthorizedAccountId: "g2" })
    .where(eq(sites.id, SITE));
  await db.execute(BACKFILL);
  expect(await pinnedAccount()).toBe("g2");
});
