/**
 * Tests for the API service
 * @jest-environment jsdom
 */

import { api, ApiError } from '@/services/api';

// Mock fetch globally
const mockFetch = jest.fn();
global.fetch = mockFetch;

// Mock helpers
jest.mock('@/utils/helpers', () => ({
  getToken: jest.fn(),
}));

import { getToken } from '@/utils/helpers';

describe('API Service', () => {
  beforeEach(() => {
    jest.clearAllMocks();
    (getToken as jest.Mock).mockReturnValue(null);
  });

  describe('ApiError class', () => {
    it('should create an error with status and message', () => {
      const error = new ApiError(404, 'Not found');
      expect(error.status).toBe(404);
      expect(error.message).toBe('Not found');
      expect(error.name).toBe('ApiError');
    });

    it('should create an error with data', () => {
      const errorData = { field: 'email', reason: 'invalid' };
      const error = new ApiError(400, 'Validation error', errorData);
      expect(error.data).toEqual(errorData);
    });
  });

  describe('api.get', () => {
    it('should make a GET request without token', async () => {
      const mockResponse = { data: 'test' };
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => mockResponse,
      });

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

      expect(mockFetch).toHaveBeenCalledWith(
        'http://localhost:4000/api/test',
        expect.objectContaining({
          method: 'GET',
          headers: expect.objectContaining({
            'Content-Type': 'application/json',
          }),
        })
      );
      expect(result).toEqual(mockResponse);
    });

    it('should include Authorization header when token exists', async () => {
      (getToken as jest.Mock).mockReturnValue('test-token');
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({}),
      });

      await api.get('/test');

      expect(mockFetch).toHaveBeenCalledWith(
        expect.any(String),
        expect.objectContaining({
          headers: expect.objectContaining({
            Authorization: 'Bearer test-token',
          }),
        })
      );
    });

    it('should handle full URLs', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({}),
      });

      await api.get('http://external.api/endpoint');

      expect(mockFetch).toHaveBeenCalledWith(
        'http://external.api/endpoint',
        expect.any(Object)
      );
    });

    it('should throw ApiError on non-ok response', async () => {
      mockFetch.mockResolvedValue({
        ok: false,
        status: 401,
        statusText: 'Unauthorized',
        json: async () => ({ message: 'Invalid token' }),
      });

      await expect(api.get('/test')).rejects.toThrow(ApiError);

      // Need a fresh mock for the second assertion
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 401,
        statusText: 'Unauthorized',
        json: async () => ({ message: 'Invalid token' }),
      });

      await expect(api.get('/test')).rejects.toMatchObject({
        status: 401,
        message: 'Invalid token',
      });
    });

    it('should handle network errors', async () => {
      mockFetch.mockRejectedValue(new Error('Network error'));

      await expect(api.get('/test')).rejects.toThrow(ApiError);

      // Fresh mock for the second assertion
      mockFetch.mockRejectedValueOnce(new Error('Network error'));

      await expect(api.get('/test')).rejects.toMatchObject({
        status: 0,
        message: 'Erreur de connexion au serveur',
      });
    });
  });

  describe('api.post', () => {
    it('should make a POST request with data', async () => {
      const postData = { email: 'test@test.com', password: 'password' };
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({ success: true }),
      });

      await api.post('/auth/login', postData);

      expect(mockFetch).toHaveBeenCalledWith(
        'http://localhost:4000/api/auth/login',
        expect.objectContaining({
          method: 'POST',
          body: JSON.stringify(postData),
        })
      );
    });

    it('should make a POST request without data', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({ success: true }),
      });

      await api.post('/auth/logout');

      expect(mockFetch).toHaveBeenCalledWith(
        expect.any(String),
        expect.objectContaining({
          method: 'POST',
          body: undefined,
        })
      );
    });
  });

  describe('api.put', () => {
    it('should make a PUT request with data', async () => {
      const updateData = { name: 'New Name' };
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({ success: true }),
      });

      await api.put('/users/1', updateData);

      expect(mockFetch).toHaveBeenCalledWith(
        'http://localhost:4000/api/users/1',
        expect.objectContaining({
          method: 'PUT',
          body: JSON.stringify(updateData),
        })
      );
    });
  });

  describe('api.patch', () => {
    it('should make a PATCH request with data', async () => {
      const patchData = { status: 'active' };
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({ success: true }),
      });

      await api.patch('/users/1', patchData);

      expect(mockFetch).toHaveBeenCalledWith(
        'http://localhost:4000/api/users/1',
        expect.objectContaining({
          method: 'PATCH',
          body: JSON.stringify(patchData),
        })
      );
    });
  });

  describe('api.delete', () => {
    it('should make a DELETE request', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: async () => ({ success: true }),
      });

      await api.delete('/users/1');

      expect(mockFetch).toHaveBeenCalledWith(
        'http://localhost:4000/api/users/1',
        expect.objectContaining({
          method: 'DELETE',
        })
      );
    });
  });

  describe('Error handling', () => {
    it('should extract error message from response', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 400,
        statusText: 'Bad Request',
        json: async () => ({ message: 'Email already exists' }),
      });

      try {
        await api.post('/auth/register', {});
      } catch (error) {
        expect(error).toBeInstanceOf(ApiError);
        expect((error as ApiError).message).toBe('Email already exists');
      }
    });

    it('should use error field if message is not present', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 400,
        statusText: 'Bad Request',
        json: async () => ({ error: 'Validation failed' }),
      });

      try {
        await api.post('/auth/register', {});
      } catch (error) {
        expect((error as ApiError).message).toBe('Validation failed');
      }
    });

    it('should use statusText if JSON parsing fails', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 500,
        statusText: 'Internal Server Error',
        json: async () => { throw new Error('Invalid JSON'); },
      });

      try {
        await api.get('/test');
      } catch (error) {
        expect((error as ApiError).message).toBe('Internal Server Error');
      }
    });
  });
});
