import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'

import type { Role, SessionUser, TokenBundle } from '@/types'
import { useAuth } from './auth-context'
import { AuthProvider } from './AuthProvider'

let storedAccessToken: string | null = null
let storedUser: SessionUser | null = null
let storedRoles: Role[] = []

const navigateMock = vi.fn()
const getMock = vi.fn()
const setRefreshTokenMock = vi.fn()
const clearRefreshTokenMock = vi.fn()
const hasSessionPresenceCookieMock = vi.fn()
const setAccessTokenMock = vi.fn((token: string, expiresIn: number) => {
  void expiresIn
  storedAccessToken = token
})
const getCurrentUserMock = vi.fn(() => storedUser)
const setCurrentUserMock = vi.fn((user: SessionUser) => {
  storedUser = user
})
const getCurrentRolesMock = vi.fn(() => storedRoles)
const setCurrentRolesMock = vi.fn((roles: Role[]) => {
  storedRoles = roles
})
const clearSessionMock = vi.fn(() => {
  storedAccessToken = null
  storedUser = null
  storedRoles = []
})
const isAuthenticatedMock = vi.fn(() => storedAccessToken !== null && storedUser !== null)
const isAccessTokenExpiredMock = vi.fn()
const initCSRFTokenMock = vi.fn()
const clearCSRFTokenMock = vi.fn()
const logoutRequestMock = vi.fn()
const refreshSessionMock = vi.fn()

vi.mock('react-router-dom', async () => {
  const actual = await vi.importActual<typeof import('react-router-dom')>('react-router-dom')
  return {
    ...actual,
    useNavigate: () => navigateMock,
  }
})

vi.mock('@/lib/http', () => ({
  get: (path: string) => getMock(path),
}))

vi.mock('@/lib/storage', () => ({
  setRefreshToken: (token: string | null | undefined) => setRefreshTokenMock(token),
  clearRefreshToken: () => clearRefreshTokenMock(),
  hasSessionPresenceCookie: () => hasSessionPresenceCookieMock(),
}))

vi.mock('@/lib/http/auth-session', () => ({
  setAccessToken: (token: string, expiresIn: number) => setAccessTokenMock(token, expiresIn),
  getCurrentUser: () => getCurrentUserMock(),
  setCurrentUser: (user: SessionUser) => setCurrentUserMock(user),
  getCurrentRoles: () => getCurrentRolesMock(),
  setCurrentRoles: (roles: Role[]) => setCurrentRolesMock(roles),
  clearSession: () => clearSessionMock(),
  isAuthenticated: () => isAuthenticatedMock(),
  isAccessTokenExpired: () => isAccessTokenExpiredMock(),
}))

vi.mock('@/lib/http/csrf', () => ({
  initCSRFToken: () => initCSRFTokenMock(),
  clearCSRFToken: () => clearCSRFTokenMock(),
}))

vi.mock('@/services/auth', () => ({
  logout: () => logoutRequestMock(),
  refreshSession: () => refreshSessionMock(),
}))

const adminUser: SessionUser = {
  id: 1,
  username: 'admin',
  email: 'admin@example.com',
  phone: '13800138000',
  nickname: 'Admin',
  avatar: '',
  status: 1,
}

const adminRoles: Role[] = [
  {
    id: 1,
    name: 'Administrator',
    code: 'admin',
    description: 'System administrator',
    is_system: true,
    is_default: false,
    status: 1,
  },
]

const refreshedSession: TokenBundle = {
  access_token: 'access-token',
  refresh_token: 'refresh-token',
  expires_in: 7200,
  user: adminUser,
}

const loginSession: TokenBundle = {
  access_token: 'login-access-token',
  refresh_token: 'login-refresh-token',
  expires_in: 3600,
  user: adminUser,
}

const operatorUser: SessionUser = {
  id: 2,
  username: 'operator',
  email: 'operator@example.com',
  phone: '13900139000',
  nickname: 'Operator',
  avatar: '',
  status: 1,
}

const operatorRoles: Role[] = [
  {
    id: 2,
    name: 'Operator',
    code: 'operator',
    description: 'Operations user',
    is_system: false,
    is_default: false,
    status: 1,
  },
]

function Probe() {
  const auth = useAuth()

  return (
    <div>
      <span data-testid="loading">{String(auth.isLoading)}</span>
      <span data-testid="authenticated">{String(auth.isAuthenticated)}</span>
      <span data-testid="username">{auth.user?.username ?? ''}</span>
      <span data-testid="roles">{auth.roles.map((role) => role.code).join(',')}</span>
      <button onClick={() => void auth.onLoginSuccess(loginSession)} type="button">
        login-success
      </button>
      <button onClick={() => void auth.refreshUser()} type="button">
        refresh-user
      </button>
      <button onClick={() => void auth.logout()} type="button">
        logout
      </button>
    </div>
  )
}

function renderAuthProvider() {
  return render(
    <AuthProvider>
      <Probe />
    </AuthProvider>,
  )
}

async function waitForProviderIdle() {
  await waitFor(() => {
    expect(screen.getByTestId('loading')).toHaveTextContent('false')
  })
}

describe('AuthProvider', () => {
  beforeEach(() => {
    window.history.pushState({}, '', '/')
    storedAccessToken = null
    storedUser = null
    storedRoles = []

    navigateMock.mockReset()
    getMock.mockReset()
    setRefreshTokenMock.mockReset()
    clearRefreshTokenMock.mockReset()
    hasSessionPresenceCookieMock.mockReset()
    setAccessTokenMock.mockReset()
    getCurrentUserMock.mockReset()
    setCurrentUserMock.mockReset()
    getCurrentRolesMock.mockReset()
    setCurrentRolesMock.mockReset()
    clearSessionMock.mockReset()
    isAuthenticatedMock.mockReset()
    isAccessTokenExpiredMock.mockReset()
    initCSRFTokenMock.mockReset()
    clearCSRFTokenMock.mockReset()
    logoutRequestMock.mockReset()
    isAccessTokenExpiredMock.mockReturnValue(true)
    isAuthenticatedMock.mockImplementation(() => storedAccessToken !== null && storedUser !== null)
    getCurrentUserMock.mockImplementation(() => storedUser)
    setCurrentUserMock.mockImplementation((user: SessionUser) => {
      storedUser = user
    })
    getCurrentRolesMock.mockImplementation(() => storedRoles)
    setCurrentRolesMock.mockImplementation((roles: Role[]) => {
      storedRoles = roles
    })
    setAccessTokenMock.mockImplementation((token: string, expiresIn: number) => {
      void expiresIn
      storedAccessToken = token
    })
    clearSessionMock.mockImplementation(() => {
      storedAccessToken = null
      storedUser = null
      storedRoles = []
    })
    hasSessionPresenceCookieMock.mockReturnValue(false)
    initCSRFTokenMock.mockResolvedValue(undefined)
    clearCSRFTokenMock.mockReturnValue(undefined)
    logoutRequestMock.mockResolvedValue(undefined)
    refreshSessionMock.mockReset()
  })

  it('reuses an in-memory authenticated session when the access token is still valid', async () => {
    storedAccessToken = 'cached-access-token'
    storedUser = adminUser
    storedRoles = adminRoles
    isAccessTokenExpiredMock.mockReturnValue(false)

    renderAuthProvider()
    await waitForProviderIdle()

    expect(refreshSessionMock).not.toHaveBeenCalled()
    expect(navigateMock).not.toHaveBeenCalled()
    expect(clearRefreshTokenMock).not.toHaveBeenCalled()
    expect(clearSessionMock).not.toHaveBeenCalled()
    expect(initCSRFTokenMock).toHaveBeenCalledTimes(1)
    expect(screen.getByTestId('authenticated')).toHaveTextContent('true')
    expect(screen.getByTestId('username')).toHaveTextContent('admin')
    expect(screen.getByTestId('roles')).toHaveTextContent('admin')
  })

  it('clears the local session when auth state has no current user and no backend session cookie exists', async () => {
    storedAccessToken = 'dangling-access-token'
    isAuthenticatedMock.mockReturnValue(true)
    isAccessTokenExpiredMock.mockReturnValue(false)
    getCurrentUserMock.mockReturnValue(null)

    renderAuthProvider()
    await waitForProviderIdle()

    expect(refreshSessionMock).not.toHaveBeenCalled()
    expect(clearRefreshTokenMock).toHaveBeenCalledTimes(1)
    expect(clearSessionMock).toHaveBeenCalledTimes(1)
    expect(screen.getByTestId('authenticated')).toHaveTextContent('false')
    expect(screen.getByTestId('username').textContent).toBe('')
  })

  it('restores the session by refreshing through the backend cookie flow', async () => {
    hasSessionPresenceCookieMock.mockReturnValue(true)
    refreshSessionMock.mockResolvedValue(refreshedSession)
    getMock.mockResolvedValue(adminRoles)

    renderAuthProvider()

    await waitFor(() => {
      expect(refreshSessionMock).toHaveBeenCalledTimes(1)
    })

    await waitForProviderIdle()

    expect(setAccessTokenMock).toHaveBeenCalledWith('access-token', 7200)
    expect(setRefreshTokenMock).toHaveBeenCalledWith('refresh-token')
    expect(setCurrentUserMock).toHaveBeenCalledWith(adminUser)
    expect(setCurrentRolesMock).toHaveBeenCalledWith(adminRoles)
    expect(getMock).toHaveBeenCalledWith('/users/1/roles')
    expect(initCSRFTokenMock).toHaveBeenCalledTimes(1)
    expect(navigateMock).not.toHaveBeenCalled()
    expect(screen.getByTestId('username')).toHaveTextContent('admin')
    expect(screen.getByTestId('roles')).toHaveTextContent('admin')
  })

  it('restores the session with empty roles when the role lookup fails after refresh', async () => {
    hasSessionPresenceCookieMock.mockReturnValue(true)
    refreshSessionMock.mockResolvedValue(refreshedSession)
    getMock.mockRejectedValue(new Error('roles lookup failed'))

    renderAuthProvider()
    await waitForProviderIdle()

    expect(setAccessTokenMock).toHaveBeenCalledWith('access-token', 7200)
    expect(setRefreshTokenMock).toHaveBeenCalledWith('refresh-token')
    expect(setCurrentUserMock).toHaveBeenCalledWith(adminUser)
    expect(setCurrentRolesMock).toHaveBeenCalledWith([])
    expect(initCSRFTokenMock).toHaveBeenCalledTimes(1)
    expect(screen.getByTestId('authenticated')).toHaveTextContent('true')
    expect(screen.getByTestId('username')).toHaveTextContent('admin')
    expect(screen.getByTestId('roles').textContent).toBe('')
  })

  it('clears local state when refresh fails against the backend cookie flow', async () => {
    hasSessionPresenceCookieMock.mockReturnValue(true)
    refreshSessionMock.mockRejectedValue(new Error('missing refresh cookie'))

    renderAuthProvider()

    await waitFor(() => {
      expect(refreshSessionMock).toHaveBeenCalledTimes(1)
    })

    await waitForProviderIdle()

    expect(clearRefreshTokenMock).toHaveBeenCalledTimes(1)
    expect(clearSessionMock).toHaveBeenCalledTimes(1)
    expect(navigateMock).not.toHaveBeenCalled()
    expect(screen.getByTestId('authenticated')).toHaveTextContent('false')
  })

  it('persists tokens, user, roles, and csrf state after login success', async () => {
    renderAuthProvider()
    await waitForProviderIdle()
    vi.clearAllMocks()

    getMock.mockResolvedValue(adminRoles)
    fireEvent.click(screen.getByRole('button', { name: 'login-success' }))

    await waitFor(() => {
      expect(setAccessTokenMock).toHaveBeenCalledWith('login-access-token', 3600)
    })

    expect(setRefreshTokenMock).toHaveBeenCalledWith('login-refresh-token')
    expect(setCurrentUserMock).toHaveBeenCalledWith(adminUser)
    expect(setCurrentRolesMock).toHaveBeenCalledWith(adminRoles)
    expect(getMock).toHaveBeenCalledWith('/users/1/roles')
    expect(initCSRFTokenMock).toHaveBeenCalledTimes(1)
    expect(screen.getByTestId('authenticated')).toHaveTextContent('true')
    expect(screen.getByTestId('username')).toHaveTextContent('admin')
    expect(screen.getByTestId('roles')).toHaveTextContent('admin')
  })

  it('refreshes the current user and roles from the backend', async () => {
    renderAuthProvider()
    await waitForProviderIdle()
    vi.clearAllMocks()

    getMock.mockImplementation((path: string) => {
      if (path === '/auth/userinfo') {
        return Promise.resolve(operatorUser)
      }
      if (path === '/users/2/roles') {
        return Promise.resolve(operatorRoles)
      }
      return Promise.reject(new Error(`Unexpected path: ${path}`))
    })

    fireEvent.click(screen.getByRole('button', { name: 'refresh-user' }))

    await waitFor(() => {
      expect(setCurrentUserMock).toHaveBeenCalledWith(operatorUser)
    })

    expect(setCurrentRolesMock).toHaveBeenCalledWith(operatorRoles)
    expect(screen.getByTestId('username')).toHaveTextContent('operator')
    expect(screen.getByTestId('roles')).toHaveTextContent('operator')
  })

  it('logs refreshUser failures without corrupting the current auth state', async () => {
    storedAccessToken = 'cached-access-token'
    storedUser = adminUser
    storedRoles = adminRoles
    isAccessTokenExpiredMock.mockReturnValue(false)

    renderAuthProvider()
    await waitForProviderIdle()
    vi.clearAllMocks()

    getMock.mockRejectedValue(new Error('userinfo failed'))

    fireEvent.click(screen.getByRole('button', { name: 'refresh-user' }))

    // Wait for the state to settle after refresh failure
    await waitFor(() => {
      expect(screen.getByTestId('username')).toHaveTextContent('admin')
    })

    expect(screen.getByTestId('roles')).toHaveTextContent('admin')
  })

  it('clears the local session and navigates to login when logout succeeds', async () => {
    storedAccessToken = 'cached-access-token'
    storedUser = adminUser
    storedRoles = adminRoles
    isAccessTokenExpiredMock.mockReturnValue(false)

    renderAuthProvider()
    await waitForProviderIdle()
    vi.clearAllMocks()

    fireEvent.click(screen.getByRole('button', { name: 'logout' }))

    await waitFor(() => {
      expect(logoutRequestMock).toHaveBeenCalledTimes(1)
    })

    expect(clearRefreshTokenMock).toHaveBeenCalledTimes(1)
    expect(clearSessionMock).toHaveBeenCalledTimes(1)
    expect(clearCSRFTokenMock).toHaveBeenCalledTimes(1)
    expect(navigateMock).toHaveBeenCalledWith('/login')
    expect(screen.getByTestId('authenticated')).toHaveTextContent('false')
    expect(screen.getByTestId('username').textContent).toBe('')
  })

  it('clears the local session and navigates to login when logout fails', async () => {
    storedAccessToken = 'cached-access-token'
    storedUser = adminUser
    storedRoles = adminRoles
    isAccessTokenExpiredMock.mockReturnValue(false)
    logoutRequestMock.mockRejectedValue(new Error('logout failed'))

    renderAuthProvider()
    await waitForProviderIdle()
    vi.clearAllMocks()

    logoutRequestMock.mockRejectedValue(new Error('logout failed'))
    fireEvent.click(screen.getByRole('button', { name: 'logout' }))

    await waitFor(() => {
      expect(logoutRequestMock).toHaveBeenCalledTimes(1)
    })

    expect(clearRefreshTokenMock).toHaveBeenCalledTimes(1)
    expect(clearSessionMock).toHaveBeenCalledTimes(1)
    expect(clearCSRFTokenMock).toHaveBeenCalledTimes(1)
    expect(navigateMock).toHaveBeenCalledWith('/login')
    expect(screen.getByTestId('authenticated')).toHaveTextContent('false')
    expect(screen.getByTestId('username').textContent).toBe('')
  })
})
