import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { API_BASE_URL, API_CONTENT_TYPES, API_HEADERS } from '@/shared/constants/api';
import { AuthExpiredError, ApiError, apiRequest } from '@/shared/api/httpClient';

interface RequestSnapshot {
  readonly input: Parameters<typeof fetch>[0];
  readonly init: Parameters<typeof fetch>[1];
}

function createJsonResponse(body: unknown, init?: ResponseInit): Response {
  return Response.json(body, init);
}

function stubFetch(response: Response): readonly RequestSnapshot[] {
  const requests: RequestSnapshot[] = [];
  const fetchMock: typeof fetch = async (input, init) => {
    requests.push({ input, init });
    return response;
  };

  vi.stubGlobal('fetch', fetchMock);
  return requests;
}

function stubFetchSequence(responses: readonly Response[]): readonly RequestSnapshot[] {
  const requests: RequestSnapshot[] = [];
  const fetchMock: typeof fetch = async (input, init) => {
    const response = responses[requests.length];
    requests.push({ input, init });
    return response;
  };

  vi.stubGlobal('fetch', fetchMock);
  return requests;
}

describe('http client', () => {
  beforeEach(() => {
    vi.stubGlobal('window', {});
  });

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

  it('sends JSON requests with browser credentials', async () => {
    const requests = stubFetch(createJsonResponse({ id: 'created' }));

    await expect(
      apiRequest('/frame_entries', {
        method: 'POST',
        body: { formal: 'Plan' },
      }),
    ).resolves.toEqual({ id: 'created' });

    const request = requests[0];
    const headers = new Headers(request.init?.headers);

    expect(request.input).toBe(`${API_BASE_URL}/frame_entries`);
    expect(request.init?.credentials).toBe('include');
    expect(request.init?.method).toBe('POST');
    expect(headers.get(API_HEADERS.contentType)).toBe(API_CONTENT_TYPES.json);
    expect(headers.has('Authorization')).toBe(false);
    expect(request.init?.body).toBe('{"formal":"Plan"}');
  });

  it('normalizes paths without a leading slash and defaults to GET', async () => {
    const requests = stubFetch(createJsonResponse({ ok: true }));

    await expect(apiRequest('auth/me')).resolves.toEqual({ ok: true });

    expect(requests[0].input).toBe(`${API_BASE_URL}/auth/me`);
    expect(requests[0].init?.credentials).toBe('include');
    expect(requests[0].init?.method).toBe('GET');
  });

  it('returns undefined for successful empty responses', async () => {
    stubFetch(new Response(null, { status: 204 }));

    await expect(apiRequest('/documents/doc-1', { method: 'DELETE' })).resolves.toBeUndefined();
  });

  it('preserves structured backend error details', async () => {
    stubFetch(createJsonResponse(
      {
        message: 'Validation failed',
        code: 'VALIDATION_ERROR',
        details: { field: 'email' },
      },
      { status: 422 },
    ));

    await expect(apiRequest('/auth/signup', { method: 'POST', body: { email: 'bad' } })).rejects.toMatchObject({
      name: 'ApiError',
      status: 422,
      code: 'VALIDATION_ERROR',
      details: { field: 'email' },
      message: 'Validation failed',
    });
  });

  it('uses text error responses when the backend does not return JSON', async () => {
    stubFetch(new Response('Service unavailable', {
      status: 503,
      headers: {
        [API_HEADERS.contentType]: 'text/plain',
      },
    }));

    await expect(apiRequest('/health')).rejects.toEqual(new ApiError(503, {
      message: 'Service unavailable',
    }));
  });

  it('refreshes once and retries the original request after an auth failure', async () => {
    const requests = stubFetchSequence([
      createJsonResponse({ message: 'Unauthorized' }, { status: 401 }),
      createJsonResponse({ id: 'user-1' }),
      createJsonResponse({ id: 'frame-1' }),
    ]);

    await expect(apiRequest('/frame_entries')).resolves.toEqual({ id: 'frame-1' });

    expect(requests).toHaveLength(3);
    expect(requests[0].input).toBe(`${API_BASE_URL}/frame_entries`);
    expect(requests[1].input).toBe(`${API_BASE_URL}/auth/refresh`);
    expect(requests[1].init?.method).toBe('POST');
    expect(requests[2].input).toBe(`${API_BASE_URL}/frame_entries`);
  });

  it('throws auth-expired when refresh credentials are invalid', async () => {
    stubFetchSequence([
      createJsonResponse({ message: 'Unauthorized' }, { status: 401 }),
      createJsonResponse({ message: 'Unauthorized' }, { status: 401 }),
    ]);

    await expect(apiRequest('/frame_entries')).rejects.toEqual(new AuthExpiredError());
  });

  it('does not loop when the retried request still fails with auth status', async () => {
    const requests = stubFetchSequence([
      createJsonResponse({ message: 'Unauthorized' }, { status: 401 }),
      createJsonResponse({ id: 'user-1' }),
      createJsonResponse({ message: 'Unauthorized' }, { status: 401 }),
    ]);

    await expect(apiRequest('/frame_entries')).rejects.toEqual(new AuthExpiredError());

    expect(requests).toHaveLength(3);
  });

  it('does not recursively refresh refresh requests', async () => {
    const requests = stubFetchSequence([
      createJsonResponse({ message: 'Unauthorized' }, { status: 401 }),
    ]);

    await expect(
      apiRequest('/auth/refresh', {
        method: 'POST',
        skipAuthRefresh: true,
      }),
    ).rejects.toEqual(new AuthExpiredError());

    expect(requests).toHaveLength(1);
  });
});
