/**
 * AuthContext Unit Tests
 *
 * Tests the AuthProvider, useAuth hook, and the exported USE_MOCK flag.
 * Covers: initial state, login (mock + real), logout, register, and
 * restoration of user from localStorage on mount.
 */
import React from 'react'
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { renderHook, act, waitFor } from '@testing-library/react'

// ---------------------------------------------------------------------------
// Use vi.hoisted to create mock functions before vi.mock hoisting
// ---------------------------------------------------------------------------

const { mockApiLogin, mockApiRegister, mockClearTokens, mockGetAccessToken } = vi.hoisted(() => ({
  mockApiLogin: vi.fn(),
  mockApiRegister: vi.fn(),
  mockClearTokens: vi.fn(),
  mockGetAccessToken: vi.fn(() => null),
}))

vi.mock('@/lib/api', () => ({
  api: {
    login: mockApiLogin,
    register: mockApiRegister,
  },
  clearTokens: mockClearTokens,
  getAccessToken: mockGetAccessToken,
  setTokens: vi.fn(),
}))

// ---------------------------------------------------------------------------
// Import AuthContext after mocks are set up
// ---------------------------------------------------------------------------
import { AuthProvider, useAuth, USE_MOCK } from './AuthContext'

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function mockLocalStorage() {
  const store: Record<string, string> = {}
  return {
    getItem: vi.fn((key: string) => store[key] ?? null),
    setItem: vi.fn((key: string, value: string) => {
      store[key] = value
    }),
    removeItem: vi.fn((key: string) => {
      delete store[key]
    }),
    clear: vi.fn(() => {
      Object.keys(store).forEach((k) => delete store[k])
    }),
    get length() {
      return Object.keys(store).length
    },
    key: vi.fn((index: number) => Object.keys(store)[index] ?? null),
  }
}

const wrapper = ({ children }: { children: React.ReactNode }) => (
  <AuthProvider>{children}</AuthProvider>
)

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe('AuthContext', () => {
  let storage: ReturnType<typeof mockLocalStorage>

  beforeEach(() => {
    storage = mockLocalStorage()
    Object.defineProperty(window, 'localStorage', { value: storage, writable: true })
    vi.clearAllMocks()
    // Default: no stored token
    mockGetAccessToken.mockReturnValue(null)
  })

  afterEach(() => {
    storage.clear()
  })

  // ---- USE_MOCK flag ----

  describe('USE_MOCK export', () => {
    it('is exported as a boolean', () => {
      expect(typeof USE_MOCK).toBe('boolean')
    })

    it('is true in test/development environment or based on env var', () => {
      // vitest runs with NODE_ENV=test. The AuthContext checks for
      // NODE_ENV === 'development' or NEXT_PUBLIC_USE_MOCK === 'true'.
      // We verify the export exists and is a boolean.
      expect([true, false]).toContain(USE_MOCK)
    })
  })

  // ---- Initial state ----

  describe('initial state', () => {
    it('starts with user as null', async () => {
      const { result } = renderHook(() => useAuth(), { wrapper })

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

      expect(result.current.user).toBeNull()
    })

    it('starts with isAuthenticated as false', async () => {
      const { result } = renderHook(() => useAuth(), { wrapper })

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

      expect(result.current.isAuthenticated).toBe(false)
    })

    it('starts with isLoading true, then becomes false', async () => {
      const { result } = renderHook(() => useAuth(), { wrapper })

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

  // ---- Restoring user from localStorage ----

  describe('restoring user from localStorage', () => {
    it('restores user when token and stored user exist', async () => {
      const storedUser = {
        id: 'u1',
        name: 'Restored User',
        email: 'restored@test.com',
        role: 'brand',
        is_verified: true,
      }

      mockGetAccessToken.mockReturnValue('valid-token')
      storage.setItem('miaosi_user', JSON.stringify(storedUser))

      const { result } = renderHook(() => useAuth(), { wrapper })

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

      expect(result.current.user).toEqual(storedUser)
      expect(result.current.isAuthenticated).toBe(true)
    })

    it('does not restore user when token is missing', async () => {
      const storedUser = { id: 'u1', name: 'No Token', role: 'brand', is_verified: true }
      storage.setItem('miaosi_user', JSON.stringify(storedUser))
      mockGetAccessToken.mockReturnValue(null)

      const { result } = renderHook(() => useAuth(), { wrapper })

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

      expect(result.current.user).toBeNull()
      expect(result.current.isAuthenticated).toBe(false)
    })

    it('clears invalid JSON from localStorage gracefully', async () => {
      mockGetAccessToken.mockReturnValue('some-token')
      storage.setItem('miaosi_user', 'not-valid-json{{{')

      const { result } = renderHook(() => useAuth(), { wrapper })

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

      expect(mockClearTokens).toHaveBeenCalled()
      expect(storage.removeItem).toHaveBeenCalledWith('miaosi_user')
      expect(result.current.user).toBeNull()
    })
  })

  // ---- Login ----

  describe('login', () => {
    // When USE_MOCK is true, the AuthProvider uses mock login logic.
    // When false, it calls api.login().

    if (!USE_MOCK) {
      it('calls api.login and sets user on success (real API path)', async () => {
        const loginResponse = {
          access_token: 'at',
          refresh_token: 'rt',
          token_type: 'bearer',
          expires_in: 900,
          user: {
            id: 'u1',
            name: 'API User',
            email: 'api@test.com',
            role: 'brand',
            is_verified: true,
          },
        }
        mockApiLogin.mockResolvedValueOnce(loginResponse)

        const { result } = renderHook(() => useAuth(), { wrapper })

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

        let loginResult: { success: boolean; error?: string }
        await act(async () => {
          loginResult = await result.current.login({
            email: 'api@test.com',
            password: 'pass123',
          })
        })

        expect(loginResult!.success).toBe(true)
        expect(result.current.user?.name).toBe('API User')
        expect(result.current.isAuthenticated).toBe(true)
        expect(storage.setItem).toHaveBeenCalledWith(
          'miaosi_user',
          expect.stringContaining('API User')
        )
      })

      it('returns error on api.login failure (real API path)', async () => {
        mockApiLogin.mockRejectedValueOnce(new Error('Invalid credentials'))

        const { result } = renderHook(() => useAuth(), { wrapper })

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

        let loginResult: { success: boolean; error?: string }
        await act(async () => {
          loginResult = await result.current.login({
            email: 'bad@test.com',
            password: 'wrong',
          })
        })

        expect(loginResult!.success).toBe(false)
        expect(loginResult!.error).toBe('Invalid credentials')
        expect(result.current.user).toBeNull()
      })
    }

    if (USE_MOCK) {
      it('logs in with mock user for known email (mock path)', async () => {
        const { result } = renderHook(() => useAuth(), { wrapper })

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

        let loginResult: { success: boolean; error?: string }
        await act(async () => {
          loginResult = await result.current.login({
            email: 'creator@demo.com',
            password: 'demo123',
          })
        })

        expect(loginResult!.success).toBe(true)
        expect(result.current.user?.email).toBe('creator@demo.com')
        expect(result.current.isAuthenticated).toBe(true)
      })

      it('returns error for unknown email in mock mode', async () => {
        const { result } = renderHook(() => useAuth(), { wrapper })

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

        let loginResult: { success: boolean; error?: string }
        await act(async () => {
          loginResult = await result.current.login({
            email: 'unknown@demo.com',
            password: 'demo123',
          })
        })

        expect(loginResult!.success).toBe(false)
        expect(loginResult!.error).toBeDefined()
      })

      it('returns error for wrong password in mock mode', async () => {
        const { result } = renderHook(() => useAuth(), { wrapper })

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

        let loginResult: { success: boolean; error?: string }
        await act(async () => {
          loginResult = await result.current.login({
            email: 'creator@demo.com',
            password: 'wrongpassword',
          })
        })

        expect(loginResult!.success).toBe(false)
        expect(loginResult!.error).toBe('密码错误')
      })

      it('allows email code login for known user in mock mode', async () => {
        const { result } = renderHook(() => useAuth(), { wrapper })

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

        let loginResult: { success: boolean; error?: string }
        await act(async () => {
          loginResult = await result.current.login({
            email: 'brand@demo.com',
            email_code: '123456',
          })
        })

        expect(loginResult!.success).toBe(true)
        expect(result.current.user?.email).toBe('brand@demo.com')
        expect(result.current.user?.role).toBe('brand')
      })
    }
  })

  // ---- Register ----

  describe('register', () => {
    const registerData = {
      email: 'new@example.com',
      password: 'secure123',
      name: 'New User',
      role: 'creator' as const,
      email_code: '123456',
    }

    if (!USE_MOCK) {
      it('calls api.register and sets user on success (real API path)', async () => {
        const registerResponse = {
          access_token: 'at',
          refresh_token: 'rt',
          token_type: 'bearer',
          expires_in: 900,
          user: {
            id: 'u2',
            name: 'New User',
            email: 'new@example.com',
            role: 'creator',
            is_verified: false,
          },
        }
        mockApiRegister.mockResolvedValueOnce(registerResponse)

        const { result } = renderHook(() => useAuth(), { wrapper })

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

        let registerResult: { success: boolean; error?: string }
        await act(async () => {
          registerResult = await result.current.register(registerData)
        })

        expect(registerResult!.success).toBe(true)
        expect(result.current.user?.name).toBe('New User')
        expect(result.current.isAuthenticated).toBe(true)
      })

      it('returns error on api.register failure', async () => {
        mockApiRegister.mockRejectedValueOnce(new Error('Email already exists'))

        const { result } = renderHook(() => useAuth(), { wrapper })

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

        let registerResult: { success: boolean; error?: string }
        await act(async () => {
          registerResult = await result.current.register(registerData)
        })

        expect(registerResult!.success).toBe(false)
        expect(registerResult!.error).toBe('Email already exists')
      })
    }

    if (USE_MOCK) {
      it('creates a mock user on register in mock mode', async () => {
        const { result } = renderHook(() => useAuth(), { wrapper })

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

        let registerResult: { success: boolean; error?: string }
        await act(async () => {
          registerResult = await result.current.register(registerData)
        })

        expect(registerResult!.success).toBe(true)
        expect(result.current.user?.email).toBe('new@example.com')
        expect(result.current.user?.name).toBe('New User')
        expect(result.current.user?.role).toBe('creator')
        expect(result.current.isAuthenticated).toBe(true)
      })
    }
  })

  // ---- Logout ----

  describe('logout', () => {
    it('clears user state, tokens, and localStorage', async () => {
      const storedUser = {
        id: 'u1',
        name: 'Logged In',
        email: 'in@test.com',
        role: 'brand',
        is_verified: true,
      }
      mockGetAccessToken.mockReturnValue('valid-token')
      storage.setItem('miaosi_user', JSON.stringify(storedUser))

      const { result } = renderHook(() => useAuth(), { wrapper })

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

      expect(result.current.user).not.toBeNull()
      expect(result.current.isAuthenticated).toBe(true)

      act(() => {
        result.current.logout()
      })

      expect(result.current.user).toBeNull()
      expect(result.current.isAuthenticated).toBe(false)
      expect(mockClearTokens).toHaveBeenCalled()
      expect(storage.removeItem).toHaveBeenCalledWith('miaosi_user')
    })
  })

  // ---- switchRole ----

  describe('switchRole', () => {
    it('updates the user role and persists to localStorage', async () => {
      const storedUser = {
        id: 'u1',
        name: 'Multi Role',
        email: 'multi@test.com',
        role: 'brand',
        is_verified: true,
      }
      mockGetAccessToken.mockReturnValue('valid-token')
      storage.setItem('miaosi_user', JSON.stringify(storedUser))

      const { result } = renderHook(() => useAuth(), { wrapper })

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

      expect(result.current.user?.role).toBe('brand')

      act(() => {
        result.current.switchRole('agency')
      })

      expect(result.current.user?.role).toBe('agency')
      // Check localStorage was updated with new role
      const storedCalls = storage.setItem.mock.calls.filter(
        ([key]: [string, string]) => key === 'miaosi_user'
      )
      const lastStored = JSON.parse(storedCalls[storedCalls.length - 1][1])
      expect(lastStored.role).toBe('agency')
    })

    it('does nothing when no user is logged in', async () => {
      const { result } = renderHook(() => useAuth(), { wrapper })

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

      act(() => {
        result.current.switchRole('creator')
      })

      expect(result.current.user).toBeNull()
    })
  })

  // ---- useAuth outside provider ----

  describe('useAuth outside AuthProvider', () => {
    it('throws an error when used outside AuthProvider', () => {
      const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})

      expect(() => {
        renderHook(() => useAuth())
      }).toThrow('useAuth must be used within an AuthProvider')

      consoleSpy.mockRestore()
    })
  })
})
