import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { createLiveRepos } from "./repos";
import { loadTokens, saveTokens } from "./http";

function installLocalStorage() {
  const store = new Map<string, string>();
  const shim: Storage = {
    get length() {
      return store.size;
    },
    clear: () => store.clear(),
    getItem: (key) => store.get(key) ?? null,
    key: (index) => [...store.keys()][index] ?? null,
    removeItem: (key) => void store.delete(key),
    setItem: (key, value) => void store.set(key, String(value)),
  };
  Object.defineProperty(globalThis, "localStorage", {
    value: shim,
    configurable: true,
    writable: true,
  });
}

// The auth bootstrap treats a null result from me() as "signed out" and sends the user to the
// login page. Returning null for a network blip therefore logged people out mid-session, and
// signing in again would not have worked either because the backend was the thing that was down.
describe("auth.me session handling", () => {
  const originalFetch = globalThis.fetch;
  const tokens = { access_token: "access-1", refresh_token: "refresh-1" };

  beforeEach(() => {
    installLocalStorage();
    saveTokens(tokens);
  });

  afterEach(() => {
    globalThis.fetch = originalFetch;
  });

  it("returns null and clears tokens when the backend rejects the session", async () => {
    globalThis.fetch = (async () =>
      new Response(JSON.stringify({ code: 401002, message: "invalid token", data: null, error: null }), {
        status: 401,
        headers: { "Content-Type": "application/json" },
      })) as typeof fetch;

    await expect(createLiveRepos().auth.me()).resolves.toBeNull();
    expect(loadTokens()).toBeNull();
  });

  it("throws and keeps tokens when the network is down", async () => {
    globalThis.fetch = (async () => {
      throw new TypeError("Failed to fetch");
    }) as typeof fetch;

    await expect(createLiveRepos().auth.me()).rejects.toThrow();
    expect(loadTokens()?.access_token).toBe("access-1");
  });

  it("throws and keeps tokens when the backend is restarting", async () => {
    globalThis.fetch = (async () =>
      new Response("<html>502 Bad Gateway</html>", {
        status: 502,
        headers: { "Content-Type": "text/html" },
      })) as typeof fetch;

    await expect(createLiveRepos().auth.me()).rejects.toThrow();
    expect(loadTokens()?.refresh_token).toBe("refresh-1");
  });

  it("keeps tokens when the access token expired but the refresh call could not reach the server", async () => {
    globalThis.fetch = (async (input: RequestInfo | URL) => {
      if (String(input).endsWith("/auth/refresh")) throw new TypeError("Failed to fetch");
      return new Response(JSON.stringify({ code: 401002, message: "expired", data: null, error: null }), {
        status: 401,
        headers: { "Content-Type": "application/json" },
      });
    }) as typeof fetch;

    await expect(createLiveRepos().auth.me()).rejects.toThrow();
    expect(loadTokens()?.refresh_token).toBe("refresh-1");
  });

  it("clears tokens when the refresh endpoint says the refresh token is invalid", async () => {
    globalThis.fetch = (async (input: RequestInfo | URL) =>
      String(input).endsWith("/auth/refresh")
        ? new Response(JSON.stringify({ code: 401002, message: "invalid refresh", data: null, error: null }), {
            status: 401,
            headers: { "Content-Type": "application/json" },
          })
        : new Response(JSON.stringify({ code: 401002, message: "expired", data: null, error: null }), {
            status: 401,
            headers: { "Content-Type": "application/json" },
          })) as typeof fetch;

    await expect(createLiveRepos().auth.me()).resolves.toBeNull();
    expect(loadTokens()).toBeNull();
  });

  it("retries the original request with the renewed token", async () => {
    const seen: string[] = [];
    globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
      const url = String(input);
      seen.push(`${url}:${(init?.headers as Record<string, string>)?.Authorization ?? ""}`);
      if (url.endsWith("/auth/refresh")) {
        return new Response(
          JSON.stringify({
            code: 102000,
            message: "ok",
            data: { tokens: { access_token: "access-2", refresh_token: "refresh-2" } },
            error: null,
          }),
          { status: 200, headers: { "Content-Type": "application/json" } },
        );
      }
      if (seen.length === 1) {
        return new Response(JSON.stringify({ code: 401002, message: "expired", data: null, error: null }), {
          status: 401,
          headers: { "Content-Type": "application/json" },
        });
      }
      return new Response(
        JSON.stringify({ code: 102000, message: "ok", data: { uid: 7, email: "a@b.c" }, error: null }),
        { status: 200, headers: { "Content-Type": "application/json" } },
      );
    }) as typeof fetch;

    await expect(createLiveRepos().auth.me()).resolves.not.toBeNull();
    expect(seen[2]).toContain("Bearer access-2");
    expect(loadTokens()?.access_token).toBe("access-2");
  });
});
