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

import type { LoginLog, LoginLogListParams, LoginLogListResponse } from '@/types/login-log'
import { LoginLogsPage } from './LoginLogsPage'

const listLoginLogsMock = vi.fn<(params?: LoginLogListParams) => Promise<LoginLogListResponse>>()

vi.mock('antd', async () => {
  const React = await import('react')

  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)
  }

  function flattenChildren(children: ReactNode): string {
    if (typeof children === 'string' || typeof children === 'number') {
      return String(children)
    }
    if (Array.isArray(children)) {
      return children.map(flattenChildren).join(' ').trim()
    }
    return ''
  }

  const RangePicker = ({
    placeholder = ['start', 'end'],
    onChange,
  }: {
    placeholder?: [string, string]
    onChange?: (dates: null, dateStrings: [string, string]) => void
  }) => {
    const [start, setStart] = React.useState('')
    const [end, setEnd] = React.useState('')

    return (
      <div>
        <input
          aria-label={placeholder[0]}
          value={start}
          onChange={(event) => {
            const nextStart = event.target.value
            setStart(nextStart)
            onChange?.(null, [nextStart, end])
          }}
        />
        <input
          aria-label={placeholder[1]}
          value={end}
          onChange={(event) => {
            const nextEnd = event.target.value
            setEnd(nextEnd)
            onChange?.(null, [start, nextEnd])
          }}
        />
      </div>
    )
  }

  return {
    Button: ({
      children,
      onClick,
      htmlType,
      type: buttonType,
      icon,
      ...props
    }: {
      children?: ReactNode
      onClick?: () => void
      htmlType?: 'button' | 'submit' | 'reset'
      [key: string]: unknown
    }) => {
      void buttonType
      void icon

      return (
        <button type={htmlType ?? 'button'} onClick={onClick} {...props}>
          {children}
        </button>
      )
    },
    DatePicker: { RangePicker },
    Input: ({
      onPressEnter,
      prefix,
      allowClear,
      ...props
    }: {
      onPressEnter?: () => void
      prefix?: ReactNode
      allowClear?: boolean
      [key: string]: unknown
    }) => {
      void prefix
      void allowClear

      return (
        <input
          {...props}
          onKeyDown={(event) => {
            if (event.key === 'Enter') {
              onPressEnter?.()
            }
          }}
        />
      )
    },
    Select: ({
      value,
      onChange,
      options = [],
      placeholder,
      allowClear,
      ...props
    }: {
      value?: string | number
      onChange?: (value: unknown) => void
      options?: Array<{ value: string | number, label: ReactNode }>
      placeholder?: string
      allowClear?: boolean
      [key: string]: unknown
    }) => {
      void allowClear

      return (
        <select
          aria-label={placeholder ?? 'select'}
          value={value === undefined ? '' : String(value)}
          onChange={(event) => {
            const nextValue = event.target.value
            if (nextValue === '') {
              onChange?.(undefined)
              return
            }

            const matchedOption = options.find((option) => String(option.value) === nextValue)
            onChange?.(matchedOption?.value ?? nextValue)
          }}
          {...props}
        >
          <option value="">{placeholder ?? 'all'}</option>
          {options.map((option) => (
            <option key={String(option.value)} value={String(option.value)}>
              {flattenChildren(option.label)}
            </option>
          ))}
        </select>
      )
    },
    Space: ({ children }: { children?: ReactNode }) => <div>{children}</div>,
    Table: ({
      columns,
      dataSource,
      rowKey,
      locale,
      pagination,
    }: {
      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 }
      pagination?: {
        current?: number
        pageSize?: number
        total?: number
        onChange?: (page: number, pageSize: number) => void
      }
    }) => {
      const rows = dataSource ?? []

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

      return (
        <div>
          <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>
          <button
            type="button"
            onClick={() => pagination?.onChange?.(1, 50)}
          >
            paginate
          </button>
          <span>{`${pagination?.current ?? 1}-${pagination?.pageSize ?? 20}-${pagination?.total ?? rows.length}`}</span>
        </div>
      )
    },
    Tag: ({ children }: { children?: ReactNode }) => <span>{children}</span>,
  }
})

vi.mock('@ant-design/icons', () => ({
  DownloadOutlined: () => <span>download</span>,
  EyeOutlined: () => <span>eye</span>,
  ReloadOutlined: () => <span>reload</span>,
  SearchOutlined: () => <span>search</span>,
}))

vi.mock('@/components/common', () => ({
  PageHeader: ({
    title,
    description,
    actions,
  }: {
    title: ReactNode
    description?: ReactNode
    actions?: ReactNode
  }) => (
    <section data-testid="page-header">
      <h1>{title}</h1>
      <p>{description}</p>
      {actions}
    </section>
  ),
}))

vi.mock('@/components/feedback', () => ({
  PageEmpty: ({ description }: { description?: ReactNode }) => <div>{description ?? 'empty'}</div>,
  PageError: ({
    description,
    onRetry,
  }: {
    description?: ReactNode
    onRetry?: () => void
  }) => (
    <div>
      <p>{description}</p>
      <button type="button" onClick={onRetry}>retry</button>
    </div>
  ),
}))

vi.mock('@/components/layout', () => ({
  PageLayout: ({ children }: { children?: ReactNode }) => <div>{children}</div>,
  FilterCard: ({ children }: { children?: ReactNode }) => <div>{children}</div>,
  TableCard: ({ children }: { children?: ReactNode }) => <div>{children}</div>,
}))

vi.mock('@/services/login-logs', () => ({
  listLoginLogs: (params?: LoginLogListParams) => listLoginLogsMock(params),
}))

vi.mock('./LoginLogDetailDrawer', () => ({
  LoginLogDetailDrawer: ({
    open,
    log,
    onClose,
  }: {
    open: boolean
    log: LoginLog | null
    onClose: () => void
  }) => (
    open ? (
      <div data-testid="login-log-detail-drawer">
        <span>{`detail:${log?.id ?? 'none'}`}</span>
        <button type="button" onClick={onClose}>close detail</button>
      </div>
    ) : null
  ),
}))

function buildLog(id: number, userId: number, status: 0 | 1): LoginLog {
  return {
    id,
    user_id: userId,
    login_type: 1,
    device_id: `device-${id}`,
    ip: `10.0.0.${id}`,
    location: 'Shanghai',
    status,
    fail_reason: status === 0 ? 'bad password' : undefined,
    created_at: `2026-03-27 0${id}:00:00`,
  }
}

describe('LoginLogsPage', () => {
  let currentLogs: LoginLog[]

  beforeEach(() => {
    currentLogs = [
      buildLog(1, 7, 1),
      buildLog(2, 8, 0),
      buildLog(3, 7, 0),
    ]

    listLoginLogsMock.mockReset()
    listLoginLogsMock.mockImplementation(async (params?: LoginLogListParams) => {
      const page = params?.page ?? 1
      const pageSize = params?.page_size ?? 20

      let items = currentLogs

      if (params?.user_id !== undefined) {
        items = items.filter((log) => log.user_id === params.user_id)
      }

      if (params?.status !== undefined) {
        items = items.filter((log) => log.status === params.status)
      }

      const start = (page - 1) * pageSize
      const pagedItems = items.slice(start, start + pageSize)

      return {
        items: pagedItems.map((log) => ({ ...log })),
        total: items.length,
        page,
        page_size: pageSize,
      }
    })
  })

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

  it('loads, filters, resets, paginates, refreshes, and opens detail drawer', async () => {
    const user = userEvent.setup()

    render(<LoginLogsPage />)

    expect(await screen.findByText('10.0.0.1')).toBeInTheDocument()
    expect(screen.getByText('10.0.0.2')).toBeInTheDocument()
    expect(listLoginLogsMock).toHaveBeenLastCalledWith(expect.objectContaining({
      page: 1,
      page_size: 20,
      user_id: undefined,
      status: undefined,
    }))

    // Find buttons by their text content
    const resetButton = screen.getByRole('button', { name: '重置' })
    const searchButton = screen.getByRole('button', { name: '查询' })
    const refreshButton = screen.getByRole('button', { name: '刷新' })
    const [userIdInput] = screen.getAllByRole('textbox')
    const statusSelect = screen.getByRole('combobox')

    await user.clear(userIdInput)
    await user.type(userIdInput, '7abc')
    await user.selectOptions(statusSelect, '0')
    await user.click(searchButton)

    await waitFor(() => expect(screen.queryByText('10.0.0.1')).not.toBeInTheDocument())
    expect(screen.getByText('10.0.0.3')).toBeInTheDocument()
    expect(listLoginLogsMock).toHaveBeenLastCalledWith(expect.objectContaining({
      user_id: 7,
      status: 0,
    }))

    await user.click(resetButton)

    // After reset, the component re-fetches. Wait for the UI to show unfiltered data (all 3 logs).
    await waitFor(() => {
      expect(screen.queryByText('10.0.0.1')).toBeInTheDocument()
      expect(screen.queryByText('10.0.0.2')).toBeInTheDocument()
      expect(screen.queryByText('10.0.0.3')).toBeInTheDocument()
    }, { timeout: 5000 })

    const callCountBeforeRefresh = listLoginLogsMock.mock.calls.length
    await user.click(refreshButton)
    await waitFor(() => expect(listLoginLogsMock.mock.calls.length).toBeGreaterThan(callCountBeforeRefresh))

    await user.click(screen.getByRole('button', { name: 'paginate' }))
    await waitFor(() => expect(listLoginLogsMock).toHaveBeenLastCalledWith(expect.objectContaining({
      page: 1,
      page_size: 50,
    })))

    const firstRow = screen.getByTestId('table-row-1')
    await user.click(within(firstRow).getByRole('button'))
    expect(screen.getByTestId('login-log-detail-drawer')).toHaveTextContent('detail:1')

    await user.click(screen.getByRole('button', { name: 'close detail' }))
    await waitFor(() => expect(screen.queryByTestId('login-log-detail-drawer')).not.toBeInTheDocument())
  })

  it('renders the page error state and retries fetching', async () => {
    const user = userEvent.setup()

    listLoginLogsMock.mockReset()
    listLoginLogsMock.mockRejectedValueOnce(new Error('login logs failed'))
    listLoginLogsMock.mockResolvedValue({
      items: currentLogs.map((log) => ({ ...log })),
      total: currentLogs.length,
      page: 1,
      page_size: 20,
    })

    render(<LoginLogsPage />)

    expect(await screen.findByText('login logs failed')).toBeInTheDocument()

    await user.click(screen.getByRole('button', { name: 'retry' }))

    await waitFor(() => expect(screen.getByText('10.0.0.1')).toBeInTheDocument())
    expect(listLoginLogsMock).toHaveBeenCalledTimes(2)
  })
})
