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 { deriveSiteGscState } from "./site-state";

const RUN_DB = process.env.DB_TESTS === "1";
const SCOPE = "https://www.googleapis.com/auth/webmasters.readonly";
const ORG = "sgs-org";
const yes = async () => true;
const no = async () => false;

const USERS = [
  "sgs-auth",
  "sgs-nogoogle",
  "sgs-noscope",
  "sgs-multi",
  "sgs-mem",
];
const SITES = [
  "sgs-noprop",
  "sgs-noauth",
  "sgs-ok",
  "sgs-nog",
  "sgs-nos",
  "sgs-pin",
  "sgs-pin-gone",
  "sgs-pin-unscoped",
  "sgs-nopin",
  "sgs-memsite",
];

/** 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: "sgs-auth", name: "a", email: "sgs-auth@example.test" },
    { id: "sgs-nogoogle", name: "n", email: "sgs-nogoogle@example.test" },
    { id: "sgs-noscope", name: "s", email: "sgs-noscope@example.test" },
    { id: "sgs-multi", name: "m", email: "sgs-multi@example.test" },
    { id: "sgs-mem", name: "e", email: "sgs-mem@example.test" },
  ]);
  await db
    .insert(organization)
    .values({ id: ORG, name: "sgs", slug: ORG, createdAt: new Date() });
  await db.insert(member).values([
    {
      id: "sgs-m1",
      organizationId: ORG,
      userId: "sgs-auth",
      role: "owner",
      createdAt: new Date(),
    },
    {
      id: "sgs-m2",
      organizationId: ORG,
      userId: "sgs-multi",
      role: "admin",
      createdAt: new Date(),
    },
    // Rôle `member` SANS ligne site_access : le vrai `userCanAccessSite` doit
    // répondre false. Sert à exercer la dépendance par DÉFAUT.
    {
      id: "sgs-m3",
      organizationId: ORG,
      userId: "sgs-mem",
      role: "member",
      createdAt: new Date(),
    },
  ]);
  await db.insert(account).values([
    {
      id: "sgs-a1",
      accountId: "g1",
      providerId: "google",
      userId: "sgs-auth",
      scope: SCOPE,
      createdAt: new Date(t0),
      updatedAt: new Date(),
    },
    {
      id: "sgs-a2",
      accountId: "g2",
      providerId: "google",
      userId: "sgs-noscope",
      scope: "openid email",
      createdAt: new Date(t0),
      updatedAt: new Date(),
    },
    // Trois comptes Google pour la MÊME autorité : le cas agence, encouragé
    // par accountLinking.allowDifferentEmails (src/lib/auth.ts:131).
    {
      id: "sgs-a3",
      accountId: "g-old",
      providerId: "google",
      userId: "sgs-multi",
      scope: SCOPE,
      createdAt: new Date(t0),
      updatedAt: new Date(),
    },
    {
      id: "sgs-a4",
      accountId: "g-new",
      providerId: "google",
      userId: "sgs-multi",
      scope: SCOPE,
      createdAt: new Date(t0 + 1000),
      updatedAt: new Date(),
    },
    {
      id: "sgs-a5",
      accountId: "g-nos",
      providerId: "google",
      userId: "sgs-multi",
      scope: "openid email",
      createdAt: new Date(t0 + 2000),
      updatedAt: new Date(),
    },
    {
      id: "sgs-a6",
      accountId: "g5",
      providerId: "google",
      userId: "sgs-mem",
      scope: SCOPE,
      createdAt: new Date(t0),
      updatedAt: new Date(),
    },
  ]);
  await db.insert(sites).values([
    { id: "sgs-noprop", orgId: ORG, name: "n", url: "https://n.test" },
    {
      id: "sgs-noauth",
      orgId: ORG,
      name: "a",
      url: "https://a.test",
      gscProperty: "sc-domain:a.test",
    },
    {
      id: "sgs-ok",
      orgId: ORG,
      name: "o",
      url: "https://o.test",
      gscProperty: "sc-domain:o.test",
      gscAuthorizedUserId: "sgs-auth",
      gscAuthorizedAccountId: "g1",
    },
    {
      id: "sgs-nog",
      orgId: ORG,
      name: "g",
      url: "https://g.test",
      gscProperty: "sc-domain:g.test",
      gscAuthorizedUserId: "sgs-nogoogle",
    },
    {
      id: "sgs-nos",
      orgId: ORG,
      name: "s",
      url: "https://s.test",
      gscProperty: "sc-domain:s.test",
      gscAuthorizedUserId: "sgs-noscope",
    },
    {
      id: "sgs-pin",
      orgId: ORG,
      name: "p",
      url: "https://p.test",
      gscProperty: "sc-domain:p.test",
      gscAuthorizedUserId: "sgs-multi",
      gscAuthorizedAccountId: "g-new",
    },
    {
      id: "sgs-pin-gone",
      orgId: ORG,
      name: "pg",
      url: "https://pg.test",
      gscProperty: "sc-domain:pg.test",
      gscAuthorizedUserId: "sgs-multi",
      gscAuthorizedAccountId: "g-disparu",
    },
    {
      id: "sgs-pin-unscoped",
      orgId: ORG,
      name: "pu",
      url: "https://pu.test",
      gscProperty: "sc-domain:pu.test",
      gscAuthorizedUserId: "sgs-multi",
      gscAuthorizedAccountId: "g-nos",
    },
    {
      id: "sgs-nopin",
      orgId: ORG,
      name: "np",
      url: "https://np.test",
      gscProperty: "sc-domain:np.test",
      gscAuthorizedUserId: "sgs-multi",
    },
    {
      id: "sgs-memsite",
      orgId: ORG,
      name: "ms",
      url: "https://ms.test",
      gscProperty: "sc-domain:ms.test",
      gscAuthorizedUserId: "sgs-mem",
      gscAuthorizedAccountId: "g5",
    },
  ]);
});

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

test.skipIf(!RUN_DB)("site inconnu → no-property", async () => {
  expect(
    (await deriveSiteGscState("sgs-ghost", { canAuthorityAccessSite: yes }))
      .state,
  ).toBe("no-property");
});

test.skipIf(!RUN_DB)("aucune propriété mappée → no-property", async () => {
  expect(
    (await deriveSiteGscState("sgs-noprop", { canAuthorityAccessSite: yes }))
      .state,
  ).toBe("no-property");
});

test.skipIf(!RUN_DB)("propriété sans autorité → no-authority", async () => {
  expect(
    (await deriveSiteGscState("sgs-noauth", { canAuthorityAccessSite: yes }))
      .state,
  ).toBe("no-authority");
});

test.skipIf(!RUN_DB)(
  "autorité sans accès au site → authority-lost",
  async () => {
    const r = await deriveSiteGscState("sgs-ok", {
      canAuthorityAccessSite: no,
    });
    expect(r.state).toBe("authority-lost");
    expect(r.authorityAccountId).toBe("g1");
  },
);

test.skipIf(!RUN_DB)(
  "autorité sans compte Google → no-google-account",
  async () => {
    expect(
      (await deriveSiteGscState("sgs-nog", { canAuthorityAccessSite: yes }))
        .state,
    ).toBe("no-google-account");
  },
);

test.skipIf(!RUN_DB)(
  "compte Google sans le scope → missing-scope",
  async () => {
    expect(
      (await deriveSiteGscState("sgs-nos", { canAuthorityAccessSite: yes }))
        .state,
    ).toBe("missing-scope");
  },
);

test.skipIf(!RUN_DB)(
  "tout en place → ok, avec propriété, autorité et compte",
  async () => {
    const r = await deriveSiteGscState("sgs-ok", {
      canAuthorityAccessSite: yes,
    });
    expect(r.state).toBe("ok");
    expect(r.property).toBe("sc-domain:o.test");
    expect(r.authorityUserId).toBe("sgs-auth");
    expect(r.authorityAccountId).toBe("g1");
  },
);

// Le cœur de la correction : l'autorité désigne un utilisateur ET un compte.
test.skipIf(!RUN_DB)("compte épinglé présent et scopé → retenu", async () => {
  const r = await deriveSiteGscState("sgs-pin", {
    canAuthorityAccessSite: yes,
  });
  expect(r.state).toBe("ok");
  // `g-new` est le plus RÉCENT : sans l'épinglage, l'ordre total aurait
  // choisi `g-old`. C'est ce qui distingue les deux sites d'un consultant.
  expect(r.authorityAccountId).toBe("g-new");
});

// Pin posé mais compte disparu : PAS de repli. Retomber sur un autre compte
// rendrait « ok » un site que Google refusera — l'échec sortirait alors en 403
// opaque dans search-analytics.ts, hors du type, ce que 0071 supprime.
test.skipIf(!RUN_DB)(
  "compte épinglé disparu → no-google-account, sans repli",
  async () => {
    const r = await deriveSiteGscState("sgs-pin-gone", {
      canAuthorityAccessSite: yes,
    });
    expect(r.state).toBe("no-google-account");
    // `g-old` et `g-new` sont là, scopés, et ne doivent PAS être choisis.
    expect(r.authorityAccountId).toBe("g-disparu");
  },
);

test.skipIf(!RUN_DB)(
  "compte épinglé dé-scopé → missing-scope, sans repli",
  async () => {
    const r = await deriveSiteGscState("sgs-pin-unscoped", {
      canAuthorityAccessSite: yes,
    });
    expect(r.state).toBe("missing-scope");
    // La charge utile nomme le compte fautif : « le compte X n'a plus le
    // scope » est un message réparable, « ça ne marche pas » ne l'est pas.
    expect(r.authorityAccountId).toBe("g-nos");
  },
);

// Le pendant du test précédent : `pinnedAccountId === null` est un chemin
// LÉGITIME (mappings antérieurs à l'épinglage, lignes NULL du backfill), pas
// une anomalie. Le distinguer est tout l'objet des deux branches.
test.skipIf(!RUN_DB)(
  "aucun compte épinglé → ordre total, pas de ligne arbitraire",
  async () => {
    const r = await deriveSiteGscState("sgs-nopin", {
      canAuthorityAccessSite: yes,
    });
    expect(r.state).toBe("ok");
    expect(r.authorityAccountId).toBe("g-old");
  },
);

// Les tests ci-dessus injectent tous la dépendance : sans les deux suivants,
// une inversion d'arguments dans l'appel par défaut passerait au vert.
test.skipIf(!RUN_DB)(
  "dépendance par DÉFAUT : owner de l'org → ok",
  async () => {
    expect((await deriveSiteGscState("sgs-ok")).state).toBe("ok");
  },
);

test.skipIf(!RUN_DB)(
  "dépendance par DÉFAUT : member sans site_access → authority-lost",
  async () => {
    expect((await deriveSiteGscState("sgs-memsite")).state).toBe(
      "authority-lost",
    );
  },
);
