import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook, waitFor } from '@testing-library/react';
import { useAsync } from './useAsync';
import * as errorHandling from '../utils/errorHandling';

// Mock the errorHandling module
vi.mock('../utils/errorHandling', async () => {
  const actual = await vi.importActual('../utils/errorHandling');
  return {
    ...actual,
    handleError: vi.fn((error: unknown) => {
      if (error instanceof Error) {
        return error;
      }
      return new Error('Handled error');
    })
  };
});

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

  describe('initial state', () => {
    it('should initialize with null data, null error, and loading false', () => {
      const { result } = renderHook(() => useAsync<string>());

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

    it('should provide an execute function', () => {
      const { result } = renderHook(() => useAsync<string>());

      expect(result.current.execute).toBeInstanceOf(Function);
    });
  });

  describe('loading states', () => {
    it('should set loading to true when executing', async () => {
      const { result } = renderHook(() => useAsync<string>());

      const asyncFn = vi.fn(() => new Promise<string>((resolve) => {
        setTimeout(() => resolve('test data'), 100);
      }));

      result.current.execute(asyncFn);

      // Loading should be true immediately after execute is called
      expect(result.current.loading).toBe(true);
      expect(result.current.data).toBeNull();
      expect(result.current.error).toBeNull();

      await waitFor(() => {
        expect(result.current.loading).toBe(false);
      });
    });

    it('should set loading to false after successful execution', async () => {
      const { result } = renderHook(() => useAsync<string>());

      const asyncFn = vi.fn(async () => 'test data');

      await result.current.execute(asyncFn);

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

    it('should set loading to false after failed execution', async () => {
      const { result } = renderHook(() => useAsync<string>());

      const error = new Error('Test error');
      const asyncFn = vi.fn(async () => {
        throw error;
      });

      try {
        await result.current.execute(asyncFn);
      } catch (e) {
        // Expected to throw
      }

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

    it('should reset data and error when starting new execution', async () => {
      const { result } = renderHook(() => useAsync<string>());

      // First execution with data
      await result.current.execute(async () => 'first data');

      expect(result.current.data).toBe('first data');
      expect(result.current.error).toBeNull();

      // Second execution that will take some time
      const slowAsyncFn = vi.fn(() => new Promise<string>((resolve) => {
        setTimeout(() => resolve('second data'), 100);
      }));

      result.current.execute(slowAsyncFn);

      // Immediately after calling execute, state should be reset
      expect(result.current.data).toBeNull();
      expect(result.current.error).toBeNull();
      expect(result.current.loading).toBe(true);

      await waitFor(() => {
        expect(result.current.loading).toBe(false);
      });
    });
  });

  describe('data flow', () => {
    it('should set data on successful execution', async () => {
      const { result } = renderHook(() => useAsync<string>());

      const testData = 'success data';
      const asyncFn = vi.fn(async () => testData);

      await result.current.execute(asyncFn);

      expect(result.current.data).toBe(testData);
      expect(result.current.error).toBeNull();
      expect(result.current.loading).toBe(false);
      expect(asyncFn).toHaveBeenCalledTimes(1);
    });

    it('should handle different data types', async () => {
      // Test with number
      const { result: numberResult } = renderHook(() => useAsync<number>());
      await numberResult.current.execute(async () => 42);
      expect(numberResult.current.data).toBe(42);

      // Test with object
      const { result: objectResult } = renderHook(() => useAsync<{ id: number; name: string }>());
      const testObject = { id: 1, name: 'test' };
      await objectResult.current.execute(async () => testObject);
      expect(objectResult.current.data).toEqual(testObject);

      // Test with array
      const { result: arrayResult } = renderHook(() => useAsync<string[]>());
      const testArray = ['a', 'b', 'c'];
      await arrayResult.current.execute(async () => testArray);
      expect(arrayResult.current.data).toEqual(testArray);
    });

    it('should return data from execute function', async () => {
      const { result } = renderHook(() => useAsync<string>());

      const testData = 'returned data';
      const asyncFn = vi.fn(async () => testData);

      const returnedData = await result.current.execute(asyncFn);

      expect(returnedData).toBe(testData);
    });

    it('should handle null data', async () => {
      const { result } = renderHook(() => useAsync<string | null>());

      const asyncFn = vi.fn(async () => null);

      await result.current.execute(asyncFn);

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

    it('should replace previous data with new data', async () => {
      const { result } = renderHook(() => useAsync<string>());

      await result.current.execute(async () => 'first');
      expect(result.current.data).toBe('first');

      await result.current.execute(async () => 'second');
      expect(result.current.data).toBe('second');

      await result.current.execute(async () => 'third');
      expect(result.current.data).toBe('third');
    });
  });

  describe('error handling', () => {
    it('should set error on failed execution', async () => {
      const { result } = renderHook(() => useAsync<string>());

      const error = new Error('Test error');
      const asyncFn = vi.fn(async () => {
        throw error;
      });

      try {
        await result.current.execute(asyncFn);
      } catch (e) {
        // Expected to throw
      }

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

    it('should call handleError with error and context', async () => {
      const { result } = renderHook(() => useAsync<string>());

      const error = new Error('Test error');
      const asyncFn = vi.fn(async () => {
        throw error;
      });

      try {
        await result.current.execute(asyncFn);
      } catch (e) {
        // Expected to throw
      }

      expect(errorHandling.handleError).toHaveBeenCalledWith(error, 'Async Operation');
    });

    it('should re-throw the handled error', async () => {
      const { result } = renderHook(() => useAsync<string>());

      const originalError = new Error('Test error');
      const asyncFn = vi.fn(async () => {
        throw originalError;
      });

      await expect(result.current.execute(asyncFn)).rejects.toThrow();
    });

    it('should handle errors of different types', async () => {
      const { result } = renderHook(() => useAsync<string>());

      // Test with Error object
      const error1 = new Error('Error object');
      await expect(result.current.execute(async () => {
        throw error1;
      })).rejects.toThrow();

      // Test with string error
      await expect(result.current.execute(async () => {
        throw 'String error';
      })).rejects.toThrow();

      // Test with number error
      await expect(result.current.execute(async () => {
        throw 404;
      })).rejects.toThrow();
    });

    it('should clear error on successful subsequent execution', async () => {
      const { result } = renderHook(() => useAsync<string>());

      // First execution fails
      try {
        await result.current.execute(async () => {
          throw new Error('First error');
        });
      } catch (e) {
        // Expected to throw
      }

      expect(result.current.error).not.toBeNull();

      // Second execution succeeds
      await result.current.execute(async () => 'success');

      expect(result.current.error).toBeNull();
      expect(result.current.data).toBe('success');
    });

    it('should replace previous error with new error', async () => {
      const { result } = renderHook(() => useAsync<string>());

      const error1 = new Error('First error');
      const error2 = new Error('Second error');

      // First execution fails
      try {
        await result.current.execute(async () => {
          throw error1;
        });
      } catch (e) {
        // Expected to throw
      }

      expect(result.current.error).toBe(error1);

      // Second execution also fails
      try {
        await result.current.execute(async () => {
          throw error2;
        });
      } catch (e) {
        // Expected to throw
      }

      expect(result.current.error).toBe(error2);
    });
  });

  describe('execute function stability', () => {
    it('should maintain the same execute function reference', () => {
      const { result, rerender } = renderHook(() => useAsync<string>());

      const firstExecute = result.current.execute;
      rerender();
      const secondExecute = result.current.execute;

      expect(firstExecute).toBe(secondExecute);
    });
  });

  describe('concurrent executions', () => {
    it('should handle multiple concurrent executions', async () => {
      const { result } = renderHook(() => useAsync<string>());

      const slowAsyncFn = vi.fn(() => new Promise<string>((resolve) => {
        setTimeout(() => resolve('slow result'), 100);
      }));

      const fastAsyncFn = vi.fn(() => new Promise<string>((resolve) => {
        setTimeout(() => resolve('fast result'), 10);
      }));

      // Start slow execution
      const slowPromise = result.current.execute(slowAsyncFn);

      // Start fast execution (will reset state)
      const fastPromise = result.current.execute(fastAsyncFn);

      // Fast one should complete
      await fastPromise;
      expect(result.current.data).toBe('fast result');

      // Slow one should also complete, but will override the fast result
      await slowPromise;
      // Note: In the current implementation, the last executed function will set the final state
    });
  });
});
