// src/lib/connectors/core.test.ts — couche action org-scopée du connecteur
// Mangools (postgres, section gated DB_TESTS=1, même pattern que
// store.test.ts / seranking/integration-actions.test.ts). La DI (`deps`) mocke
// la gate owner/admin (`canManage`), la résolution d'org/user et surtout le
// `ping` de validation : AUCUN appel réseau vers Mangools ici (le ping par
// défaut, qui frappe l'API, n'est jamais invoqué). On vérifie le round-trip
// chiffré via le vrai store, et l'invariant clé : sur ping raté, l'ancien
// token est CONSERVÉ (le check se fait AVANT setConnector).
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
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 { orgConnectors } from "@/lib/db/domain-schema";
import { connectMangools, disconnectMangools, getMangoolsStatus } from "./core";
import { getConnectorStatus, getConnectorToken, setConnector } from "./store";

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

describe.skipIf(!RUN)("connectors core — Mangools (postgres)", () => {
  const ORG = "org-connectors-core-test";
  const USER = "user-connectors-core-test";

  const admin = {
    orgId: async () => ORG,
    userId: async () => USER,
    canManage: async () => true,
  };
  const member = {
    orgId: async () => ORG,
    userId: async () => USER,
    canManage: async () => false,
  };
  const pingOk = async () => {};
  const pingFail = async () => {
    throw new Error("invalid");
  };

  async function cleanup() {
    // FK org_connectors.orgId → organization.id ON DELETE CASCADE.
    await db.delete(organization).where(eq(organization.id, ORG));
  }

  async function resetOrg() {
    await cleanup();
    await db.insert(organization).values({
      id: ORG,
      name: "connectors-core-test",
      slug: "connectors-core-test",
      createdAt: new Date(),
    });
  }

  async function readRow() {
    const [row] = await db
      .select({
        encryptedToken: orgConnectors.encryptedToken,
        status: orgConnectors.status,
        connectedBy: orgConnectors.connectedBy,
      })
      .from(orgConnectors)
      .where(eq(orgConnectors.id, `${ORG}-mangools`));
    return row ?? null;
  }

  beforeAll(async () => {
    await assertTestDb();
    await resetOrg();
  });

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

  test("connectMangools : ping ok → token stocké chiffré + actif", async () => {
    await resetOrg();
    const res = await connectMangools("mangools-token-abc", {
      ...admin,
      ping: pingOk,
    });

    expect(res).toEqual({ ok: true });
    const row = await readRow();
    expect(row?.status).toBe("active");
    expect(row?.connectedBy).toBe(USER);
    // Jamais en clair, mais déchiffrable.
    expect(row?.encryptedToken).not.toBe("mangools-token-abc");
    expect(row?.encryptedToken.startsWith("v1:")).toBe(true);
    expect(await getConnectorToken(ORG, "mangools")).toBe("mangools-token-abc");
  });

  test("connectMangools : ping raté → invalid_token, ancien token CONSERVÉ", async () => {
    await resetOrg();
    // Un token valide déjà en place.
    await setConnector(ORG, "mangools", "old-valid-token", USER);

    const res = await connectMangools("new-bad-token", {
      ...admin,
      ping: pingFail,
    });

    expect(res).toEqual({ ok: false, error: "invalid_token" });
    // L'écrasement n'a PAS eu lieu (check avant setConnector).
    expect(await getConnectorToken(ORG, "mangools")).toBe("old-valid-token");
    expect(await getConnectorStatus(ORG, "mangools")).toEqual({
      connected: true,
      status: "active",
    });
  });

  test("connectMangools : non-admin → refus (forbidden), rien stocké", async () => {
    await resetOrg();
    const res = await connectMangools("whatever-token", {
      ...member,
      ping: pingOk,
    });

    expect(res).toEqual({ ok: false, error: "forbidden" });
    expect(await readRow()).toBeNull();
  });

  test("connectMangools : token vide → invalid_token, rien stocké", async () => {
    await resetOrg();
    const res = await connectMangools("   ", { ...admin, ping: pingOk });
    expect(res).toEqual({ ok: false, error: "invalid_token" });
    expect(await readRow()).toBeNull();
  });

  test("connectMangools : remplacement d'un token existant (ping ok)", async () => {
    await resetOrg();
    await setConnector(ORG, "mangools", "first-token", USER);

    const res = await connectMangools("second-token", {
      ...admin,
      ping: pingOk,
    });

    expect(res).toEqual({ ok: true });
    expect(await getConnectorToken(ORG, "mangools")).toBe("second-token");
  });

  test("disconnectMangools : admin → supprime le connecteur (état absent)", async () => {
    await resetOrg();
    await setConnector(ORG, "mangools", "to-remove", USER);

    const res = await disconnectMangools(admin);

    expect(res).toEqual({ ok: true });
    expect(await readRow()).toBeNull();
    expect(await getConnectorStatus(ORG, "mangools")).toEqual({
      connected: false,
      status: null,
    });
  });

  test("disconnectMangools : non-admin → refus, connecteur conservé", async () => {
    await resetOrg();
    await setConnector(ORG, "mangools", "keep-me", USER);

    const res = await disconnectMangools(member);

    expect(res).toEqual({ ok: false, error: "forbidden" });
    expect(await getConnectorToken(ORG, "mangools")).toBe("keep-me");
  });

  test("getMangoolsStatus : reflète active / absent", async () => {
    await resetOrg();
    expect(await getMangoolsStatus({ orgId: async () => ORG })).toEqual({
      connected: false,
      status: null,
    });

    await setConnector(ORG, "mangools", "some-token", USER);
    expect(await getMangoolsStatus({ orgId: async () => ORG })).toEqual({
      connected: true,
      status: "active",
    });
  });
});
