import { http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
import { describe, it, expect, beforeAll, afterEach, afterAll } from 'vitest'
import { request, fromApi, toApi } from '@/api/client'
import { ApiError } from '@/types'

describe('fromApi', () => {
  it('converts snake_case keys to camelCase', () => {
    const input = { first_name: 'John', last_name: 'Doe' }
    const result = fromApi<{ firstName: string; lastName: string }>(input)
    expect(result).toEqual({ firstName: 'John', lastName: 'Doe' })
  })

  it('converts nested objects recursively', () => {
    const input = {
      user_name: 'alice',
      contact_info: { email_address: 'alice@example.com' },
    }
    const result = fromApi<{
      userName: string
      contactInfo: { emailAddress: string }
    }>(input)
    expect(result).toEqual({
      userName: 'alice',
      contactInfo: { emailAddress: 'alice@example.com' },
    })
  })

  it('converts arrays recursively', () => {
    const input = [{ item_name: 'a' }, { item_name: 'b' }]
    const result = fromApi<Array<{ itemName: string }>>(input)
    expect(result).toEqual([{ itemName: 'a' }, { itemName: 'b' }])
  })

  it('returns primitives unchanged', () => {
    expect(fromApi<string>('hello')).toBe('hello')
    expect(fromApi<number>(42)).toBe(42)
    expect(fromApi<null>(null)).toBeNull()
  })
})

describe('toApi', () => {
  it('converts camelCase keys to snake_case', () => {
    const input = { firstName: 'John', lastName: 'Doe' }
    const result = toApi<{ first_name: string; last_name: string }>(input)
    expect(result).toEqual({ first_name: 'John', last_name: 'Doe' })
  })

  it('converts nested objects recursively', () => {
    const input = {
      userName: 'alice',
      contactInfo: { emailAddress: 'alice@example.com' },
    }
    const result = toApi<{
      user_name: string
      contact_info: { email_address: string }
    }>(input)
    expect(result).toEqual({
      user_name: 'alice',
      contact_info: { email_address: 'alice@example.com' },
    })
  })

  it('converts arrays recursively', () => {
    const input = [{ itemName: 'a' }, { itemName: 'b' }]
    const result = toApi<Array<{ item_name: string }>>(input)
    expect(result).toEqual([{ item_name: 'a' }, { item_name: 'b' }])
  })

  it('returns primitives unchanged', () => {
    expect(toApi<string>('hello')).toBe('hello')
    expect(toApi<number>(42)).toBe(42)
    expect(toApi<null>(null)).toBeNull()
  })
})

describe('request', () => {
  const mswServer = setupServer()

  beforeAll(() => mswServer.listen({ onUnhandledRequest: 'bypass' }))
  afterEach(() => mswServer.resetHandlers())
  afterAll(() => mswServer.close())

  it('parses JSON and converts snake_case keys to camelCase on success', async () => {
    mswServer.use(
      http.get('http://localhost:3000/api/test', () => {
        return HttpResponse.json({
          id: '1',
          created_at: '2025-01-01',
          nested_obj: { inner_key: 'value' },
        })
      })
    )

    const result = await request<{
      id: string
      createdAt: string
      nestedObj: { innerKey: string }
    }>('GET', '/api/test')

    expect(result).toEqual({
      id: '1',
      createdAt: '2025-01-01',
      nestedObj: { innerKey: 'value' },
    })
  })

  it('throws ApiError with status and message on HTTP error', async () => {
    mswServer.use(
      http.get('http://localhost:3000/api/test', () => {
        return HttpResponse.json({ message: 'Not found' }, { status: 404 })
      })
    )

    await expect(request('GET', '/api/test')).rejects.toThrow(ApiError)
    try {
      await request('GET', '/api/test')
    } catch (error) {
      expect(error).toBeInstanceOf(ApiError)
      const apiError = error as ApiError
      expect(apiError.status).toBe(404)
      expect(apiError.message).toBe('Not found')
    }
  })

  it('throws ApiError with default message when error body has no message', async () => {
    mswServer.use(
      http.get('http://localhost:3000/api/test', () => {
        return HttpResponse.json({ details: 'something' }, { status: 500 })
      })
    )

    try {
      await request('GET', '/api/test')
    } catch (error) {
      expect(error).toBeInstanceOf(ApiError)
      const apiError = error as ApiError
      expect(apiError.status).toBe(500)
      expect(apiError.message).toContain('500')
    }
  })

  it('throws ApiError with code field when error response includes code', async () => {
    mswServer.use(
      http.get('http://localhost:3000/api/test', () => {
        return HttpResponse.json({ error: 'Model not found', code: 'MODEL_NOT_FOUND' }, { status: 404 })
      })
    )

    try {
      await request('GET', '/api/test')
    } catch (error) {
      expect(error).toBeInstanceOf(ApiError)
      const apiError = error as ApiError
      expect(apiError.status).toBe(404)
      expect(apiError.message).toBe('Model not found')
      expect(apiError.code).toBe('MODEL_NOT_FOUND')
    }
  })

  it('throws Error on network failure', async () => {
    mswServer.use(
      http.get('http://localhost:3000/api/test', () => {
        return HttpResponse.error()
      })
    )

    await expect(request('GET', '/api/test')).rejects.toThrow()
  })

  it('returns undefined for 204 No Content', async () => {
    mswServer.use(
      http.delete('http://localhost:3000/api/test/1', () => {
        return new HttpResponse(null, { status: 204 })
      })
    )

    const result = await request('DELETE', '/api/test/1')
    expect(result).toBeUndefined()
  })

  it('sends body with camelCase keys converted to snake_case', async () => {
    let receivedBody: Record<string, unknown> | null = null

    mswServer.use(
      http.post('http://localhost:3000/api/test', async ({ request }) => {
        receivedBody = (await request.json()) as Record<string, unknown>
        return HttpResponse.json({ id: '1' })
      })
    )

    await request('POST', '/api/test', {
      providerId: 'prov-1',
      modelName: 'gpt-4',
    })

    expect(receivedBody).toEqual({
      provider_id: 'prov-1',
      model_name: 'gpt-4',
    })
  })

  it('sends Content-Type header as application/json', async () => {
    let contentType: string | null = null

    mswServer.use(
      http.post('http://localhost:3000/api/test', async ({ request }) => {
        contentType = request.headers.get('Content-Type')
        return HttpResponse.json({ id: '1' })
      })
    )

    await request('POST', '/api/test', { name: 'test' })

    expect(contentType).toBe('application/json')
  })
})
