import type { ReactElement, ReactNode } from 'react'
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { message } from 'antd'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'

import type { OAuthProviderInfo } from '@/types'
import type { Device } from '@/types/device'
import type { LoginLog } from '@/types/login-log'
import type { OperationLog } from '@/types/operation-log'
import { ProfileSecurityPage } from './ProfileSecurityPage'

const originalGetComputedStyle = window.getComputedStyle
const useAuthMock = vi.fn()
const parseOAuthCallbackHashMock = vi.fn()
const getAuthCapabilitiesMock = vi.fn<() => Promise<{
  password: boolean
  email_activation: boolean
  email_code: boolean
  sms_code: boolean
  password_reset: boolean
  admin_bootstrap_required: boolean
  oauth_providers: OAuthProviderInfo[]
}>>()
const listSocialAccountsMock = vi.fn()
const startSocialBindingMock = vi.fn()
const unbindSocialAccountMock = vi.fn()
const getTOTPStatusMock = vi.fn<() => Promise<{ totp_enabled: boolean }>>()
const getTOTPSetupMock = vi.fn()
const enableTOTPMock = vi.fn<(code: string) => Promise<void>>()
const disableTOTPMock = vi.fn<(code: string) => Promise<void>>()
const updatePasswordMock = vi.fn()
const uploadAvatarMock = vi.fn<(userId: number, file: File) => Promise<void>>()
const listDevicesMock = vi.fn<() => Promise<{ items: Device[] }>>()
const updateDeviceStatusMock = vi.fn<(id: number, status: 0 | 1) => Promise<void>>()
const deleteDeviceMock = vi.fn<(id: number) => Promise<void>>()
const listMyLoginLogsMock = vi.fn<() => Promise<{ items: LoginLog[] }>>()
const listMyOperationLogsMock = vi.fn<() => Promise<{ items: OperationLog[] }>>()

vi.mock('antd', async () => {
  const actual = await vi.importActual<typeof import('antd')>('antd')

  function resolveRowKey<RecordType extends Record<string, unknown>>(
    record: RecordType,
    rowKey: string | ((row: RecordType) => string | number) | undefined,
    index: number,
  ): string {
    if (typeof rowKey === 'function') {
      return String(rowKey(record))
    }
    if (typeof rowKey === 'string') {
      return String(record[rowKey] ?? index)
    }
    return String(index)
  }

  return {
    ...actual,
    Popconfirm: ({
      children,
      title,
      onConfirm,
    }: {
      children: ReactElement
      title?: ReactNode
      onConfirm?: () => void
    }) => (
      <div>
        {children}
        <button type="button" onClick={() => onConfirm?.()}>
          {typeof title === 'string' ? title : 'confirm action'}
        </button>
      </div>
    ),
    Table: ({
      columns,
      dataSource,
      rowKey,
      locale,
    }: {
      columns: Array<{
        key?: string
        title?: ReactNode
        dataIndex?: string
        render?: (value: unknown, record: Record<string, unknown>, index: number) => ReactNode
      }>
      dataSource?: Array<Record<string, unknown>>
      rowKey?: string | ((row: Record<string, unknown>) => string | number)
      locale?: { emptyText?: ReactNode }
    }) => {
      const rows = dataSource ?? []

      if (rows.length === 0) {
        return <div>{locale?.emptyText ?? null}</div>
      }

      return (
        <table>
          <thead>
            <tr>
              {columns.map((column, index) => (
                <th key={column.key ?? column.dataIndex ?? index}>{column.title}</th>
              ))}
            </tr>
          </thead>
          <tbody>
            {rows.map((record, rowIndex) => (
              <tr
                key={resolveRowKey(record, rowKey, rowIndex)}
                data-testid={`table-row-${resolveRowKey(record, rowKey, rowIndex)}`}
              >
                {columns.map((column, columnIndex) => {
                  const value = column.dataIndex ? record[column.dataIndex] : undefined
                  const content = column.render ? column.render(value, record, rowIndex) : value
                  return (
                    <td key={column.key ?? column.dataIndex ?? columnIndex}>
                      {content as ReactNode}
                    </td>
                  )
                })}
              </tr>
            ))}
          </tbody>
        </table>
      )
    },
    Upload: ({
      children,
      beforeUpload,
      accept,
    }: {
      children: ReactNode
      beforeUpload?: (file: File) => boolean | Promise<boolean>
      accept?: string
    }) => (
      <label>
        {children}
        <input
          data-testid="upload-input"
          type="file"
          accept={accept}
          onChange={(event) => {
            const files = Array.from(event.currentTarget.files ?? [])
            files.forEach((file) => {
              void beforeUpload?.(file)
            })
          }}
        />
      </label>
    ),
  }
})

vi.mock('@/app/providers/auth-context', () => ({
  useAuth: () => useAuthMock(),
}))

vi.mock('@/lib/auth/oauth', () => ({
  parseOAuthCallbackHash: (hash: string) => parseOAuthCallbackHashMock(hash),
}))

vi.mock('@/services/auth', () => ({
  getAuthCapabilities: () => getAuthCapabilitiesMock(),
}))

vi.mock('@/services/social-accounts', () => ({
  listSocialAccounts: () => listSocialAccountsMock(),
  startSocialBinding: (payload: unknown) => startSocialBindingMock(payload),
  unbindSocialAccount: (provider: string, payload: unknown) => unbindSocialAccountMock(provider, payload),
}))

vi.mock('@/services/profile', () => ({
  disableTOTP: (code: string) => disableTOTPMock(code),
  enableTOTP: (code: string) => enableTOTPMock(code),
  getTOTPSetup: () => getTOTPSetupMock(),
  getTOTPStatus: () => getTOTPStatusMock(),
  updatePassword: (userId: number, payload: unknown) => updatePasswordMock(userId, payload),
  uploadAvatar: (userId: number, file: File) => uploadAvatarMock(userId, file),
}))

vi.mock('@/services/devices', () => ({
  deleteDevice: (id: number) => deleteDeviceMock(id),
  listDevices: () => listDevicesMock(),
  updateDeviceStatus: (id: number, status: 0 | 1) => updateDeviceStatusMock(id, status),
}))

vi.mock('@/services/login-logs', () => ({
  listMyLoginLogs: () => listMyLoginLogsMock(),
}))

vi.mock('@/services/operation-logs', () => ({
  listMyOperationLogs: () => listMyOperationLogsMock(),
}))

const contactBindingsSectionMock = vi.fn(() => <div data-testid="contact-bindings-section" />)

vi.mock('./ContactBindingsSection', () => ({
  ContactBindingsSection: (props: unknown) => contactBindingsSectionMock(props),
}))

function buildDevice(id: number, name: string, status: 0 | 1, isTrusted = false): Device {
  return {
    id,
    user_id: 1,
    device_id: `device-${id}`,
    device_name: name,
    device_type: 3,
    device_os: 'Windows',
    device_browser: 'Chrome',
    ip: `10.0.0.${id}`,
    location: 'Shanghai',
    status,
    is_trusted: isTrusted,
    trust_expires_at: isTrusted ? '2026-04-30 23:59:59' : null,
    last_active_time: `2026-03-27 1${id}:00:00`,
    created_at: '2026-03-27 10:00:00',
    updated_at: '2026-03-27 10:30:00',
  }
}

describe('ProfileSecurityPage behavior', () => {
  let currentDevices: Device[]
  let refreshUserMock: ReturnType<typeof vi.fn>

  beforeEach(() => {
    currentDevices = [
      buildDevice(101, 'Primary Device', 1),
      buildDevice(102, 'Backup Device', 0),
    ]

    refreshUserMock = vi.fn().mockResolvedValue(undefined)

    useAuthMock.mockReturnValue({
      user: { id: 1, avatar: '', username: 'admin' },
      refreshUser: refreshUserMock,
      roles: [],
      isAdmin: false,
      isAuthenticated: true,
      isLoading: false,
      onLoginSuccess: vi.fn(),
      logout: vi.fn(),
    })

    getAuthCapabilitiesMock.mockReset()
    parseOAuthCallbackHashMock.mockReset()
    listSocialAccountsMock.mockReset()
    startSocialBindingMock.mockReset()
    unbindSocialAccountMock.mockReset()
    getTOTPStatusMock.mockReset()
    getTOTPSetupMock.mockReset()
    enableTOTPMock.mockReset()
    disableTOTPMock.mockReset()
    updatePasswordMock.mockReset()
    uploadAvatarMock.mockReset()
    listDevicesMock.mockReset()
    updateDeviceStatusMock.mockReset()
    deleteDeviceMock.mockReset()
    listMyLoginLogsMock.mockReset()
    listMyOperationLogsMock.mockReset()

    getAuthCapabilitiesMock.mockResolvedValue({
      password: true,
      email_activation: false,
      email_code: true,
      sms_code: true,
      password_reset: true,
      admin_bootstrap_required: false,
      oauth_providers: [{ provider: 'github', name: 'GitHub' }],
    })
    parseOAuthCallbackHashMock.mockReturnValue({})
    listSocialAccountsMock.mockResolvedValue([])
    startSocialBindingMock.mockResolvedValue({
      auth_url: 'https://oauth.example.com/github',
      state: 'state-demo',
    })
    unbindSocialAccountMock.mockResolvedValue(undefined)
    getTOTPStatusMock.mockResolvedValue({ totp_enabled: false })
    getTOTPSetupMock.mockResolvedValue({
      secret: 'totp-secret',
      qr_code_base64: 'abc123',
      recovery_codes: ['rc-1', 'rc-2'],
    })
    enableTOTPMock.mockResolvedValue(undefined)
    disableTOTPMock.mockResolvedValue(undefined)
    updatePasswordMock.mockResolvedValue(undefined)
    uploadAvatarMock.mockResolvedValue(undefined)
    listDevicesMock.mockImplementation(async () => ({
      items: currentDevices.map((device) => ({ ...device })),
    }))
    updateDeviceStatusMock.mockImplementation(async (id: number, status: 0 | 1) => {
      currentDevices = currentDevices.map((device) => (
        device.id === id ? { ...device, status } : device
      ))
    })
    deleteDeviceMock.mockImplementation(async (id: number) => {
      currentDevices = currentDevices.filter((device) => device.id !== id)
    })
    listMyLoginLogsMock.mockResolvedValue({
      items: [{
        id: 201,
        login_type: 1,
        device_id: 'device-101',
        ip: '10.0.0.1',
        location: 'Shanghai',
        status: 1,
        created_at: '2026-03-27 09:00:00',
      }],
    })
    listMyOperationLogsMock.mockResolvedValue({
      items: [{
        id: 301,
        operation_type: 'user',
        operation_name: 'update profile',
        request_method: 'PUT',
        request_path: '/api/v1/users/me',
        request_params: '{}',
        response_status: 200,
        ip: '10.0.0.1',
        user_agent: 'Chrome',
        created_at: '2026-03-27 09:10:00',
      }],
    })
    contactBindingsSectionMock.mockClear()

    vi.spyOn(window, 'getComputedStyle').mockImplementation((element) => {
      return originalGetComputedStyle.call(window, element)
    })
    vi.spyOn(message, 'success').mockImplementation(() => undefined as never)
    vi.spyOn(message, 'error').mockImplementation(() => undefined as never)
    window.history.replaceState({}, '', '/profile/security')
  })

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

  it('opens the TOTP setup flow, validates the code, and enables TOTP', async () => {
    const user = userEvent.setup()

    render(<ProfileSecurityPage />)

    await waitFor(() => expect(screen.getByRole('button', { name: '启用 TOTP' })).toBeInTheDocument())

    await user.click(screen.getByRole('button', { name: '启用 TOTP' }))

    await waitFor(() => expect(getTOTPSetupMock).toHaveBeenCalledTimes(1))
    expect(await screen.findByText('密钥：totp-secret')).toBeInTheDocument()

    await user.click(screen.getByRole('button', { name: '确认启用' }))
    expect(message.error).toHaveBeenCalledWith('请输入 6 位验证码')
    expect(enableTOTPMock).not.toHaveBeenCalled()

    await user.type(screen.getByPlaceholderText('请输入 6 位验证码'), '123456')
    await user.click(screen.getByRole('button', { name: '确认启用' }))

    await waitFor(() => expect(enableTOTPMock).toHaveBeenCalledWith('123456'))
    expect(message.success).toHaveBeenCalledWith('TOTP 已启用')
    expect(screen.getByRole('button', { name: '禁用 TOTP' })).toBeInTheDocument()
  })

  it('shows an error when the initial security data fetch fails', async () => {
    listDevicesMock.mockRejectedValueOnce(new Error('load failed'))

    render(<ProfileSecurityPage />)

    await waitFor(() => expect(message.error).toHaveBeenCalledWith('load failed'))
    expect(await screen.findByText('暂无设备数据')).toBeInTheDocument()
  })

  it('handles a successful social callback, clears the hash, and refetches security data', async () => {
    parseOAuthCallbackHashMock.mockReturnValueOnce({
      status: 'success',
      provider: 'github',
      message: 'bind ok',
    })
    window.history.replaceState({}, '', '/profile/security#social-callback')
    const replaceStateSpy = vi.spyOn(window.history, 'replaceState')

    render(<ProfileSecurityPage />)

    await waitFor(() => expect(message.success).toHaveBeenCalledWith('bind ok'))
    await waitFor(() => expect(listDevicesMock).toHaveBeenCalledTimes(2))
    expect(replaceStateSpy.mock.calls.at(-1)?.[2]).toBe('/profile/security')
  })

  it('shows callback errors without refetching security data', async () => {
    parseOAuthCallbackHashMock.mockReturnValueOnce({
      status: 'error',
      provider: 'github',
      message: 'bind failed',
    })
    window.history.replaceState({}, '', '/profile/security#social-callback')

    render(<ProfileSecurityPage />)

    await waitFor(() => expect(message.error).toHaveBeenCalledWith('bind failed'))
    expect(listDevicesMock).toHaveBeenCalledTimes(1)
  })

  it('validates disable input and disables TOTP when a code is provided', async () => {
    const user = userEvent.setup()
    getTOTPStatusMock.mockResolvedValueOnce({ totp_enabled: true })

    render(<ProfileSecurityPage />)

    await waitFor(() => expect(screen.getByRole('button', { name: '禁用 TOTP' })).toBeInTheDocument())

    await user.click(screen.getByRole('button', { name: '禁用 TOTP' }))
    await user.click(screen.getByRole('button', { name: '确认禁用' }))

    expect(message.error).toHaveBeenCalledWith('请输入验证码或恢复码')
    expect(disableTOTPMock).not.toHaveBeenCalled()

    await user.type(screen.getByPlaceholderText('验证码或恢复码'), '654321')
    await user.click(screen.getByRole('button', { name: '确认禁用' }))

    await waitFor(() => expect(disableTOTPMock).toHaveBeenCalledWith('654321'))
    expect(message.success).toHaveBeenCalledWith('TOTP 已禁用')
  })

  it('rejects invalid avatar files and uploads valid images', async () => {
    render(<ProfileSecurityPage />)

    const uploadInput = await screen.findByTestId('upload-input') as HTMLInputElement
    const invalidFile = new File(['plain-text'], 'avatar.txt', { type: 'text/plain' })

    fireEvent.change(uploadInput, { target: { files: [invalidFile] } })
    expect(message.error).toHaveBeenCalledWith('只能上传图片文件')
    expect(uploadAvatarMock).not.toHaveBeenCalled()

    const oversizedFile = new File([new Uint8Array(2 * 1024 * 1024)], 'avatar-large.png', {
      type: 'image/png',
    })

    fireEvent.change(uploadInput, { target: { files: [oversizedFile] } })
    expect(message.error).toHaveBeenCalledWith('图片大小不能超过 2MB')
    expect(uploadAvatarMock).not.toHaveBeenCalled()

    const validFile = new File([new Uint8Array(1024)], 'avatar.png', { type: 'image/png' })

    fireEvent.change(uploadInput, { target: { files: [validFile] } })

    await waitFor(() => expect(uploadAvatarMock).toHaveBeenCalledWith(1, expect.any(File)))
    expect(refreshUserMock).toHaveBeenCalledTimes(1)
    expect(message.success).toHaveBeenCalledWith('头像上传成功')
  })

  it('submits password changes for the current user', async () => {
    const user = userEvent.setup()
    const { container } = render(<ProfileSecurityPage />)

    await waitFor(() => expect(container.querySelectorAll('input[type="password"]')).toHaveLength(3))

    const passwordInputs = Array.from(
      container.querySelectorAll('input[type="password"]'),
    ) as HTMLInputElement[]

    await user.type(passwordInputs[0], 'CurrentPass123')
    await user.type(passwordInputs[1], 'NewPass123!')
    await user.type(passwordInputs[2], 'NewPass123!')
    await user.click(container.querySelector('button[type="submit"]') as HTMLButtonElement)

    await waitFor(() =>
      expect(updatePasswordMock).toHaveBeenCalledWith(1, {
        current_password: 'CurrentPass123',
        new_password: 'NewPass123!',
        confirm_password: 'NewPass123!',
      }),
    )
    expect(message.success).toHaveBeenCalledWith('密码修改成功')
  })

  it('passes contact binding capabilities to ContactBindingsSection', async () => {
    render(<ProfileSecurityPage />)

    await waitFor(() => expect(contactBindingsSectionMock).toHaveBeenCalled())
    const latestProps = contactBindingsSectionMock.mock.calls.at(-1)?.[0] as {
      userId: number
      emailBindingEnabled: boolean
      phoneBindingEnabled: boolean
      refreshSessionUser: () => Promise<void>
    }

    expect(latestProps.userId).toBe(1)
    expect(latestProps.emailBindingEnabled).toBe(true)
    expect(latestProps.phoneBindingEnabled).toBe(true)
    await latestProps.refreshSessionUser()
    expect(refreshUserMock).toHaveBeenCalledTimes(1)
  })

  it('toggles device status, refetches the list, and deletes devices', async () => {
    const user = userEvent.setup()

    render(<ProfileSecurityPage />)

    expect(await screen.findByText('Primary Device')).toBeInTheDocument()
    expect(screen.getByText('Backup Device')).toBeInTheDocument()

    await user.click(within(screen.getByTestId('table-row-101')).getByRole('button', { name: '禁用' }))
    await waitFor(() => expect(updateDeviceStatusMock).toHaveBeenCalledWith(101, 0))
    expect(message.success).toHaveBeenCalledWith('设备已禁用')
    await waitFor(() => expect(within(screen.getByTestId('table-row-101')).getByRole('button', { name: '启用' })).toBeInTheDocument())

    await user.click(within(screen.getByTestId('table-row-102')).getByRole('button', { name: '启用' }))
    await waitFor(() => expect(updateDeviceStatusMock).toHaveBeenCalledWith(102, 1))
    expect(message.success).toHaveBeenCalledWith('设备已重新激活')
    await waitFor(() => expect(within(screen.getByTestId('table-row-102')).getByRole('button', { name: '禁用' })).toBeInTheDocument())

    await user.click(within(screen.getByTestId('table-row-101')).getByRole('button', { name: '确定删除此设备吗？' }))

    await waitFor(() => expect(deleteDeviceMock).toHaveBeenCalledWith(101))
    expect(message.success).toHaveBeenCalledWith('设备已删除')
    await waitFor(() => expect(screen.queryByTestId('table-row-101')).not.toBeInTheDocument())
  })

  it('unbinds a social account and refreshes the security data', async () => {
    const user = userEvent.setup()
    listSocialAccountsMock.mockResolvedValue([
      {
        id: 11,
        provider: 'github',
        nickname: 'octocat',
        avatar: '',
        status: 1,
        created_at: '2026-03-27 09:20:00',
      },
    ])

    render(<ProfileSecurityPage />)

    const row = await screen.findByTestId('table-row-11')
    await user.click(within(row).getByRole('button', { name: '解绑' }))

    const dialog = await screen.findByRole('dialog')
    const inputs = Array.from(dialog.querySelectorAll('input')) as HTMLInputElement[]
    await user.type(inputs[0], 'SecurePass123')
    await user.type(inputs[1], '654321')
    await user.click(within(dialog).getByRole('button', { name: '确认解绑' }))

    await waitFor(() =>
      expect(unbindSocialAccountMock).toHaveBeenCalledWith('github', {
        current_password: 'SecurePass123',
        totp_code: '654321',
      }),
    )
    await waitFor(() => expect(listSocialAccountsMock).toHaveBeenCalledTimes(2))
    expect(message.success).toHaveBeenCalledWith(expect.stringContaining('GitHub'))
  })

  it('skips protected actions when no authenticated user is available', async () => {
    useAuthMock.mockReturnValue({
      user: null,
      refreshUser: refreshUserMock,
      roles: [],
      isAdmin: false,
      isAuthenticated: false,
      isLoading: false,
      onLoginSuccess: vi.fn(),
      logout: vi.fn(),
    })

    const user = userEvent.setup()
    const { container } = render(<ProfileSecurityPage />)

    await waitFor(() => expect(listDevicesMock).not.toHaveBeenCalled())
    expect(screen.queryByTestId('contact-bindings-section')).not.toBeInTheDocument()

    const validFile = new File([new Uint8Array(1024)], 'avatar.png', { type: 'image/png' })
    fireEvent.change(await screen.findByTestId('upload-input'), { target: { files: [validFile] } })
    expect(uploadAvatarMock).not.toHaveBeenCalled()

    const passwordInputs = Array.from(
      container.querySelectorAll('input[type="password"]'),
    ) as HTMLInputElement[]
    await user.type(passwordInputs[0], 'CurrentPass123')
    await user.type(passwordInputs[1], 'NewPass123!')
    await user.type(passwordInputs[2], 'NewPass123!')
    await user.click(container.querySelector('button[type="submit"]') as HTMLButtonElement)
    expect(updatePasswordMock).not.toHaveBeenCalled()
  })
})
