import { afterEach, beforeEach, describe, expect, it, jest } from "@jest/globals";

const stubbedGlobals = new Map();
const vi = {
  fn: (...args) => jest.fn(...args),
  spyOn: (...args) => jest.spyOn(...args),
  resetModules: () => jest.resetModules(),
  restoreAllMocks: () => jest.restoreAllMocks(),
  stubGlobal: (name, value) => {
    if (!stubbedGlobals.has(name)) {
      stubbedGlobals.set(name, Object.getOwnPropertyDescriptor(globalThis, name));
    }
    Object.defineProperty(globalThis, name, {
      configurable: true,
      writable: true,
      value,
    });
  },
  unstubAllGlobals: () => {
    for (const [name, descriptor] of stubbedGlobals.entries()) {
      if (descriptor) {
        Object.defineProperty(globalThis, name, descriptor);
      } else {
        Reflect.deleteProperty(globalThis, name);
      }
    }
    stubbedGlobals.clear();
  },
};

async function loadAuth() {
  vi.resetModules();
  return import("../../../frontend/src/auth.js");
}

describe("auth helpers", () => {
  beforeEach(() => {
    globalThis.__ATLAS_KEYCLOAK_FACTORY__ = null;
    globalThis.__ATLAS_KEYCLOAK_CONSTRUCTOR__ = null;
  });

  afterEach(() => {
    vi.restoreAllMocks();
    vi.unstubAllGlobals();
    vi.resetModules();
  });

  it("normalizes token groups", async () => {
    const { normalizeGroups } = await loadAuth();

    expect(normalizeGroups(["/dev", "admin", 3, null, ""])).toEqual(["dev", "admin"]);
    expect(normalizeGroups("not-an-array")).toEqual([]);
  });

  it("attaches a bearer token when fetching", async () => {
    const authModule = await loadAuth();
    authModule.auth.token = "bearer-token";

    const fetchMock = vi.fn(async () => new Response("{}", { status: 200 }));
    vi.stubGlobal("fetch", fetchMock);

    await authModule.authFetch("/api/healthz");

    expect(fetchMock).toHaveBeenCalledTimes(1);
    const [, options] = fetchMock.mock.calls[0];
    expect(new Headers(options.headers).get("Authorization")).toBe("Bearer bearer-token");
  });

  it("initializes auth state from the auth config endpoint", async () => {
    const authModule = await loadAuth();
    vi.spyOn(window, "setInterval").mockReturnValue(1234);
    vi.stubGlobal(
      "fetch",
      vi.fn(async () =>
        new Response(JSON.stringify({ enabled: false, login_url: "", reset_url: "" }), {
          status: 200,
          headers: { "content-type": "application/json" },
        }),
      ),
    );

    await authModule.initAuth();

    expect(authModule.auth.ready).toBe(true);
    expect(authModule.auth.enabled).toBe(false);
  });

  it("falls back to the keycloak-js constructor when no injected factory exists", async () => {
    const client = {
      authenticated: true,
      token: "mock-token",
      tokenParsed: {
        preferred_username: "bob",
        email: "bob@example.dev",
        email_verified: true,
        groups: ["/ops"],
      },
      init: vi.fn(async () => true),
      login: vi.fn(async () => {}),
      logout: vi.fn(async () => {}),
      updateToken: vi.fn(async () => true),
    };
    globalThis.__ATLAS_KEYCLOAK_CONSTRUCTOR__ = function MockKeycloak() {
      return client;
    };

    const authModule = await loadAuth();
    const created = authModule.createKeycloak({ url: "https://sso.example.dev" });

    expect(created).toBe(client);
  });

  it("uses the real Keycloak constructor when no test hooks are present", async () => {
    const authModule = await loadAuth();
    const client = authModule.createKeycloak({
      url: "https://sso.example.dev",
      realm: "atlas",
      clientId: "portal-client",
    });

    expect(client).toHaveProperty("init");
    expect(client).toHaveProperty("login");
    expect(client).toHaveProperty("logout");
  });

  it("reuses the auth initialization promise and tolerates empty token fields", async () => {
    const client = {
      authenticated: false,
      token: "",
      tokenParsed: undefined,
      init: vi.fn(async () => true),
      login: vi.fn(async () => {}),
      logout: vi.fn(async () => {}),
      updateToken: vi.fn(async () => true),
    };
    globalThis.__ATLAS_KEYCLOAK_FACTORY__ = () => client;

    const authModule = await loadAuth();
    vi.spyOn(window, "setInterval").mockReturnValue(1234);
    vi.stubGlobal(
      "fetch",
      vi.fn(async () =>
        new Response(
          JSON.stringify({
            enabled: true,
            url: "https://sso.example.dev",
            realm: "atlas",
            client_id: "portal-client",
            login_url: "https://sso.example.dev/login",
            reset_url: "https://sso.example.dev/reset",
            account_url: "https://sso.example.dev/account",
            account_password_url: "https://sso.example.dev/account/#/security/signingin",
          }),
          {
            status: 200,
            headers: { "content-type": "application/json" },
          },
        ),
      ),
    );

    const first = authModule.initAuth();
    const second = authModule.initAuth();

    await first;
    await second;
    client.onAuthSuccess();

    expect(authModule.auth.authenticated).toBe(false);
    expect(authModule.auth.username).toBe("");
    expect(authModule.auth.email).toBe("");
    expect(authModule.auth.emailVerified).toBeNull();
    expect(authModule.auth.groups).toEqual([]);
  });

  it("hydrates and proxies keycloak actions when enabled", async () => {
    const calls = { init: 0, login: 0, logout: 0, updateToken: 0 };
    const client = {
      authenticated: true,
      token: "mock-token",
      tokenParsed: {
        preferred_username: "alice",
        email: "alice@example.dev",
        email_verified: true,
        groups: ["/dev", "/admin"],
      },
      init: vi.fn(async () => {
        calls.init += 1;
        return true;
      }),
      login: vi.fn(async () => {
        calls.login += 1;
      }),
      logout: vi.fn(async () => {
        calls.logout += 1;
      }),
      updateToken: vi.fn(async () => {
        calls.updateToken += 1;
        return true;
      }),
    };
    globalThis.__ATLAS_KEYCLOAK_FACTORY__ = () => client;

    const authModule = await loadAuth();
    vi.spyOn(window, "setInterval").mockReturnValue(1234);
    vi.stubGlobal(
      "fetch",
      vi.fn(async () =>
        new Response(
          JSON.stringify({
            enabled: true,
            url: "https://sso.example.dev",
            realm: "atlas",
            client_id: "portal-client",
            login_url: "https://sso.example.dev/login",
            reset_url: "https://sso.example.dev/reset",
            account_url: "https://sso.example.dev/account",
            account_password_url: "https://sso.example.dev/account/#/security/signingin",
          }),
          {
            status: 200,
            headers: { "content-type": "application/json" },
          },
        ),
      ),
    );

    await authModule.initAuth();
    await authModule.login("/account", "   ");
    await authModule.login("/account", "alice");
    await authModule.logout();
    await authModule.authFetch("/api/healthz");

    expect(calls.init).toBe(1);
    expect(calls.login).toBe(2);
    expect(calls.logout).toBe(1);
    expect(calls.updateToken).toBeGreaterThan(0);
    expect(client.login.mock.calls[0][0]).not.toHaveProperty("loginHint");
    expect(client.login.mock.calls[1][0]).toMatchObject({ loginHint: "alice" });
    expect(authModule.auth.ready).toBe(true);
    expect(authModule.auth.enabled).toBe(true);
    expect(authModule.auth.authenticated).toBe(true);
    expect(authModule.auth.username).toBe("alice");
    expect(authModule.auth.email).toBe("alice@example.dev");
    expect(authModule.auth.emailVerified).toBe(true);
    expect(authModule.auth.groups).toEqual(["dev", "admin"]);
    expect(authModule.auth.token).toBe("mock-token");
  });

  it("covers the auth refresh handlers and polling branches", async () => {
    const intervalCalls = [];
    const client = {
      authenticated: true,
      token: "refresh-token",
      tokenParsed: {
        preferred_username: "carol",
        email: "carol@example.dev",
        email_verified: false,
        groups: ["/ops"],
      },
      init: vi.fn(async () => true),
      login: vi.fn(async () => {}),
      logout: vi.fn(async () => {}),
      updateToken: vi.fn(async () => true),
    };
    globalThis.__ATLAS_KEYCLOAK_FACTORY__ = () => client;

    const authModule = await loadAuth();
    vi.spyOn(window, "setInterval").mockImplementation((callback) => {
      intervalCalls.push(callback);
      return 1234;
    });
    vi.stubGlobal(
      "fetch",
      vi.fn(async () =>
        new Response(
          JSON.stringify({
            enabled: true,
            url: "https://sso.example.dev",
            realm: "atlas",
            client_id: "portal-client",
            login_url: "https://sso.example.dev/login",
            reset_url: "https://sso.example.dev/reset",
            account_url: "https://sso.example.dev/account",
            account_password_url: "https://sso.example.dev/account/#/security/signingin",
          }),
          {
            status: 200,
            headers: { "content-type": "application/json" },
          },
        ),
      ),
    );

    await authModule.initAuth();
    expect(intervalCalls).toHaveLength(1);

    client.onAuthSuccess();
    client.onAuthLogout();
    client.onAuthRefreshSuccess();

    client.updateToken.mockResolvedValueOnce(true);
    client.onTokenExpired();
    await Promise.resolve();

    client.updateToken.mockRejectedValueOnce(new Error("boom"));
    client.onTokenExpired();
    await Promise.resolve();

    client.updateToken.mockResolvedValueOnce(true);
    intervalCalls[0]();
    await Promise.resolve();

    client.authenticated = false;
    intervalCalls[0]();
    await Promise.resolve();

    client.authenticated = true;
    client.updateToken.mockRejectedValueOnce(new Error("refresh failed"));
    await authModule.authFetch("/api/healthz");

    expect(authModule.auth.username).toBe("carol");
    expect(authModule.auth.emailVerified).toBe(false);
    expect(authModule.auth.groups).toEqual(["ops"]);
    expect(client.updateToken).toHaveBeenCalled();
  });

  it("leaves auth alone when login/logout are called before initialization", async () => {
    const authModule = await loadAuth();
    const assign = vi.fn();
    Object.defineProperty(window, "location", {
      configurable: true,
      value: { ...window.location, assign },
    });
    authModule.auth.loginUrl = "https://sso.example.dev/login";
    const fetchMock = vi.fn(async () => new Response("{}", { status: 200 }));
    vi.stubGlobal("fetch", fetchMock);

    await authModule.login("/account", "alice");
    await authModule.logout();
    await authModule.authFetch("/api/healthz", { headers: { "X-Test": "1" } });

    expect(fetchMock).toHaveBeenCalledTimes(1);
    const [, options] = fetchMock.mock.calls[0];
    const headers = new Headers(options.headers);
    expect(headers.get("X-Test")).toBe("1");
    expect(headers.get("Authorization")).toBeNull();
    expect(assign).toHaveBeenCalledWith("https://sso.example.dev/login");
  });

  it("recovers when the auth config endpoint fails", async () => {
    const authModule = await loadAuth();
    vi.stubGlobal("fetch", vi.fn(async () => new Response("boom", { status: 500 })));

    await authModule.initAuth();

    expect(authModule.auth.ready).toBe(true);
    expect(authModule.auth.enabled).toBe(false);
    expect(authModule.auth.authenticated).toBe(false);
  });
});
