// src/hooks/useApi.test.ts
import { renderHook, act, waitFor } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { useApi } from './useApi';
import { logger } from '../services/logger.client';
import { notifyError } from '../services/notificationService';

// Mock dependencies
const mockApiFunction = vi.fn();
vi.mock('../services/logger.client', () => ({
  logger: {
    error: vi.fn(),
    info: vi.fn(),
  },
}));
vi.mock('../services/notificationService', () => ({
  // We need to get a reference to the mock to check if it was called.
  notifyError: vi.fn(),
}));

describe('useApi Hook', () => {
  beforeEach(() => {
    console.log('--- Test Setup: Resetting Mocks ---');
    vi.resetAllMocks();
  });

  it('should initialize with correct default states', () => {
    const { result } = renderHook(() => useApi(mockApiFunction));

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

  it('should set loading to true and return data on successful execution', async () => {
    const mockData = { id: 1, name: 'Test' };
    mockApiFunction.mockResolvedValue(new Response(JSON.stringify(mockData)));

    const { result } = renderHook(() => useApi<typeof mockData, [string]>(mockApiFunction));

    let promise: Promise<typeof mockData | null>;
    act(() => {
      promise = result.current.execute('test-arg');
    });

    expect(result.current.loading).toBe(true);

    await act(async () => {
      await promise;
    });

    expect(result.current.loading).toBe(false);
    expect(result.current.data).toEqual(mockData);
    expect(result.current.error).toBeNull();
    expect(mockApiFunction).toHaveBeenCalledWith('test-arg', expect.any(AbortSignal));
  });

  it('should return the data from execute function on success', async () => {
    const mockData = { id: 1 };
    mockApiFunction.mockResolvedValue(new Response(JSON.stringify(mockData)));
    const { result } = renderHook(() => useApi(mockApiFunction));

    let returnedData;
    await act(async () => {
      returnedData = await result.current.execute();
    });
    expect(returnedData).toEqual(mockData);
  });

  it('should set error state on failed execution', async () => {
    const mockError = new Error('API Failure');
    mockApiFunction.mockRejectedValue(mockError);

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

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

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

  it('should return null from execute function on failure', async () => {
    mockApiFunction.mockRejectedValue(new Error('Fail'));
    const { result } = renderHook(() => useApi(mockApiFunction));

    let returnedData;
    await act(async () => {
      returnedData = await result.current.execute();
    });
    expect(returnedData).toBeNull();
  });

  it('should clear previous error when execute is called again', async () => {
    console.log('Test: should clear previous error when execute is called again');
    mockApiFunction.mockRejectedValueOnce(new Error('Fail'));

    // We use a controlled promise for the second call to assert state while it is pending
    let resolveSecondCall: (value: Response) => void;
    const secondCallPromise = new Promise<Response>((resolve) => {
      resolveSecondCall = resolve;
    });
    mockApiFunction.mockReturnValueOnce(secondCallPromise);

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

    // First call fails
    console.log('Step: Executing first call (expected failure)');
    await act(async () => {
      try {
        await result.current.execute();
      } catch {
        // We expect this to fail
      }
    });
    console.log('Step: First call finished. Error state:', result.current.error);
    expect(result.current.error).not.toBeNull();

    // Second call starts
    let executePromise: Promise<any>;
    console.log('Step: Starting second call');
    act(() => {
      executePromise = result.current.execute();
    });

    // Error should be cleared immediately upon execution start
    console.log('Step: Second call started. Error state (should be null):', result.current.error);
    expect(result.current.error).toBeNull();

    // Resolve the second call
    console.log('Step: Resolving second call promise');
    resolveSecondCall!(new Response(JSON.stringify({ success: true })));

    await act(async () => {
      await executePromise;
    });
    console.log('Step: Second call finished');
  });

  it('should handle 204 No Content responses correctly', async () => {
    mockApiFunction.mockResolvedValue(new Response(null, { status: 204 }));

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

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

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

  it('should reset the state to initial values when reset is called', async () => {
    const mockData = { id: 1, name: 'Test Data' };
    mockApiFunction.mockResolvedValue(new Response(JSON.stringify(mockData)));

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

    // First, execute to populate the state
    await act(async () => {
      await result.current.execute();
    });

    // Assert that state is populated
    expect(result.current.data).toEqual(mockData);
    expect(result.current.loading).toBe(false);

    // Now, call reset
    act(() => {
      result.current.reset();
    });

    // Assert that state is back to initial values
    expect(result.current.data).toBeNull();
    expect(result.current.loading).toBe(false);
    expect(result.current.isRefetching).toBe(false);
    expect(result.current.error).toBeNull();
  });

  describe('isRefetching state', () => {
    it('should set isRefetching to true on second call, but not first', async () => {
      console.log('Test: isRefetching state - success path');
      // First call setup
      let resolveFirst: (val: Response) => void;
      const firstPromise = new Promise<Response>((resolve) => {
        resolveFirst = resolve;
      });
      mockApiFunction.mockReturnValueOnce(firstPromise);

      const { result } = renderHook(() => useApi<{ data: string }, []>(mockApiFunction));

      // --- First call ---
      let firstCallPromise: Promise<any>;
      console.log('Step: Starting first call');
      act(() => {
        firstCallPromise = result.current.execute();
      });

      // During the first call, loading is true, but isRefetching is false
      console.log(
        'Check: First call in flight. loading:',
        result.current.loading,
        'isRefetching:',
        result.current.isRefetching,
      );
      expect(result.current.loading).toBe(true);
      expect(result.current.isRefetching).toBe(false);

      console.log('Step: Resolving first call');
      resolveFirst!(new Response(JSON.stringify({ data: 'first call' })));
      await act(async () => {
        await firstCallPromise;
      });

      // After the first call, both are false
      console.log(
        'Check: First call done. loading:',
        result.current.loading,
        'isRefetching:',
        result.current.isRefetching,
      );
      expect(result.current.loading).toBe(false);
      expect(result.current.isRefetching).toBe(false);
      expect(result.current.data).toEqual({ data: 'first call' });

      // --- Second call ---
      let resolveSecond: (val: Response) => void;
      const secondPromise = new Promise<Response>((resolve) => {
        resolveSecond = resolve;
      });
      mockApiFunction.mockReturnValueOnce(secondPromise);

      let secondCallPromise: Promise<any>;
      console.log('Step: Starting second call');
      act(() => {
        secondCallPromise = result.current.execute();
      });

      // During the second call, both loading and isRefetching are true
      console.log(
        'Check: Second call in flight. loading:',
        result.current.loading,
        'isRefetching:',
        result.current.isRefetching,
      );
      expect(result.current.loading).toBe(true);
      expect(result.current.isRefetching).toBe(true);

      console.log('Step: Resolving second call');
      resolveSecond!(new Response(JSON.stringify({ data: 'second call' })));
      await act(async () => {
        await secondCallPromise;
      });

      // After the second call, both are false again
      console.log(
        'Check: Second call done. loading:',
        result.current.loading,
        'isRefetching:',
        result.current.isRefetching,
      );
      expect(result.current.loading).toBe(false);
      expect(result.current.isRefetching).toBe(false);
      expect(result.current.data).toEqual({ data: 'second call' });
    });

    it('should not set isRefetching to true if the first call failed', async () => {
      console.log('Test: isRefetching state - failure path');
      // First call fails
      mockApiFunction.mockRejectedValueOnce(new Error('Fail'));
      const { result } = renderHook(() => useApi(mockApiFunction));

      console.log('Step: Executing first call (fail)');
      await act(async () => {
        try {
          await result.current.execute();
        } catch {}
      });
      expect(result.current.error).not.toBeNull();

      // Second call succeeds
      let resolveSecond: (val: Response) => void;
      const secondPromise = new Promise<Response>((resolve) => {
        resolveSecond = resolve;
      });
      mockApiFunction.mockReturnValueOnce(secondPromise);

      let secondCallPromise: Promise<any>;
      console.log('Step: Starting second call');
      act(() => {
        secondCallPromise = result.current.execute();
      });

      // Should still be loading (initial load behavior) because first load never succeeded
      console.log(
        'Check: Second call in flight. loading:',
        result.current.loading,
        'isRefetching:',
        result.current.isRefetching,
      );
      expect(result.current.loading).toBe(true);
      expect(result.current.isRefetching).toBe(false);

      console.log('Step: Resolving second call');
      resolveSecond!(new Response(JSON.stringify({ data: 'success' })));
      await act(async () => {
        await secondCallPromise;
      });
    });
  });

  describe('Error Response Handling', () => {
    it('should parse a simple JSON error message from a non-ok response', async () => {
      const errorPayload = { message: 'Server is on fire' };
      mockApiFunction.mockResolvedValue(
        new Response(JSON.stringify(errorPayload), { status: 500 }),
      );

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

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

      expect(result.current.error).toBeInstanceOf(Error);
      expect(result.current.error?.message).toBe('Server is on fire');
    });

    it('should parse a Zod-style error message array from a non-ok response', async () => {
      const errorPayload = {
        issues: [
          { path: ['body', 'email'], message: 'Invalid email' },
          { path: ['body', 'password'], message: 'Password too short' },
        ],
      };
      mockApiFunction.mockResolvedValue(
        new Response(JSON.stringify(errorPayload), { status: 400 }),
      );

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

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

      expect(result.current.error).toBeInstanceOf(Error);
      expect(result.current.error?.message).toBe(
        'body.email: Invalid email; body.password: Password too short',
      );
    });

    it('should handle Zod-style error issues without a path', async () => {
      const errorPayload = {
        issues: [{ message: 'Global error' }],
      };
      mockApiFunction.mockResolvedValue(
        new Response(JSON.stringify(errorPayload), { status: 400 }),
      );

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

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

      expect(result.current.error).toBeInstanceOf(Error);
      expect(result.current.error?.message).toBe('Error: Global error');
    });

    it('should fall back to status text if JSON parsing fails', async () => {
      mockApiFunction.mockResolvedValue(
        new Response('Gateway Timeout', {
          status: 504,
          statusText: 'Gateway Timeout',
        }),
      );

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

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

      expect(result.current.error).toBeInstanceOf(Error);
      expect(result.current.error?.message).toBe('Request failed with status 504: Gateway Timeout');
    });

    it('should fall back to status text if JSON response is valid but lacks error fields', async () => {
      // Valid JSON but no 'message' or 'issues'
      mockApiFunction.mockResolvedValue(
        new Response(JSON.stringify({ foo: 'bar' }), {
          status: 400,
          statusText: 'Bad Request',
        }),
      );

      const { result } = renderHook(() => useApi(mockApiFunction));
      await act(async () => {
        await result.current.execute();
      });

      expect(result.current.error).toBeInstanceOf(Error);
      expect(result.current.error?.message).toBe('Request failed with status 400: Bad Request');
    });

    it('should handle non-Error objects thrown by apiFunction', async () => {
      // Throwing a string instead of an Error object
      mockApiFunction.mockRejectedValue('String Error');

      const { result } = renderHook(() => useApi(mockApiFunction));
      await act(async () => {
        await result.current.execute();
      });

      expect(result.current.error).toBeInstanceOf(Error);
      // The hook wraps unknown errors
      expect(result.current.error?.message).toBe('An unknown error occurred.');
    });
  });

  describe('Request Cancellation', () => {
    it('should not set an error state if the request is aborted on unmount', async () => {
      console.log('Test: Request Cancellation');
      // Create a promise that we can control from outside
      const controlledPromise = new Promise<Response>(() => {
        // Never resolve
      });
      mockApiFunction.mockReturnValue(controlledPromise);

      const { result, unmount } = renderHook(() => useApi(mockApiFunction));

      // Start the API call
      console.log('Step: Executing call');
      act(() => {
        result.current.execute();
      });

      // The request is now in-flight
      expect(result.current.loading).toBe(true);

      // Unmount the component, which should trigger the AbortController
      console.log('Step: Unmounting');
      unmount();

      // The error should be null because the AbortError is caught and ignored
      console.log('Check: Error state after unmount:', result.current.error);
      expect(result.current.error).toBeNull();
    });
  });

  describe('Side Effects', () => {
    it('should call notifyError and logger.error on failure', async () => {
      const mockError = new Error('Boom');
      mockApiFunction.mockRejectedValue(mockError);

      const { result } = renderHook(() => useApi(mockApiFunction));
      await act(async () => {
        await result.current.execute();
      });

      expect(notifyError).toHaveBeenCalledWith('Boom');
      expect(logger.error).toHaveBeenCalledWith('API call failed in useApi hook', {
        error: 'Boom',
        functionName: 'Mock',
      });
    });

    it('should call logger.info on abort', async () => {
      // Mock implementation that rejects when the signal is aborted
      mockApiFunction.mockImplementation((...args: any[]) => {
        const signal = args[args.length - 1] as AbortSignal;
        return new Promise<Response>((_, reject) => {
          if (signal.aborted) {
            const err = new Error('Aborted');
            err.name = 'AbortError';
            reject(err);
          } else {
            signal.addEventListener('abort', () => {
              const err = new Error('Aborted');
              err.name = 'AbortError';
              reject(err);
            });
          }
        });
      });

      const { result, unmount } = renderHook(() => useApi(mockApiFunction));
      act(() => {
        result.current.execute();
      });
      unmount();

      await waitFor(() => {
        expect(logger.info).toHaveBeenCalledWith('API request was cancelled.', {
          functionName: 'Mock',
        });
      });
      expect(notifyError).not.toHaveBeenCalled();
    });
  });
});
