/**
 * Unrecoverable-401 contract test.
 *
 * Regression guard for the "stuck on Authenticating…" bug: when the refresh
 * token is dead (expired, or blacklisted after rotation), the refresh handler
 * fails and the session is UNRECOVERABLE. The client must then:
 *   - report failure from the proactive path (`refreshNow()` → null), and
 *   - fire `auth.onUnauthorized(cb)` from the reactive (interceptor) path,
 * so the app can clear tokens and show the login form. If neither fires, stale
 * tokens sit in storage and the guard hangs forever.
 *
 * The store reads `window.localStorage` and captures `isBrowser` at import
 * time, so we install a minimal localStorage on globalThis BEFORE importing it.
 */

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

function installBrowserStorage() {
  const map = new Map<string, string>();
  const storage = {
    getItem: (k: string) => (map.has(k) ? map.get(k)! : null),
    setItem: (k: string, v: string) => void map.set(k, v),
    removeItem: (k: string) => void map.delete(k),
    clear: () => map.clear(),
  };
  vi.stubGlobal('window', { localStorage: storage, location: { protocol: 'http:' } });
  vi.stubGlobal('localStorage', storage);
  return map;
}

describe('unrecoverable 401 handling', () => {
  beforeEach(() => {
    vi.unstubAllGlobals();
    vi.resetModules();
  });

  it('refreshNow() returns null when the refresh handler fails (blacklisted token)', async () => {
    installBrowserStorage();
    const { auth } = await import('../../_api/generated/helpers/auth');

    auth.setToken('access-stale');
    auth.setRefreshToken('R-blacklisted');

    // Model a server that rejects the (blacklisted) refresh token.
    auth.setRefreshHandler(async () => {
      const err = new Error('Token is blacklisted') as Error & { code: string };
      err.code = 'token_not_valid';
      throw err;
    });

    const store = auth as unknown as { refreshNow?: () => Promise<string | null> };
    if (typeof store.refreshNow !== 'function') return; // pre-regen: skip gracefully

    const result = await store.refreshNow();
    expect(result).toBeNull(); // proactive path reports the dead session
  });

  it('refreshNow() returns null when there is no refresh token at all', async () => {
    installBrowserStorage();
    const { auth } = await import('../../_api/generated/helpers/auth');

    auth.setToken('access-orphan');
    // no refresh token set
    auth.setRefreshHandler(async () => ({ access: 'never', refresh: 'never' }));

    const store = auth as unknown as { refreshNow?: () => Promise<string | null> };
    if (typeof store.refreshNow !== 'function') return;

    expect(await store.refreshNow()).toBeNull();
  });

  it('onUnauthorized callback can be registered and cleared (wiring the login-form escape hatch)', async () => {
    installBrowserStorage();
    const { auth } = await import('../../_api/generated/helpers/auth');

    // The AuthContext relies on this being a no-throw registration surface.
    const cb = vi.fn();
    expect(() => auth.onUnauthorized(cb)).not.toThrow();
    expect(() => auth.onUnauthorized(null)).not.toThrow();
  });
});

/**
 * Reactive path: the response interceptor installed by installAuthOnClient must
 * call onUnauthorized(response) when a 401 cannot be recovered, and must NOT
 * call it when the refresh handler rescues the request. We install the auth
 * machinery onto a FAKE client that just captures the response interceptor, then
 * invoke it with a synthetic 401 — exercising the real recovery branch logic.
 */
describe('response interceptor → onUnauthorized (reactive 401 path)', () => {
  beforeEach(() => {
    vi.unstubAllGlobals();
    vi.resetModules();
  });

  async function installOnFakeClient() {
    installBrowserStorage();
    const { auth, installAuthOnClient } = await import('../../_api/generated/helpers/auth');

    let responseInterceptor:
      | ((res: Response, req: Request) => Promise<Response> | Response)
      | null = null;

    const fakeClient = {
      setConfig: () => {},
      interceptors: {
        request: { use: () => {} },
        response: {
          use: (fn: (res: Response, req: Request) => Promise<Response> | Response) => {
            responseInterceptor = fn;
          },
        },
        error: { use: () => {} },
      },
    };

    installAuthOnClient(fakeClient as never);
    return { auth, invoke: () => responseInterceptor! };
  }

  function res401(): Response {
    return new Response(JSON.stringify({ detail: 'Token is blacklisted', code: 'token_not_valid' }), {
      status: 401,
      headers: { 'content-type': 'application/json' },
    });
  }
  function req(url = 'http://localhost:8000/cfg/accounts/profile/'): Request {
    return new Request(url, { method: 'GET' });
  }

  it('fires onUnauthorized when there is NO refresh handler / token', async () => {
    const { auth, invoke } = await installOnFakeClient();
    const onUnauth = vi.fn();
    auth.onUnauthorized(onUnauth);
    // No refresh token, no handler → tryRefresh returns null → onUnauthorized fires.

    const out = await invoke()(res401(), req());
    expect(onUnauth).toHaveBeenCalledTimes(1);
    expect(out.status).toBe(401); // original 401 surfaced
  });

  it('fires onUnauthorized when the refresh handler FAILS (blacklisted token)', async () => {
    const { auth, invoke } = await installOnFakeClient();
    auth.setToken('access-stale');
    auth.setRefreshToken('R-dead');
    auth.setRefreshHandler(async () => {
      throw new Error('Token is blacklisted');
    });
    const onUnauth = vi.fn();
    auth.onUnauthorized(onUnauth);

    const out = await invoke()(res401(), req());
    expect(onUnauth).toHaveBeenCalledTimes(1);
    expect(out.status).toBe(401);
  });

  it('does NOT fire onUnauthorized on a non-401 response', async () => {
    const { auth, invoke } = await installOnFakeClient();
    const onUnauth = vi.fn();
    auth.onUnauthorized(onUnauth);

    const ok = new Response('{}', { status: 200 });
    const out = await invoke()(ok, req());
    expect(onUnauth).not.toHaveBeenCalled();
    expect(out.status).toBe(200);
  });

  it('does NOT fire onUnauthorized when refresh SUCCEEDS and retry is attempted', async () => {
    const { auth, invoke } = await installOnFakeClient();
    auth.setToken('access-stale');
    auth.setRefreshToken('R-good');
    auth.setRefreshHandler(async () => ({ access: 'access-fresh', refresh: 'R-rotated' }));
    const onUnauth = vi.fn();
    auth.onUnauthorized(onUnauth);

    // The interceptor retries via global fetch after a successful refresh.
    // Stub fetch to return 200 for the retry so onUnauthorized is not reached.
    const fetchMock = vi.fn(async () => new Response('{}', { status: 200 }));
    vi.stubGlobal('fetch', fetchMock);

    const out = await invoke()(res401(), req());
    expect(fetchMock).toHaveBeenCalledTimes(1); // retry happened
    expect(onUnauth).not.toHaveBeenCalled(); // recovered → no logout
    expect(out.status).toBe(200);
    expect(auth.getToken()).toBe('access-fresh'); // rotated tokens persisted
    expect(auth.getRefreshToken()).toBe('R-rotated');
  });

  it('fires onUnauthorized when the retry STILL returns 401', async () => {
    const { auth, invoke } = await installOnFakeClient();
    auth.setToken('access-stale');
    auth.setRefreshToken('R-good');
    auth.setRefreshHandler(async () => ({ access: 'access-fresh', refresh: 'R-rotated' }));
    const onUnauth = vi.fn();
    auth.onUnauthorized(onUnauth);

    // Refresh succeeds but the retried request is still rejected (401).
    const fetchMock = vi.fn(async () => new Response('{}', { status: 401 }));
    vi.stubGlobal('fetch', fetchMock);

    const out = await invoke()(res401(), req());
    expect(fetchMock).toHaveBeenCalledTimes(1);
    expect(onUnauth).toHaveBeenCalledTimes(1); // dead even after refresh+retry
    expect(out.status).toBe(401);
  });
});
