/**
 * Tests for the useApi hook
 * @jest-environment jsdom
 */

import { renderHook, act, waitFor } from '@testing-library/react';
import { useApi, useFetchData } from '@/hooks/useApi';
import { api } from '@/services/api';

// Mock the api module
jest.mock('@/services/api', () => ({
  api: {
    get: jest.fn(),
    post: jest.fn(),
    put: jest.fn(),
    patch: jest.fn(),
    delete: jest.fn(),
  },
  ApiError: class ApiError extends Error {
    constructor(public status: number, message: string, public data?: unknown) {
      super(message);
      this.name = 'ApiError';
    }
  },
}));

// Mock react-hot-toast - define mock inside factory to avoid hoisting issues
jest.mock('react-hot-toast', () => {
  const mockToast = {
    error: jest.fn(),
    success: jest.fn(),
  };
  return {
    __esModule: true,
    default: mockToast,
    toast: mockToast,
    error: mockToast.error,
    success: mockToast.success,
  };
});

// Import to access the mock for assertions
import { toast } from 'react-hot-toast';

describe('useApi Hook', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  describe('initial state', () => {
    it('should have correct initial state', () => {
      const { result } = renderHook(() => useApi());

      expect(result.current.data).toBeNull();
      expect(result.current.loading).toBe(false);
      expect(result.current.error).toBeNull();
    });
  });

  describe('execute', () => {
    it('should execute GET request successfully', async () => {
      const mockData = { id: 1, name: 'Test' };
      (api.get as jest.Mock).mockResolvedValueOnce(mockData);

      const { result } = renderHook(() => useApi<typeof mockData>());

      let response: typeof mockData | null = null;
      await act(async () => {
        response = await result.current.execute('get', '/test');
      });

      expect(api.get).toHaveBeenCalledWith('/test');
      expect(result.current.data).toEqual(mockData);
      expect(result.current.loading).toBe(false);
      expect(result.current.error).toBeNull();
      expect(response).toEqual(mockData);
    });

    it('should execute POST request with data', async () => {
      const postData = { email: 'test@test.com' };
      const mockResponse = { success: true };
      (api.post as jest.Mock).mockResolvedValueOnce(mockResponse);

      const { result } = renderHook(() => useApi());

      await act(async () => {
        await result.current.execute('post', '/auth/login', postData);
      });

      expect(api.post).toHaveBeenCalledWith('/auth/login', postData);
      expect(result.current.data).toEqual(mockResponse);
    });

    it('should execute PUT request', async () => {
      const putData = { name: 'Updated' };
      (api.put as jest.Mock).mockResolvedValueOnce({ success: true });

      const { result } = renderHook(() => useApi());

      await act(async () => {
        await result.current.execute('put', '/users/1', putData);
      });

      expect(api.put).toHaveBeenCalledWith('/users/1', putData);
    });

    it('should execute PATCH request', async () => {
      const patchData = { status: 'active' };
      (api.patch as jest.Mock).mockResolvedValueOnce({ success: true });

      const { result } = renderHook(() => useApi());

      await act(async () => {
        await result.current.execute('patch', '/users/1', patchData);
      });

      expect(api.patch).toHaveBeenCalledWith('/users/1', patchData);
    });

    it('should execute DELETE request', async () => {
      (api.delete as jest.Mock).mockResolvedValueOnce({ success: true });

      const { result } = renderHook(() => useApi());

      await act(async () => {
        await result.current.execute('delete', '/users/1');
      });

      expect(api.delete).toHaveBeenCalledWith('/users/1');
    });

    it('should handle errors and show toast', async () => {
      const errorMessage = 'Something went wrong';
      (api.get as jest.Mock).mockRejectedValueOnce(new Error(errorMessage));

      const { result } = renderHook(() => useApi());

      await act(async () => {
        await result.current.execute('get', '/test');
      });

      expect(result.current.data).toBeNull();
      expect(result.current.loading).toBe(false);
      expect(result.current.error).toBe(errorMessage);
      expect(toast.error).toHaveBeenCalledWith(errorMessage);
    });

    it('should return null on error', async () => {
      (api.get as jest.Mock).mockRejectedValueOnce(new Error('Error'));

      const { result } = renderHook(() => useApi());

      let response: unknown = 'initial';
      await act(async () => {
        response = await result.current.execute('get', '/test');
      });

      expect(response).toBeNull();
    });

    it('should set loading state during request', async () => {
      let resolvePromise: (value: unknown) => void;
      const promise = new Promise((resolve) => {
        resolvePromise = resolve;
      });
      (api.get as jest.Mock).mockReturnValueOnce(promise);

      const { result } = renderHook(() => useApi());

      act(() => {
        result.current.execute('get', '/test');
      });

      // Should be loading immediately after calling execute
      expect(result.current.loading).toBe(true);

      // Resolve the promise
      await act(async () => {
        resolvePromise!({ data: 'test' });
        await promise;
      });

      expect(result.current.loading).toBe(false);
    });
  });

  describe('reset', () => {
    it('should reset state to initial values', async () => {
      const mockData = { id: 1 };
      (api.get as jest.Mock).mockResolvedValueOnce(mockData);

      const { result } = renderHook(() => useApi());

      await act(async () => {
        await result.current.execute('get', '/test');
      });

      expect(result.current.data).toEqual(mockData);

      act(() => {
        result.current.reset();
      });

      expect(result.current.data).toBeNull();
      expect(result.current.loading).toBe(false);
      expect(result.current.error).toBeNull();
    });
  });
});

describe('useFetchData Hook', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('should have correct initial state before refetch', () => {
    const { result } = renderHook(() => useFetchData('/test'));

    // Before calling refetch, data should be null and loading should be true
    expect(result.current.data).toBeNull();
    expect(result.current.loading).toBe(true);
    expect(result.current.error).toBeNull();
    expect(typeof result.current.refetch).toBe('function');
  });

  it('should extract data from response.data', async () => {
    const mockData = { id: 1, name: 'Test' };
    (api.get as jest.Mock).mockResolvedValue({ data: mockData });

    const { result } = renderHook(() => useFetchData<typeof mockData>('/test'));

    await act(async () => {
      await result.current.refetch();
    });

    expect(result.current.data).toEqual(mockData);
    expect(result.current.loading).toBe(false);
  });

  it('should use response directly if no data field', async () => {
    const mockData = { id: 1, name: 'Test' };
    (api.get as jest.Mock).mockResolvedValue(mockData);

    const { result } = renderHook(() => useFetchData<typeof mockData>('/test'));

    await act(async () => {
      await result.current.refetch();
    });

    expect(result.current.data).toEqual(mockData);
  });

  it('should call onSuccess callback', async () => {
    const mockData = { id: 1 };
    const onSuccess = jest.fn();
    (api.get as jest.Mock).mockResolvedValue({ data: mockData });

    const { result } = renderHook(() =>
      useFetchData('/test', { onSuccess })
    );

    await act(async () => {
      await result.current.refetch();
    });

    expect(onSuccess).toHaveBeenCalledWith(mockData);
  });

  it('should call onError callback on error', async () => {
    const errorMessage = 'Fetch failed';
    const onError = jest.fn();
    (api.get as jest.Mock).mockRejectedValue(new Error(errorMessage));

    const { result } = renderHook(() =>
      useFetchData('/test', { onError, showErrorToast: false })
    );

    await act(async () => {
      await result.current.refetch();
    });

    expect(onError).toHaveBeenCalledWith(errorMessage);
    expect(result.current.error).toBe(errorMessage);
  });

  it('should show error toast by default', async () => {
    (api.get as jest.Mock).mockRejectedValue(new Error('Error'));

    const { result } = renderHook(() => useFetchData('/test'));

    await act(async () => {
      await result.current.refetch();
    });

    expect(toast.error).toHaveBeenCalled();
  });

  it('should not show error toast when showErrorToast is false', async () => {
    (api.get as jest.Mock).mockRejectedValue(new Error('Error'));

    const { result } = renderHook(() =>
      useFetchData('/test', { showErrorToast: false })
    );

    await act(async () => {
      await result.current.refetch();
    });

    expect(toast.error).not.toHaveBeenCalled();
  });

  it('should allow refetching data', async () => {
    const mockData1 = { id: 1 };
    const mockData2 = { id: 2 };
    (api.get as jest.Mock)
      .mockResolvedValueOnce({ data: mockData1 })
      .mockResolvedValueOnce({ data: mockData2 });

    const { result } = renderHook(() => useFetchData('/test'));

    await act(async () => {
      await result.current.refetch();
    });

    expect(result.current.data).toEqual(mockData1);

    await act(async () => {
      await result.current.refetch();
    });

    expect(result.current.data).toEqual(mockData2);
  });
});
