import { beforeEach, describe, expect, it, vi } from "vitest";

/**
 * Auth FALHA ALTO, não baixo.
 *
 * O defeito que este arquivo trava: `loadAuthUser` descartava o erro das queries de
 * `platform_admins` e `user_organizations`. Query falhando devolvia `null`, `null`
 * virava `[]`, e `[]` significa "usuário sem organização".
 *
 * Resultado medido em 2026-07-30: com o PostgREST fora do ar (`name resolution
 * failed`) depois de um restart do Docker, TODOS os cards de admin sumiram do hub de
 * configurações. Custou seis diagnósticos errados — build velho, processo velho,
 * cache estático, filtro de papel — porque a falha se disfarçava de decisão de
 * autorização.
 *
 * Degradar permissão em silêncio é o pior desfecho num caminho de auth: parece
 * autorização e é infraestrutura.
 */

const consultas: { platformAdmins: unknown; memberships: unknown } = {
  platformAdmins: { data: null, error: null },
  memberships: { data: [], error: null },
};

// `get` entrou junto com a cadeia de idioma (usuário → organização): o
// resolvedor pergunta ao cookie qual organização está ativa. O dublê tinha
// `getAll`/`set` e não `get` — menos completo que a API real, e o teste caía
// por falta do dublê, não por defeito.
vi.mock("next/headers", () => ({
  cookies: async () => ({ get: () => undefined, getAll: () => [], set: () => {} }),
}));
vi.mock("next/navigation", () => ({ redirect: () => { throw new Error("redirect"); } }));

/** A organização do acompanhamento administrativo, lida por id (sem membership). */
const orgDoSuporte: { data: unknown } = { data: { timezone: "Europe/Lisbon", currency: "EUR", country: "PT" } };
vi.mock("@/lib/supabase/admin", () => ({
  createAdminClient: () => ({
    from: () => {
      const chain = { select: () => chain, eq: () => chain, maybeSingle: async () => orgDoSuporte };
      return chain;
    },
  }),
}));

vi.mock("@/lib/supabase/server", () => ({
  createClient: async () => ({
    rpc: async () => ({ data: null, error: null }),
    auth: {
      getUser: async () => ({
        data: { user: { id: "u1", email: "a@b.c", user_metadata: {} } },
        error: null,
      }),
    },
    from: (tabela: string) => {
      const alvo = tabela === "platform_admins" ? "platformAdmins" : "memberships";
      const resultado = () => consultas[alvo as keyof typeof consultas];
      const chain = {
        select: () => chain,
        eq: () => chain,
        // ⚠️ `is()` para memberships devolve o RESULTADO, e não a cadeia — o que
        // fazia dele o terminal obrigatório. A consulta de memberships passou a
        // ordenar (a lista decide qual organização fica ativa sem cookie, e sem
        // `ORDER BY` "a primeira" é o que o Postgres devolver), então o terminal
        // agora pode vir depois de `.order()`. O dublê precisa aceitar as duas
        // formas — e é thenable, então `await` no fim resolve igual.
        is: () =>
          alvo === "platformAdmins"
            ? { maybeSingle: async () => resultado() }
            : { ...chain, then: chain.then },
        order: () => ({ ...chain, then: chain.then }),
        maybeSingle: async () => resultado(),
        then: (r: (v: unknown) => unknown) => Promise.resolve(resultado()).then(r),
      };
      return chain;
    },
  }),
}));

const { loadAuthUser, resolveActiveOrg } = await import("@/lib/auth/server");
// O cache curto (#3696) é do PROCESSO: entre casos, ele precisa começar vazio —
// senão o snapshot de `u1` do caso anterior responde pelo seguinte.
const { esquecerTudo } = await import("@/lib/auth/cache-curto");

beforeEach(() => {
  esquecerTudo();
  consultas.platformAdmins = { data: null, error: null };
  consultas.memberships = { data: [], error: null };
});

describe("loadAuthUser — falha de permissão não vira 'sem organização'", () => {
  it("query de memberships que FALHA estoura, em vez de devolver zero orgs", async () => {
    consultas.memberships = {
      data: null,
      error: { code: "PGRST002", message: "name resolution failed" },
    };
    await expect(loadAuthUser()).rejects.toThrow(/auth_permissions_unavailable/);
  });

  it("query de platform_admins que FALHA estoura", async () => {
    // `data: null` aqui é AMBÍGUO: é o que a RLS devolve para quem não é admin E o
    // que sobra quando a query quebra. Sem checar o erro, um banco instável rebaixa
    // um super-admin em silêncio.
    consultas.platformAdmins = {
      data: null,
      error: { code: "PGRST002", message: "name resolution failed" },
    };
    await expect(loadAuthUser()).rejects.toThrow(/auth_permissions_unavailable/);
  });

  it("a mensagem diz que NÃO foi decisão de autorização", async () => {
    // Quem lê o erro precisa saber, na primeira linha, que não é permissão — senão
    // procura no lugar errado, como aconteceu.
    consultas.memberships = { data: null, error: { code: "X", message: "boom" } };
    await expect(loadAuthUser()).rejects.toThrow(/NÃO foi rebaixada por decisão de autorização/);
  });

  it("usuário que REALMENTE não tem organização segue funcionando, sem erro", async () => {
    // O contraponto: lista vazia SEM erro é estado legítimo (convite pendente,
    // acesso revogado) e não pode virar exceção.
    consultas.memberships = { data: [], error: null };
    const u = await loadAuthUser();
    expect(u?.organizations).toEqual([]);
  });

  it("usuário com organização resolve normalmente", async () => {
    consultas.memberships = {
      data: [{ organization_id: "o1", role: "admin", organizations: { display_name: "Acme" } }],
      error: null,
    };
    const u = await loadAuthUser();
    expect(u?.organizations).toEqual([
      // `locale` e `timezone` são o idioma e o fuso padrão da ORGANIZAÇÃO, que
      // entram na membership para quem resolve a sessão não precisar de uma
      // segunda consulta — o idioma para a interface, o fuso para a Agenda abrir
      // na semana de quem olha. Os dois vêm `null` aqui porque o dublê não
      // devolve as colunas, e é isso que este caso fixa: quando a consulta não
      // traz, a sessão recebe `null` em vez de `undefined` ou de um padrão
      // inventado no meio do caminho.
      {
        organization_id: "o1",
        organization_name: "Acme",
        role: "admin",
        locale: null,
        timezone: null,
        // Mesma carona, mesmo contrato: quando a consulta não traz, chega
        // `null`, e não um padrão inventado no meio do caminho.
        currency: null,
        country: null,
        // Status e tipo da suspensão da organização (spec da cobrança §4):
        // mesma carona, mesmo contrato — sem a coluna, `null`.
        org_status: null,
        suspended_kind: null,
        interface_settings: { preset: "completa" },
      },
    ]);
  });

  it("traz status e tipo de suspensão da org e o scope do platform admin", async () => {
    consultas.platformAdmins = { data: { user_id: "u1", scope: "support_readonly", revoked_at: null }, error: null };
    consultas.memberships = {
      data: [{ organization_id: "o1", role: "admin", organizations: { display_name: "Acme", status: "suspended", suspended_kind: "cobranca" } }],
      error: null,
    };
    const u = await loadAuthUser();
    expect(u?.is_platform_admin).toBe(true);
    expect(u?.platform_admin_scope).toBe("support_readonly");
    expect(u?.organizations[0]).toMatchObject({ org_status: "suspended", suspended_kind: "cobranca" });
  });

  /**
   * O FIO ATÉ A TELA. `ActiveOrg` é o que o cliente enxerga (`useActiveOrg`), e
   * é de lá que o rótulo do valor e o documento do contato saem. Sem estas duas
   * colunas atravessando, as telas caem no padrão e voltam a dizer `R$` e `CPF`
   * dentro de uma empresa em euro — sem nada ficar vermelho.
   */
  it("a organização ATIVA leva a moeda e o país até o cliente", async () => {
    consultas.memberships = {
      data: [
        {
          organization_id: "o1",
          role: "admin",
          organizations: {
            display_name: "Stolia",
            locale: "pt-BR",
            timezone: "Europe/Lisbon",
            currency: "EUR",
            country: "PT",
            status: "active",
          },
        },
      ],
      error: null,
    };
    const u = await loadAuthUser();
    const ativa = await resolveActiveOrg(u!);
    expect(ativa).toMatchObject({ orgId: "o1", currency: "EUR", country: "PT" });
  });

  /**
   * Acompanhamento administrativo não tem membership, e este caminho devolvia a
   * organização PELADA — sem fuso, sem moeda e sem país. Quem entra para apoiar
   * uma empresa em euro via `R$` na tela do negócio.
   */
  it("no acompanhamento administrativo a organização também chega completa", async () => {
    const ativa = await resolveActiveOrg({
      id: "u1",
      support: { status: "active", organization_id: "o9", name: "Stolia", access_mode: "full" },
    } as never);
    expect(ativa).toMatchObject({
      orgId: "o9",
      role: "admin",
      timezone: "Europe/Lisbon",
      currency: "EUR",
      country: "PT",
    });
  });
});
