import { describe, it, expect, beforeEach, vi } from 'vitest'
import { apiGet, apiPost, apiPatch, apiPut, apiDelete, ApiError } from '@/lib/api/client'
import { mockFetch, setMockFetchResponse, setMockFetchError, resetFetchMock } from '../../../mocks/fetch'

// Mock window.location for URL building
const mockLocation = {
  origin: 'http://localhost:3000',
}
vi.stubGlobal('location', mockLocation)

describe('API Client', () => {
  beforeEach(() => {
    resetFetchMock()
  })

  describe('apiGet', () => {
    it('should make GET request and return JSON response', async () => {
      const mockData = { id: '1', name: 'Test' }
      setMockFetchResponse({ json: mockData })

      const result = await apiGet('/api/test')

      expect(mockFetch).toHaveBeenCalledWith(
        'http://localhost:3000/api/test',
        expect.objectContaining({
          method: 'GET',
          headers: expect.objectContaining({
            'Content-Type': 'application/json',
          }),
        })
      )
      expect(result).toEqual(mockData)
    })

    it('should append query params to URL', async () => {
      setMockFetchResponse({ json: {} })

      await apiGet('/api/test', { params: { page: 1, limit: 10, active: true } })

      expect(mockFetch).toHaveBeenCalledWith(
        expect.stringContaining('page=1'),
        expect.anything()
      )
      expect(mockFetch).toHaveBeenCalledWith(
        expect.stringContaining('limit=10'),
        expect.anything()
      )
      expect(mockFetch).toHaveBeenCalledWith(
        expect.stringContaining('active=true'),
        expect.anything()
      )
    })

    it('should skip undefined params', async () => {
      setMockFetchResponse({ json: {} })

      await apiGet('/api/test', { params: { page: 1, filter: undefined } })

      const calledUrl = mockFetch.mock.calls[0][0] as string
      expect(calledUrl).toContain('page=1')
      expect(calledUrl).not.toContain('filter')
    })

    it('should throw ApiError on non-OK response', async () => {
      setMockFetchResponse({ status: 404, ok: false, json: { error: 'Not found' } })

      await expect(apiGet('/api/test')).rejects.toThrow(ApiError)
      await expect(apiGet('/api/test')).rejects.toMatchObject({
        status: 404,
        statusText: 'Error',
      })
    })

    it('should handle network errors', async () => {
      setMockFetchError(new Error('Network error'))

      await expect(apiGet('/api/test')).rejects.toThrow('Network error')
    })
  })

  describe('apiPost', () => {
    it('should make POST request with JSON body', async () => {
      const mockData = { id: '1' }
      const requestBody = { name: 'New Item', value: 42 }
      setMockFetchResponse({ json: mockData })

      const result = await apiPost('/api/test', requestBody)

      expect(mockFetch).toHaveBeenCalledWith(
        'http://localhost:3000/api/test',
        expect.objectContaining({
          method: 'POST',
          headers: expect.objectContaining({
            'Content-Type': 'application/json',
          }),
          body: JSON.stringify(requestBody),
        })
      )
      expect(result).toEqual(mockData)
    })

    it('should handle POST without body', async () => {
      setMockFetchResponse({ json: { success: true } })

      await apiPost('/api/test')

      expect(mockFetch).toHaveBeenCalledWith(
        'http://localhost:3000/api/test',
        expect.objectContaining({
          method: 'POST',
          body: undefined,
        })
      )
    })

    it('should throw ApiError on 400 response', async () => {
      setMockFetchResponse({
        status: 400,
        ok: false,
        json: { error: 'Validation failed' }
      })

      await expect(apiPost('/api/test', { invalid: 'data' })).rejects.toThrow(ApiError)
    })

    it('should throw ApiError on 401 response', async () => {
      setMockFetchResponse({
        status: 401,
        ok: false,
        json: { error: 'Unauthorized' }
      })

      await expect(apiPost('/api/test')).rejects.toMatchObject({
        status: 401,
      })
    })
  })

  describe('apiPatch', () => {
    it('should make PATCH request with JSON body', async () => {
      const mockData = { id: '1', updated: true }
      const patchBody = { name: 'Updated Name' }
      setMockFetchResponse({ json: mockData })

      const result = await apiPatch('/api/test/1', patchBody)

      expect(mockFetch).toHaveBeenCalledWith(
        'http://localhost:3000/api/test/1',
        expect.objectContaining({
          method: 'PATCH',
          body: JSON.stringify(patchBody),
        })
      )
      expect(result).toEqual(mockData)
    })
  })

  describe('apiPut', () => {
    it('should make PUT request with JSON body', async () => {
      const mockData = { id: '1', replaced: true }
      const putBody = { name: 'Replaced Item', value: 100 }
      setMockFetchResponse({ json: mockData })

      const result = await apiPut('/api/test/1', putBody)

      expect(mockFetch).toHaveBeenCalledWith(
        'http://localhost:3000/api/test/1',
        expect.objectContaining({
          method: 'PUT',
          body: JSON.stringify(putBody),
        })
      )
      expect(result).toEqual(mockData)
    })
  })

  describe('apiDelete', () => {
    it('should make DELETE request', async () => {
      setMockFetchResponse({ json: { deleted: true } })

      const result = await apiDelete('/api/test/1')

      expect(mockFetch).toHaveBeenCalledWith(
        'http://localhost:3000/api/test/1',
        expect.objectContaining({
          method: 'DELETE',
        })
      )
      expect(result).toEqual({ deleted: true })
    })

    it('should handle 204 No Content response', async () => {
      // Simulate empty response
      mockFetch.mockResolvedValueOnce({
        ok: true,
        status: 204,
        statusText: 'No Content',
        text: vi.fn().mockResolvedValue(''),
        json: vi.fn().mockRejectedValue(new Error('No JSON')),
      } as unknown as Response)

      const result = await apiDelete('/api/test/1')

      expect(result).toBeNull()
    })
  })

  describe('ApiError', () => {
    it('should include status and data', async () => {
      const errorData = { message: 'Validation failed', errors: ['field required'] }
      setMockFetchResponse({
        status: 422,
        ok: false,
        json: errorData
      })

      try {
        await apiPost('/api/test', {})
      } catch (error) {
        expect(error).toBeInstanceOf(ApiError)
        expect((error as ApiError).status).toBe(422)
        expect((error as ApiError).data).toEqual(errorData)
      }
    })

    it('should handle non-JSON error responses', async () => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 500,
        statusText: 'Internal Server Error',
        json: vi.fn().mockRejectedValue(new Error('Invalid JSON')),
        text: vi.fn().mockResolvedValue('Internal Server Error'),
      } as unknown as Response)

      try {
        await apiGet('/api/test')
      } catch (error) {
        expect(error).toBeInstanceOf(ApiError)
        expect((error as ApiError).status).toBe(500)
        expect((error as ApiError).data).toBeUndefined()
      }
    })
  })

  describe('custom headers', () => {
    it('should merge custom headers with defaults', async () => {
      setMockFetchResponse({ json: {} })

      await apiGet('/api/test', {
        headers: {
          'X-Custom-Header': 'custom-value',
        },
      })

      expect(mockFetch).toHaveBeenCalledWith(
        expect.anything(),
        expect.objectContaining({
          headers: expect.objectContaining({
            'Content-Type': 'application/json',
            'X-Custom-Header': 'custom-value',
          }),
        })
      )
    })
  })
})
