import { MemoryRouter } from 'react-router-dom'
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import { message } from 'antd'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'

import type { AuthCapabilities, TokenBundle } from '@/types'
import { AuthContext, type AuthContextValue } from '@/app/providers/auth-context'
import { LoginPage } from './LoginPage'

const TEXT = {
  usernamePlaceholder: '用户名',
  passwordPlaceholder: '密码',
  passwordLoginTab: '密码登录',
  emailCodeTab: '邮箱验证码',
  smsCodeTab: '短信验证码',
  forgotPassword: '忘记密码？',
  resendActivation: '重新发送激活邮件',
  createAccount: '创建账号',
  oauthLogin: '第三方登录',
  useGitHub: '使用 GitHub 登录',
  useWeChat: '使用 微信 登录',
  adminBootstrapTitle: '系统尚未初始化首个管理员账号',
  adminBootstrapDescription: '当前版本不提供默认账号。请先通过部署初始化流程或管理员初始化工具创建管理员，再使用登录页进入系统。',
  bootstrapAdminAction: '初始化管理员',
}

const navigateMock = vi.fn()
const assignMock = vi.fn()
const getAuthCapabilitiesMock = vi.fn<() => Promise<AuthCapabilities>>()
const getOAuthAuthorizationUrlMock = vi.fn()
const loginByPasswordMock = vi.fn()
const loginByEmailCodeMock = vi.fn()
const loginBySmsCodeMock = vi.fn()
const sendEmailCodeMock = vi.fn()
const sendSmsCodeMock = vi.fn()
const onLoginSuccessMock = vi.fn()

const defaultCapabilities: AuthCapabilities = {
  password: true,
  email_activation: false,
  email_code: false,
  sms_code: false,
  password_reset: false,
  admin_bootstrap_required: false,
  oauth_providers: [],
}

const loginTokenBundle: TokenBundle = {
  access_token: 'access-token',
  refresh_token: 'refresh-token',
  expires_in: 7200,
  user: {
    id: 1,
    username: 'admin',
    email: 'admin@example.com',
    phone: '13800138000',
    nickname: 'Admin',
    avatar: '',
    status: 1,
  },
}

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('@/services/auth', () => ({
  getAuthCapabilities: () => getAuthCapabilitiesMock(),
  getOAuthAuthorizationUrl: (provider: string, returnTo: string) =>
    getOAuthAuthorizationUrlMock(provider, returnTo),
  loginByPassword: (payload: unknown) => loginByPasswordMock(payload),
  loginByEmailCode: (payload: unknown) => loginByEmailCodeMock(payload),
  loginBySmsCode: (payload: unknown) => loginBySmsCodeMock(payload),
  sendEmailCode: (payload: unknown) => sendEmailCodeMock(payload),
  sendSmsCode: (payload: unknown) => sendSmsCodeMock(payload),
}))

const authContextValue: AuthContextValue = {
  user: null,
  roles: [],
  isAdmin: false,
  isAuthenticated: false,
  isLoading: false,
  onLoginSuccess: async (tokenBundle) => onLoginSuccessMock(tokenBundle),
  logout: vi.fn(async () => {}),
  refreshUser: vi.fn(async () => {}),
}

function renderLoginPage(
  initialEntry:
    | string
    | {
        pathname: string
        search?: string
        state?: unknown
      } = '/login',
) {
  return render(
    <MemoryRouter initialEntries={[initialEntry]}>
      <AuthContext.Provider value={authContextValue}>
        <LoginPage />
      </AuthContext.Provider>
    </MemoryRouter>,
  )
}

async function openEmailTab() {
  fireEvent.click(screen.getByRole('tab', { name: TEXT.emailCodeTab }))
  await waitFor(() => expect(screen.getByRole('tabpanel')).toBeInTheDocument())
  return screen.getByRole('tabpanel')
}

async function openSmsTab() {
  fireEvent.click(screen.getByRole('tab', { name: TEXT.smsCodeTab }))
  await waitFor(() => expect(screen.getByRole('tabpanel')).toBeInTheDocument())
  return screen.getByRole('tabpanel')
}

describe('LoginPage', () => {
  beforeEach(() => {
    navigateMock.mockReset()
    assignMock.mockReset()
    getAuthCapabilitiesMock.mockReset()
    getOAuthAuthorizationUrlMock.mockReset()
    loginByPasswordMock.mockReset()
    loginByEmailCodeMock.mockReset()
    loginBySmsCodeMock.mockReset()
    sendEmailCodeMock.mockReset()
    sendSmsCodeMock.mockReset()
    onLoginSuccessMock.mockReset()

    getAuthCapabilitiesMock.mockResolvedValue(defaultCapabilities)
    getOAuthAuthorizationUrlMock.mockResolvedValue({
      auth_url: 'https://oauth.example.com/default',
      state: 'oauth-state',
    })
    onLoginSuccessMock.mockResolvedValue(undefined)

    vi.spyOn(message, 'success').mockImplementation(() => undefined as never)
    vi.spyOn(message, 'error').mockImplementation(() => undefined as never)

    Object.defineProperty(window, 'location', {
      configurable: true,
      value: {
        ...window.location,
        assign: assignMock,
      },
    })
  })

  afterEach(() => {
    vi.restoreAllMocks()
  })

  it('renders password login only when email, sms, reset, and activation are disabled', async () => {
    renderLoginPage()

    await waitFor(() => expect(getAuthCapabilitiesMock).toHaveBeenCalledTimes(1))

    expect(screen.getByPlaceholderText(TEXT.usernamePlaceholder)).toBeInTheDocument()
    expect(screen.getByPlaceholderText(TEXT.passwordPlaceholder)).toBeInTheDocument()
    expect(screen.queryByText(TEXT.emailCodeTab)).not.toBeInTheDocument()
    expect(screen.queryByText(TEXT.smsCodeTab)).not.toBeInTheDocument()
    expect(screen.queryByRole('link', { name: TEXT.forgotPassword })).not.toBeInTheDocument()
    expect(screen.queryByRole('link', { name: TEXT.resendActivation })).not.toBeInTheDocument()
    expect(screen.getByRole('link', { name: TEXT.createAccount })).toBeInTheDocument()
  })

  it('falls back to default capabilities when loading auth capabilities fails', async () => {
    getAuthCapabilitiesMock.mockRejectedValue(new Error('capabilities unavailable'))

    renderLoginPage()

    await waitFor(() => expect(getAuthCapabilitiesMock).toHaveBeenCalledTimes(1))

    expect(screen.getByPlaceholderText(TEXT.usernamePlaceholder)).toBeInTheDocument()
    expect(screen.getByPlaceholderText(TEXT.passwordPlaceholder)).toBeInTheDocument()
    expect(screen.queryByText(TEXT.emailCodeTab)).not.toBeInTheDocument()
    expect(screen.queryByText(TEXT.smsCodeTab)).not.toBeInTheDocument()
    expect(screen.queryByText(TEXT.oauthLogin)).not.toBeInTheDocument()
  })

  it('shows enabled login methods and recovery entries from auth capabilities', async () => {
    getAuthCapabilitiesMock.mockResolvedValue({
      ...defaultCapabilities,
      email_activation: true,
      email_code: true,
      sms_code: true,
      password_reset: true,
    })

    renderLoginPage()

    await waitFor(() => expect(screen.getByText(TEXT.emailCodeTab)).toBeInTheDocument())

    expect(screen.getByText(TEXT.passwordLoginTab)).toBeInTheDocument()
    expect(screen.getByText(TEXT.emailCodeTab)).toBeInTheDocument()
    expect(screen.getByText(TEXT.smsCodeTab)).toBeInTheDocument()
    expect(screen.getByRole('link', { name: TEXT.forgotPassword })).toBeInTheDocument()
    expect(screen.getByRole('link', { name: TEXT.resendActivation })).toBeInTheDocument()
    expect(screen.getByRole('link', { name: TEXT.createAccount })).toBeInTheDocument()
  })

  it('submits password login and navigates to the saved route after success', async () => {
    loginByPasswordMock.mockResolvedValue(loginTokenBundle)

    renderLoginPage({
      pathname: '/login',
      state: { from: { pathname: '/profile' } },
    })

    await waitFor(() => expect(getAuthCapabilitiesMock).toHaveBeenCalledTimes(1))

    fireEvent.change(screen.getByPlaceholderText(TEXT.usernamePlaceholder), {
      target: { value: 'admin' },
    })
    fireEvent.change(screen.getByPlaceholderText(TEXT.passwordPlaceholder), {
      target: { value: 'SecurePass123!' },
    })
    fireEvent.click(screen.getByRole('button'))

    await waitFor(() => {
      expect(loginByPasswordMock).toHaveBeenCalledWith(expect.objectContaining({
        username: 'admin',
        password: 'SecurePass123!',
        device_id: expect.any(String),
        device_name: expect.any(String),
        device_browser: expect.any(String),
        device_os: expect.any(String),
      }))
    })

    expect(onLoginSuccessMock).toHaveBeenCalledWith(loginTokenBundle)
    expect(message.success).toHaveBeenCalledTimes(1)
    expect(navigateMock).toHaveBeenCalledWith('/profile', { replace: true })
  })

  it('uses the safe default redirect when the login query contains an unsafe target', async () => {
    loginByPasswordMock.mockResolvedValue(loginTokenBundle)

    renderLoginPage('/login?redirect=https://evil.example.com/phish')

    await waitFor(() => expect(getAuthCapabilitiesMock).toHaveBeenCalledTimes(1))

    fireEvent.change(screen.getByPlaceholderText(TEXT.usernamePlaceholder), {
      target: { value: 'admin' },
    })
    fireEvent.change(screen.getByPlaceholderText(TEXT.passwordPlaceholder), {
      target: { value: 'SecurePass123!' },
    })
    fireEvent.click(screen.getByRole('button'))

    await waitFor(() => expect(onLoginSuccessMock).toHaveBeenCalledWith(loginTokenBundle))

    expect(navigateMock).toHaveBeenCalledWith('/dashboard', { replace: true })
  })

  it('surfaces password login failures from the backend', async () => {
    loginByPasswordMock.mockRejectedValue(new Error('invalid credentials'))

    renderLoginPage()

    await waitFor(() => expect(getAuthCapabilitiesMock).toHaveBeenCalledTimes(1))

    fireEvent.change(screen.getByPlaceholderText(TEXT.usernamePlaceholder), {
      target: { value: 'admin' },
    })
    fireEvent.change(screen.getByPlaceholderText(TEXT.passwordPlaceholder), {
      target: { value: 'wrong-pass' },
    })
    fireEvent.click(screen.getByRole('button'))

    await waitFor(() => expect(message.error).toHaveBeenCalledWith('invalid credentials'))

    expect(onLoginSuccessMock).not.toHaveBeenCalled()
    expect(navigateMock).not.toHaveBeenCalled()
  })

  it('sends an email verification code and starts the resend countdown', async () => {
    getAuthCapabilitiesMock.mockResolvedValue({
      ...defaultCapabilities,
      email_code: true,
    })
    sendEmailCodeMock.mockResolvedValue(undefined)

    renderLoginPage()

    await waitFor(() => expect(screen.getByRole('tab', { name: TEXT.emailCodeTab })).toBeInTheDocument())

    const panel = await openEmailTab()
    const [emailInput] = within(panel).getAllByRole('textbox')
    const [sendCodeButton] = within(panel).getAllByRole('button')

    fireEvent.change(emailInput, { target: { value: 'admin@example.com' } })
    fireEvent.click(sendCodeButton)

    await waitFor(() => {
      expect(sendEmailCodeMock).toHaveBeenCalledWith({ email: 'admin@example.com' })
    })

    expect(message.success).toHaveBeenCalledTimes(1)
    expect(within(screen.getByRole('tabpanel')).getAllByRole('button')[0]).toHaveTextContent('60s')
  })

  it('ignores validation errors when the email address is missing', async () => {
    getAuthCapabilitiesMock.mockResolvedValue({
      ...defaultCapabilities,
      email_code: true,
    })

    renderLoginPage()

    await waitFor(() => expect(screen.getByRole('tab', { name: TEXT.emailCodeTab })).toBeInTheDocument())

    const panel = await openEmailTab()
    const [sendCodeButton] = within(panel).getAllByRole('button')

    fireEvent.click(sendCodeButton)

    await waitFor(() => expect(sendEmailCodeMock).not.toHaveBeenCalled())

    expect(message.error).not.toHaveBeenCalled()
    expect(within(screen.getByRole('tabpanel')).getAllByRole('button')[0].textContent).not.toContain('60s')
  })

  it('submits email code login and uses the redirect query after success', async () => {
    getAuthCapabilitiesMock.mockResolvedValue({
      ...defaultCapabilities,
      email_code: true,
    })
    loginByEmailCodeMock.mockResolvedValue(loginTokenBundle)

    renderLoginPage('/login?redirect=/profile')

    await waitFor(() => expect(screen.getByRole('tab', { name: TEXT.emailCodeTab })).toBeInTheDocument())

    const panel = await openEmailTab()
    const [emailInput, codeInput] = within(panel).getAllByRole('textbox')
    const [, submitButton] = within(panel).getAllByRole('button')

    fireEvent.change(emailInput, { target: { value: 'admin@example.com' } })
    fireEvent.change(codeInput, { target: { value: '123456' } })
    fireEvent.click(submitButton)

    await waitFor(() => {
      expect(loginByEmailCodeMock).toHaveBeenCalledWith({
        email: 'admin@example.com',
        code: '123456',
      })
    })

    expect(onLoginSuccessMock).toHaveBeenCalledWith(loginTokenBundle)
    expect(navigateMock).toHaveBeenCalledWith('/profile', { replace: true })
  })

  it('surfaces email code login failures from the backend', async () => {
    getAuthCapabilitiesMock.mockResolvedValue({
      ...defaultCapabilities,
      email_code: true,
    })
    loginByEmailCodeMock.mockRejectedValue(new Error('invalid email code'))

    renderLoginPage()

    await waitFor(() => expect(screen.getByRole('tab', { name: TEXT.emailCodeTab })).toBeInTheDocument())

    const panel = await openEmailTab()
    const [emailInput, codeInput] = within(panel).getAllByRole('textbox')
    const [, submitButton] = within(panel).getAllByRole('button')

    fireEvent.change(emailInput, { target: { value: 'admin@example.com' } })
    fireEvent.change(codeInput, { target: { value: '654321' } })
    fireEvent.click(submitButton)

    await waitFor(() => expect(message.error).toHaveBeenCalledWith('invalid email code'))

    expect(onLoginSuccessMock).not.toHaveBeenCalled()
  })

  it('sends an sms verification code with the login purpose and starts the resend countdown', async () => {
    getAuthCapabilitiesMock.mockResolvedValue({
      ...defaultCapabilities,
      sms_code: true,
    })
    sendSmsCodeMock.mockResolvedValue(undefined)

    renderLoginPage()

    await waitFor(() => expect(screen.getByRole('tab', { name: TEXT.smsCodeTab })).toBeInTheDocument())

    const panel = await openSmsTab()
    const [phoneInput] = within(panel).getAllByRole('textbox')
    const [sendCodeButton] = within(panel).getAllByRole('button')

    fireEvent.change(phoneInput, { target: { value: '13800138000' } })
    fireEvent.click(sendCodeButton)

    await waitFor(() => {
      expect(sendSmsCodeMock).toHaveBeenCalledWith({
        phone: '13800138000',
        purpose: 'login',
      })
    })

    expect(message.success).toHaveBeenCalledTimes(1)
    expect(within(screen.getByRole('tabpanel')).getAllByRole('button')[0]).toHaveTextContent('60s')
  })

  it('surfaces sms code send failures and resets the countdown state', async () => {
    getAuthCapabilitiesMock.mockResolvedValue({
      ...defaultCapabilities,
      sms_code: true,
    })
    sendSmsCodeMock.mockRejectedValue(new Error('sms send failed'))

    renderLoginPage()

    await waitFor(() => expect(screen.getByRole('tab', { name: TEXT.smsCodeTab })).toBeInTheDocument())

    const panel = await openSmsTab()
    const [phoneInput] = within(panel).getAllByRole('textbox')
    const [sendCodeButton] = within(panel).getAllByRole('button')

    fireEvent.change(phoneInput, { target: { value: '13800138000' } })
    fireEvent.click(sendCodeButton)

    await waitFor(() => expect(message.error).toHaveBeenCalledWith('sms send failed'))

    expect(within(screen.getByRole('tabpanel')).getAllByRole('button')[0].textContent).not.toContain('60s')
  })

  it('submits sms code login and completes the login flow after success', async () => {
    getAuthCapabilitiesMock.mockResolvedValue({
      ...defaultCapabilities,
      sms_code: true,
    })
    loginBySmsCodeMock.mockResolvedValue(loginTokenBundle)

    renderLoginPage()

    await waitFor(() => expect(screen.getByRole('tab', { name: TEXT.smsCodeTab })).toBeInTheDocument())

    const panel = await openSmsTab()
    const [phoneInput, codeInput] = within(panel).getAllByRole('textbox')
    const [, submitButton] = within(panel).getAllByRole('button')

    fireEvent.change(phoneInput, { target: { value: '13800138000' } })
    fireEvent.change(codeInput, { target: { value: '123456' } })
    fireEvent.click(submitButton)

    await waitFor(() => {
      expect(loginBySmsCodeMock).toHaveBeenCalledWith({
        phone: '13800138000',
        code: '123456',
      })
    })

    expect(onLoginSuccessMock).toHaveBeenCalledWith(loginTokenBundle)
    expect(navigateMock).toHaveBeenCalledWith('/dashboard', { replace: true })
  })

  it('surfaces sms code login failures from the backend', async () => {
    getAuthCapabilitiesMock.mockResolvedValue({
      ...defaultCapabilities,
      sms_code: true,
    })
    loginBySmsCodeMock.mockRejectedValue(new Error('invalid sms code'))

    renderLoginPage()

    await waitFor(() => expect(screen.getByRole('tab', { name: TEXT.smsCodeTab })).toBeInTheDocument())

    const panel = await openSmsTab()
    const [phoneInput, codeInput] = within(panel).getAllByRole('textbox')
    const [, submitButton] = within(panel).getAllByRole('button')

    fireEvent.change(phoneInput, { target: { value: '13800138000' } })
    fireEvent.change(codeInput, { target: { value: '654321' } })
    fireEvent.click(submitButton)

    await waitFor(() => expect(message.error).toHaveBeenCalledWith('invalid sms code'))

    expect(onLoginSuccessMock).not.toHaveBeenCalled()
  })

  it('renders oauth login actions when oauth providers are available', async () => {
    getAuthCapabilitiesMock.mockResolvedValue({
      ...defaultCapabilities,
      oauth_providers: [
        { provider: 'github', name: 'GitHub' },
        { provider: 'wechat', name: '微信' },
      ],
    })

    renderLoginPage()

    await waitFor(() => expect(screen.getByText(TEXT.oauthLogin)).toBeInTheDocument())

    expect(screen.getByRole('button', { name: TEXT.useGitHub })).toBeInTheDocument()
    expect(screen.getByRole('button', { name: TEXT.useWeChat })).toBeInTheDocument()
  })

  it('starts oauth login with a sanitized callback return target and filters disabled providers', async () => {
    getAuthCapabilitiesMock.mockResolvedValue({
      ...defaultCapabilities,
      oauth_providers: [
        { provider: 'wechat', name: '微信', enabled: false },
        { provider: 'github', name: 'GitHub' },
      ],
    })
    getOAuthAuthorizationUrlMock.mockResolvedValue({
      auth_url: 'https://oauth.example.com/github',
      state: 'oauth-state',
    })

    renderLoginPage('/login?redirect=https://evil.example.com/phish')

    await waitFor(() => expect(screen.getByRole('button', { name: TEXT.useGitHub })).toBeInTheDocument())

    expect(screen.queryByRole('button', { name: TEXT.useWeChat })).not.toBeInTheDocument()

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

    await waitFor(() => {
      expect(getOAuthAuthorizationUrlMock).toHaveBeenCalledWith(
        'github',
        `${window.location.origin}/login/oauth/callback`,
      )
    })

    expect(assignMock).toHaveBeenCalledWith('https://oauth.example.com/github')
  })

  it('surfaces oauth startup failures and clears the loading state', async () => {
    getAuthCapabilitiesMock.mockResolvedValue({
      ...defaultCapabilities,
      oauth_providers: [{ provider: 'github', name: 'GitHub' }],
    })
    getOAuthAuthorizationUrlMock.mockRejectedValue(new Error('oauth start failed'))

    renderLoginPage('/login?redirect=/profile')

    await waitFor(() => expect(screen.getByRole('button', { name: TEXT.useGitHub })).toBeInTheDocument())

    const oauthButton = screen.getByRole('button', { name: TEXT.useGitHub })
    fireEvent.click(oauthButton)

    await waitFor(() => expect(message.error).toHaveBeenCalledWith('oauth start failed'))

    expect(assignMock).not.toHaveBeenCalled()
    await waitFor(() => expect(screen.getByRole('button', { name: TEXT.useGitHub })).not.toBeDisabled())
  })

  it('tolerates null oauth provider payloads without crashing the login page', async () => {
    getAuthCapabilitiesMock.mockResolvedValue({
      ...defaultCapabilities,
      oauth_providers: null as unknown as AuthCapabilities['oauth_providers'],
    })

    renderLoginPage()

    await waitFor(() => expect(getAuthCapabilitiesMock).toHaveBeenCalledTimes(1))

    expect(screen.getByPlaceholderText(TEXT.usernamePlaceholder)).toBeInTheDocument()
    expect(screen.getByPlaceholderText(TEXT.passwordPlaceholder)).toBeInTheDocument()
    expect(screen.queryByText(TEXT.oauthLogin)).not.toBeInTheDocument()
  })

  it('shows a first-run admin bootstrap hint when no active admin is available', async () => {
    getAuthCapabilitiesMock.mockResolvedValue({
      ...defaultCapabilities,
      admin_bootstrap_required: true,
    })

    renderLoginPage()

    await waitFor(() => expect(screen.getByText(TEXT.adminBootstrapTitle)).toBeInTheDocument())

    expect(screen.getByText(TEXT.adminBootstrapDescription)).toBeInTheDocument()
    expect(screen.getByRole('button', { name: TEXT.bootstrapAdminAction }).closest('a')).toHaveAttribute('href', '/bootstrap-admin')
  })
})
