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

import type { apiClient as ApiClientType } from '../../src/services/api-client';

// Create a fresh ApiClient for each test by re-importing
let apiClient: typeof ApiClientType;

beforeEach(async () => {
  vi.restoreAllMocks();
  // Reset module to get a fresh singleton
  vi.resetModules();
  const mod = await import('../../src/services/api-client');
  apiClient = mod.apiClient;
});

function mockFetch(body: unknown, status = 200) {
  return vi.spyOn(globalThis, 'fetch').mockResolvedValue({
    ok: status >= 200 && status < 300,
    status,
    statusText: status === 200 ? 'OK' : 'Error',
    json: () => Promise.resolve(body),
  } as Response);
}

describe('ApiClient', () => {
  describe('token management', () => {
    it('hasToken returns false initially', () => {
      expect(apiClient.hasToken).toBe(false);
    });

    it('hasToken returns true after setting token', () => {
      apiClient.accessToken = 'test-token';
      expect(apiClient.hasToken).toBe(true);
    });

    it('includes Authorization header when token is set', async () => {
      const spy = mockFetch({ data: [] });
      apiClient.accessToken = 'my-jwt';

      await apiClient.get('/test');

      const headers = spy.mock.calls[0][1]?.headers as Record<string, string>;
      expect(headers['Authorization']).toBe('Bearer my-jwt');
    });

    it('omits Authorization header when no token', async () => {
      const spy = mockFetch({ data: [] });

      await apiClient.get('/test');

      const headers = spy.mock.calls[0][1]?.headers as Record<string, string>;
      expect(headers['Authorization']).toBeUndefined();
    });
  });

  describe('get', () => {
    it('makes GET request to correct URL', async () => {
      const spy = mockFetch({ id: '1' });

      await apiClient.get('/households/hh1/stores');

      expect(spy).toHaveBeenCalledWith(
        expect.stringContaining('/households/hh1/stores'),
        expect.objectContaining({ headers: expect.any(Object) }),
      );
    });

    it('returns parsed JSON body', async () => {
      mockFetch({ id: '1', name: 'CVS' });

      const result = await apiClient.get('/test');

      expect(result).toEqual({ id: '1', name: 'CVS' });
    });
  });

  describe('post', () => {
    it('makes POST request with JSON body', async () => {
      const spy = mockFetch({ id: '1' });
      const body = { name: 'Walgreens' };

      await apiClient.post('/stores', body);

      expect(spy).toHaveBeenCalledWith(
        expect.stringContaining('/stores'),
        expect.objectContaining({
          method: 'POST',
          body: JSON.stringify(body),
        }),
      );
    });

    it('handles post without body', async () => {
      const spy = mockFetch({ id: '1' });

      await apiClient.post('/test');

      expect(spy).toHaveBeenCalledWith(
        expect.anything(),
        expect.objectContaining({ body: undefined }),
      );
    });
  });

  describe('patch', () => {
    it('makes PATCH request with JSON body', async () => {
      const spy = mockFetch({ id: '1' });
      const body = { name: 'Updated' };

      await apiClient.patch('/stores/1', body);

      expect(spy).toHaveBeenCalledWith(
        expect.stringContaining('/stores/1'),
        expect.objectContaining({
          method: 'PATCH',
          body: JSON.stringify(body),
        }),
      );
    });
  });

  describe('delete', () => {
    it('makes DELETE request', async () => {
      const spy = mockFetch(undefined, 204);

      await apiClient.delete('/stores/1');

      expect(spy).toHaveBeenCalledWith(
        expect.stringContaining('/stores/1'),
        expect.objectContaining({ method: 'DELETE' }),
      );
    });
  });

  describe('error handling', () => {
    it('throws with message from error response body', async () => {
      mockFetch({ message: 'Store not found' }, 404);

      await expect(apiClient.get('/test')).rejects.toThrow('Store not found');
    });

    it('throws with status code when body has no message', async () => {
      mockFetch({}, 500);

      await expect(apiClient.get('/test')).rejects.toThrow('Request failed: 500');
    });

    it('throws with status text when JSON parsing fails', async () => {
      vi.spyOn(globalThis, 'fetch').mockResolvedValue({
        ok: false,
        status: 502,
        statusText: 'Bad Gateway',
        json: () => Promise.reject(new Error('parse error')),
      } as Response);

      await expect(apiClient.get('/test')).rejects.toThrow('Request failed: 502 Bad Gateway');
    });

    it('returns undefined for 204 No Content', async () => {
      mockFetch(null, 204);

      const result = await apiClient.get('/test');

      expect(result).toBeUndefined();
    });
  });
});
