/* eslint-disable @typescript-eslint/no-explicit-any */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { nextTick } from 'vue';
import type { FetchError } from 'ofetch';
import type { ApiError } from '~/types/api/error';
import { useApi } from './useApi';

// Mock dependencies
vi.mock('#app', () => ({
  useRuntimeConfig: vi.fn(() => ({
    public: {
      apiBase: 'http://localhost:3100/api',
    },
  })),
}));

// Mock $fetch globally
const mockFetch = vi.fn();
vi.stubGlobal('$fetch', mockFetch);

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

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

  describe('GET requests', () => {
    it('should make a GET request and populate data on success', async () => {
      const mockData = { id: 1, name: 'Test' };
      mockFetch.mockResolvedValueOnce(mockData);

      const api = useApi();
      const result = api.get<typeof mockData>('/test');

      // Should start loading
      await nextTick();
      expect(result.loading.value).toBe(false); // Immediate execution completes quickly

      // Wait for the async operation
      await vi.waitFor(() => expect(result.data.value).toStrictEqual(mockData));

      expect(mockFetch).toHaveBeenCalledWith('/test', {
        baseURL: 'http://localhost:3100/api',
        method: 'GET',
        body: undefined,
      });
      expect(result.data.value).toEqual(mockData);
      expect(result.error.value).toBeNull();
      expect(result.loading.value).toBe(false);
    });

    it('should handle GET request with custom options', async () => {
      const mockData = { result: 'success' };
      mockFetch.mockResolvedValueOnce(mockData);

      const api = useApi();
      const result = api.get('/test', { headers: { 'X-Custom': 'header' } });

      await vi.waitFor(() => expect(result.data.value).toStrictEqual(mockData));

      expect(mockFetch).toHaveBeenCalledWith('/test', {
        baseURL: 'http://localhost:3100/api',
        method: 'GET',
        headers: { 'X-Custom': 'header' },
        body: undefined,
      });
    });

    it('should not execute immediately when immediate is false', async () => {
      const api = useApi();
      const result = api.get('/test', {}, false);

      expect(mockFetch).not.toHaveBeenCalled();
      expect(result.data.value).toBeNull();
      expect(result.loading.value).toBe(false);
    });

    it('should execute when run() is called manually', async () => {
      const mockData = { manual: true };
      mockFetch.mockResolvedValueOnce(mockData);

      const api = useApi();
      const result = api.get('/test', {}, false);

      expect(mockFetch).not.toHaveBeenCalled();

      await result.run();

      expect(mockFetch).toHaveBeenCalledWith('/test', {
        baseURL: 'http://localhost:3100/api',
        method: 'GET',
        body: undefined,
      });
      expect(result.data.value).toEqual(mockData);
    });
  });

  describe('DELETE requests', () => {
    it('should make a DELETE request', async () => {
      const mockData = { deleted: true };
      mockFetch.mockResolvedValueOnce(mockData);

      const api = useApi();
      const result = api.del<typeof mockData>('/test/1');

      await vi.waitFor(() => expect(result.data.value).toStrictEqual(mockData));

      expect(mockFetch).toHaveBeenCalledWith('/test/1', {
        baseURL: 'http://localhost:3100/api',
        method: 'DELETE',
        body: undefined,
      });
      expect(result.data.value).toEqual(mockData);
    });
  });

  describe('POST requests', () => {
    it('should make a POST request with body', async () => {
      const mockResponse = { id: 1, created: true };
      const requestBody = { name: 'New Item' };
      mockFetch.mockResolvedValueOnce(mockResponse);

      const api = useApi();
      const result = api.post<typeof mockResponse, typeof requestBody>('/test', {}, true, requestBody);

      await vi.waitFor(() => expect(result.data.value).toStrictEqual(mockResponse));

      expect(mockFetch).toHaveBeenCalledWith('/test', {
        baseURL: 'http://localhost:3100/api',
        method: 'POST',
        body: requestBody,
      });
      expect(result.data.value).toEqual(mockResponse);
    });

    it('should allow run() to be called with a different body', async () => {
      const mockResponse = { success: true };
      mockFetch.mockResolvedValueOnce(mockResponse);

      const api = useApi();
      const result = api.post<typeof mockResponse, { data: string }>('/test', {}, false);

      const body = { data: 'runtime-data' };
      await result.run(body);

      expect(mockFetch).toHaveBeenCalledWith('/test', {
        baseURL: 'http://localhost:3100/api',
        method: 'POST',
        body,
      });
      expect(result.data.value).toEqual(mockResponse);
    });
  });

  describe('PUT requests', () => {
    it('should make a PUT request with body', async () => {
      const mockResponse = { updated: true };
      const requestBody = { name: 'Updated Item' };
      mockFetch.mockResolvedValueOnce(mockResponse);

      const api = useApi();
      const result = api.put<typeof mockResponse, typeof requestBody>('/test/1', {}, true, requestBody);

      await vi.waitFor(() => expect(result.data.value).toStrictEqual(mockResponse));

      expect(mockFetch).toHaveBeenCalledWith('/test/1', {
        baseURL: 'http://localhost:3100/api',
        method: 'PUT',
        body: requestBody,
      });
    });
  });

  describe('PATCH requests', () => {
    it('should make a PATCH request with body', async () => {
      const mockResponse = { patched: true };
      const requestBody = { field: 'value' };
      mockFetch.mockResolvedValueOnce(mockResponse);

      const api = useApi();
      const result = api.patch<typeof mockResponse, typeof requestBody>('/test/1', {}, true, requestBody);

      await vi.waitFor(() => expect(result.data.value).toStrictEqual(mockResponse));

      expect(mockFetch).toHaveBeenCalledWith('/test/1', {
        baseURL: 'http://localhost:3100/api',
        method: 'PATCH',
        body: requestBody,
      });
    });
  });

  describe('Error handling', () => {
    it('should handle fetch errors with ApiError response', async () => {
      const apiError: ApiError = {
        message: 'backend.errors.not_found',
        success: false,
      };

      const fetchError: Partial<FetchError> = {
        message: 'Fetch Error',
        response: {
          _data: apiError,
        } as any,
      };

      mockFetch.mockRejectedValueOnce(fetchError);

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

      await vi.waitFor(() => expect(result.error.value).not.toBeNull());

      expect(result.data.value).toBeNull();
      expect(result.error.value).toEqual(apiError);
      expect(result.loading.value).toBe(false);
    });

    it('should handle fetch errors without ApiError response', async () => {
      const fetchError: Partial<FetchError> = {
        message: 'Network Error',
        response: undefined,
      };

      mockFetch.mockRejectedValueOnce(fetchError);

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

      await vi.waitFor(() => expect(result.error.value).not.toBeNull());

      expect(result.error.value).toEqual({
        message: 'Network Error',
        success: false,
      });
    });

    it('should use default error message when fetch error has no message', async () => {
      const fetchError: Partial<FetchError> = {
        message: '',
      };

      mockFetch.mockRejectedValueOnce(fetchError);

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

      await vi.waitFor(() => expect(result.error.value).not.toBeNull());

      expect(result.error.value).toEqual({
        message: 'backend.errors.unknown',
        success: false,
      });
    });

    it('should clear previous errors on new request', async () => {
      const fetchError: Partial<FetchError> = {
        message: 'First Error',
      };
      const mockData = { success: true };

      // First request fails
      mockFetch.mockRejectedValueOnce(fetchError);

      const api = useApi();
      const result = api.get('/test', {}, false);

      await result.run();
      await vi.waitFor(() => expect(result.error.value).not.toBeNull());
      expect(result.error.value?.message).toBe('First Error');

      // Second request succeeds
      mockFetch.mockResolvedValueOnce(mockData);
      await result.run();

      await vi.waitFor(() => expect(result.data.value).toStrictEqual(mockData));
      expect(result.error.value).toBeNull();
    });
  });

  describe('Loading state', () => {
    it('should set loading to true during request', async () => {
      let resolvePromise: (value: any) => void;
      const promise = new Promise((resolve) => {
        resolvePromise = resolve;
      });

      mockFetch.mockReturnValueOnce(promise as any);

      const api = useApi();
      const result = api.get('/test', {}, false);

      expect(result.loading.value).toBe(false);

      const runPromise = result.run();

      // Should be loading
      await nextTick();
      expect(result.loading.value).toBe(true);

      // Resolve the request
      resolvePromise!({ done: true });
      await runPromise;

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

    it('should set loading to false after error', async () => {
      let rejectPromise: (error: any) => void;
      const promise = new Promise((_, reject) => {
        rejectPromise = reject;
      });

      mockFetch.mockReturnValueOnce(promise as any);

      const api = useApi();
      const result = api.get('/test', {}, false);

      const runPromise = result.run();

      await nextTick();
      expect(result.loading.value).toBe(true);

      rejectPromise!({ message: 'Error' });
      await runPromise;

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

  describe('Return type structure', () => {
    it('should return QueryResult with correct structure', async () => {
      mockFetch.mockResolvedValueOnce({ test: 'data' });

      const api = useApi();
      const result = api.get('/test', {}, false);

      expect(result).toHaveProperty('data');
      expect(result).toHaveProperty('error');
      expect(result).toHaveProperty('loading');
      expect(result).toHaveProperty('run');
      expect(typeof result.run).toBe('function');
    });
  });
});
