import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { api } from '../request'
import { ApiError } from '@/types/api'

describe('request utils', () => {
  const mockFetch = vi.fn()
  const originalFetch = globalThis.fetch
  const originalLocation = window.location

  beforeEach(() => {
    globalThis.fetch = mockFetch
    localStorage.clear()
    vi.restoreAllMocks()
    // Mock window.location
    Object.defineProperty(window, 'location', {
      value: { href: '', pathname: '/', search: '' },
      writable: true,
    })
  })

  afterEach(() => {
    globalThis.fetch = originalFetch
    Object.defineProperty(window, 'location', {
      value: originalLocation,
      writable: true,
    })
  })

  function mockResponse(body: Record<string, unknown>, status = 200) {
    mockFetch.mockResolvedValueOnce({
      status,
      json: () => Promise.resolve(body),
    })
  }

  describe('token auto-injection', () => {
    it('sends Authorization header when token exists', async () => {
      localStorage.setItem('access_token', 'test-token')
      mockResponse({ code: 0, data: { id: 1 }, message: 'ok' })

      await api.get('/api/test')

      const [, options] = mockFetch.mock.calls[0]!
      expect(options.headers['Authorization']).toBe('Bearer test-token')
    })

    it('omits Authorization header when no token', async () => {
      mockResponse({ code: 0, data: null, message: 'ok' })

      await api.get('/api/test')

      const [, options] = mockFetch.mock.calls[0]!
      expect(options.headers['Authorization']).toBeUndefined()
    })
  })

  describe('response parsing', () => {
    it('returns data field on success (code 0)', async () => {
      mockResponse({ code: 0, data: { users: [] }, message: 'ok' })

      const result = await api.get('/api/users')

      expect(result).toEqual({ users: [] })
    })

    it('throws ApiError on business error (code !== 0)', async () => {
      mockResponse({ code: 1001, data: null, message: '参数错误' })

      await expect(api.get('/api/test')).rejects.toThrow(ApiError)
      await mockResponse({ code: 1001, data: null, message: '参数错误' })

      try {
        await api.get('/api/test')
      } catch (e) {
        expect(e).toBeInstanceOf(ApiError)
        expect((e as ApiError).code).toBe(1001)
        expect((e as ApiError).message).toBe('参数错误')
      }
    })
  })

  describe('401 handling', () => {
    it('clears tokens and redirects on 401', async () => {
      localStorage.setItem('access_token', 'expired-token')
      localStorage.setItem('refresh_token', 'expired-refresh')
      localStorage.setItem('user', '{"id":1}')

      mockFetch.mockResolvedValueOnce({
        status: 401,
        json: () => Promise.resolve({ code: 401, message: 'Unauthorized' }),
      })

      await expect(api.get('/api/test')).rejects.toThrow('登录已过期')

      expect(localStorage.getItem('access_token')).toBeNull()
      expect(localStorage.getItem('refresh_token')).toBeNull()
      expect(localStorage.getItem('user')).toBeNull()
      expect(window.location.href).toBe('/login?redirect=%2F')
    })
  })

  describe('HTTP methods', () => {
    it('GET with params', async () => {
      mockResponse({ code: 0, data: [], message: 'ok' })

      await api.get('/api/users', { page: 1, per_page: 15, name: '' })

      const [url, options] = mockFetch.mock.calls[0]!
      expect(url).toContain('/api/users?')
      expect(url).toContain('page=1')
      expect(url).toContain('per_page=15')
      expect(url).not.toContain('name=')
      expect(options.method).toBe('GET')
    })

    it('POST with body', async () => {
      mockResponse({ code: 0, data: { id: 1 }, message: 'ok' })

      await api.post('/api/users', { username: 'test' })

      const [, options] = mockFetch.mock.calls[0]!
      expect(options.method).toBe('POST')
      expect(options.body).toBe(JSON.stringify({ username: 'test' }))
    })

    it('PUT with body', async () => {
      mockResponse({ code: 0, data: null, message: 'ok' })

      await api.put('/api/users/1', { username: 'updated' })

      const [, options] = mockFetch.mock.calls[0]!
      expect(options.method).toBe('PUT')
    })

    it('PATCH with body', async () => {
      mockResponse({ code: 0, data: null, message: 'ok' })

      await api.patch('/api/users/1', { status: 1 })

      const [, options] = mockFetch.mock.calls[0]!
      expect(options.method).toBe('PATCH')
    })

    it('DELETE', async () => {
      mockResponse({ code: 0, data: null, message: 'ok' })

      await api.delete('/api/users/1')

      const [, options] = mockFetch.mock.calls[0]!
      expect(options.method).toBe('DELETE')
    })
  })

  describe('params serialization', () => {
    it('filters out undefined, null, and empty string values', async () => {
      mockResponse({ code: 0, data: [], message: 'ok' })

      await api.get('/api/test', {
        a: 'hello',
        b: undefined,
        c: null,
        d: '',
        e: 0,
      })

      const [url] = mockFetch.mock.calls[0]!
      expect(url).toContain('a=hello')
      expect(url).toContain('e=0')
      expect(url).not.toContain('b=')
      expect(url).not.toContain('c=')
      expect(url).not.toContain('d=')
    })
  })
})
