import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
import { fetch } from '../src/fetch';

describe('Enhanced Fetch', () => {
  let originalFetch: typeof globalThis.fetch;
  let mockFetch: any;
  let mockLogger: any;

  beforeEach(() => {
    originalFetch = globalThis.fetch;
    mockFetch = mock(() => Promise.resolve(new Response('test')));
    globalThis.fetch = mockFetch;

    mockLogger = {
      debug: mock(() => {}),
      info: mock(() => {}),
      error: mock(() => {}),
    };
  });

  afterEach(() => {
    globalThis.fetch = originalFetch;
  });

  describe('basic fetch', () => {
    it('should make simple GET request', async () => {
      const mockResponse = new Response('test data', { status: 200 });
      mockFetch.mockResolvedValue(mockResponse);

      const response = await fetch('https://api.example.com/data');

      expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/data', {
        method: 'GET',
        headers: {},
      });
      expect(response).toBe(mockResponse);
    });

    it('should make POST request with body', async () => {
      const mockResponse = new Response('created', { status: 201 });
      mockFetch.mockResolvedValue(mockResponse);

      const body = JSON.stringify({ name: 'test' });
      const response = await fetch('https://api.example.com/data', {
        method: 'POST',
        body,
        headers: { 'Content-Type': 'application/json' },
      });

      expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/data', {
        method: 'POST',
        body,
        headers: { 'Content-Type': 'application/json' },
      });
      expect(response).toBe(mockResponse);
    });

    it('should handle URL objects', async () => {
      const mockResponse = new Response('test');
      mockFetch.mockResolvedValue(mockResponse);

      const url = new URL('https://api.example.com/data');
      await fetch(url);

      expect(mockFetch).toHaveBeenCalledWith(url, expect.any(Object));
    });

    it('should handle Request objects', async () => {
      const mockResponse = new Response('test');
      mockFetch.mockResolvedValue(mockResponse);

      const request = new Request('https://api.example.com/data', {
        method: 'PUT',
      });
      await fetch(request);

      expect(mockFetch).toHaveBeenCalledWith(request, expect.any(Object));
    });
  });

  describe('proxy support', () => {
    it('should add proxy to request options', async () => {
      const mockResponse = new Response('proxy test');
      mockFetch.mockResolvedValue(mockResponse);

      await fetch('https://api.example.com/data', {
        proxy: 'http://proxy.example.com:8080',
      });

      expect(mockFetch).toHaveBeenCalledWith(
        'https://api.example.com/data',
        expect.objectContaining({
          proxy: 'http://proxy.example.com:8080',
        })
      );
    });

    it('should handle null proxy', async () => {
      const mockResponse = new Response('no proxy');
      mockFetch.mockResolvedValue(mockResponse);

      await fetch('https://api.example.com/data', {
        proxy: null,
      });

      expect(mockFetch).toHaveBeenCalledWith(
        'https://api.example.com/data',
        expect.not.objectContaining({
          proxy: expect.anything(),
        })
      );
    });
  });

  describe('timeout support', () => {
    it('should handle timeout', async () => {
      mockFetch.mockImplementation((url, options) => {
        return new Promise((resolve, reject) => {
          const timeoutId = setTimeout(() => resolve(new Response('delayed')), 100);

          // Listen for abort signal
          if (options?.signal) {
            options.signal.addEventListener('abort', () => {
              clearTimeout(timeoutId);
              reject(new DOMException('The operation was aborted', 'AbortError'));
            });
          }
        });
      });

      await expect(fetch('https://api.example.com/data', { timeout: 50 })).rejects.toThrow(
        'The operation was aborted'
      );
    });

    it('should clear timeout on success', async () => {
      const mockResponse = new Response('quick response');
      mockFetch.mockResolvedValue(mockResponse);

      const response = await fetch('https://api.example.com/data', {
        timeout: 1000,
      });

      expect(response).toBe(mockResponse);
    });

    it('should clear timeout on error', async () => {
      mockFetch.mockRejectedValue(new Error('Network error'));

      await expect(fetch('https://api.example.com/data', { timeout: 1000 })).rejects.toThrow(
        'Network error'
      );
    });
  });

  describe('logging', () => {
    it('should log request details', async () => {
      const mockResponse = new Response('test', {
        status: 200,
        statusText: 'OK',
        headers: new Headers({ 'content-type': 'text/plain' }),
      });
      mockFetch.mockResolvedValue(mockResponse);

      await fetch('https://api.example.com/data', {
        logger: mockLogger,
        method: 'POST',
        headers: { Authorization: 'Bearer token' },
      });

      expect(mockLogger.debug).toHaveBeenCalledWith('HTTP request', {
        method: 'POST',
        url: 'https://api.example.com/data',
        headers: { Authorization: 'Bearer token' },
        proxy: null,
      });

      expect(mockLogger.debug).toHaveBeenCalledWith('HTTP response', {
        url: 'https://api.example.com/data',
        status: 200,
        statusText: 'OK',
        ok: true,
        headers: { 'content-type': 'text/plain' },
      });
    });

    it('should log errors', async () => {
      const error = new Error('Connection failed');
      mockFetch.mockRejectedValue(error);

      await expect(fetch('https://api.example.com/data', { logger: mockLogger })).rejects.toThrow(
        'Connection failed'
      );

      expect(mockLogger.debug).toHaveBeenCalledWith('HTTP error', {
        url: 'https://api.example.com/data',
        error: 'Connection failed',
        name: 'Error',
      });
    });

    it('should use console as default logger', async () => {
      const consoleSpy = mock(console.debug);
      console.debug = consoleSpy;

      const mockResponse = new Response('test');
      mockFetch.mockResolvedValue(mockResponse);

      await fetch('https://api.example.com/data');

      expect(consoleSpy).toHaveBeenCalledTimes(2); // Request and response

      console.debug = originalFetch as any;
    });
  });

  describe('request options', () => {
    it('should forward all standard RequestInit options', async () => {
      const mockResponse = new Response('test');
      mockFetch.mockResolvedValue(mockResponse);

      const controller = new AbortController();
      const options = {
        method: 'PATCH' as const,
        headers: { 'X-Custom': 'value' },
        body: 'data',
        signal: controller.signal,
        credentials: 'include' as const,
        cache: 'no-store' as const,
        redirect: 'manual' as const,
        referrer: 'https://referrer.com',
        referrerPolicy: 'no-referrer' as const,
        integrity: 'sha256-hash',
        keepalive: true,
        mode: 'cors' as const,
      };

      await fetch('https://api.example.com/data', options);

      expect(mockFetch).toHaveBeenCalledWith(
        'https://api.example.com/data',
        expect.objectContaining(options)
      );
    });

    it('should handle undefined options', async () => {
      const mockResponse = new Response('test');
      mockFetch.mockResolvedValue(mockResponse);

      await fetch('https://api.example.com/data', undefined);

      expect(mockFetch).toHaveBeenCalledWith(
        'https://api.example.com/data',
        expect.objectContaining({
          method: 'GET',
          headers: {},
        })
      );
    });
  });

  describe('error handling', () => {
    it('should propagate fetch errors', async () => {
      const error = new TypeError('Failed to fetch');
      mockFetch.mockRejectedValue(error);

      await expect(fetch('https://api.example.com/data')).rejects.toThrow('Failed to fetch');
    });

    it('should handle non-Error objects', async () => {
      mockFetch.mockRejectedValue('string error');

      await expect(fetch('https://api.example.com/data', { logger: mockLogger })).rejects.toBe(
        'string error'
      );

      expect(mockLogger.debug).toHaveBeenCalledWith('HTTP error', {
        url: 'https://api.example.com/data',
        error: 'string error',
        name: 'Unknown',
      });
    });
  });
});
