import type { ReactElement, ReactNode } from 'react'
import { cloneElement, isValidElement } 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 { PaginatedData } from '@/types/http'
import type { Role, RoleListParams, RoleStatus } from '@/types/role'
import { RolesPage } from './RolesPage'

const listRolesMock = vi.fn<(params?: RoleListParams) => Promise<PaginatedData<Role>>>()
const deleteRoleMock = vi.fn<(id: number) => Promise<void>>()
const updateRoleStatusMock = vi.fn<(id: number, status: RoleStatus) => Promise<void>>()
const messageSuccessMock = vi.fn()
const messageErrorMock = vi.fn()

vi.mock('antd', async () => {
  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 ''
  }

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

      return (
        <button type={htmlType ?? 'button'} onClick={onClick} {...props}>
          {children}
        </button>
      )
    },
    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?.()
            }
          }}
        />
      )
    },
    Popconfirm: ({
      children,
      onConfirm,
    }: {
      children: ReactElement
      onConfirm?: () => void
    }) => {
      if (!isValidElement(children)) {
        return children
      }

      const child = children as ReactElement<{ onClick?: () => void }>

      return cloneElement(child, {
        onClick: () => onConfirm?.(),
      })
    },
    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>
        </div>
      )
    },
    Tag: ({ children }: { children?: ReactNode }) => <span>{children}</span>,
    message: {
      success: (content: string) => messageSuccessMock(content),
      error: (content: string) => messageErrorMock(content),
    },
  }
})

vi.mock('@ant-design/icons', () => ({
  DeleteOutlined: () => <span>delete</span>,
  EditOutlined: () => <span>edit</span>,
  PlusOutlined: () => <span>plus</span>,
  ReloadOutlined: () => <span>reload</span>,
  SafetyOutlined: () => <span>safety</span>,
  SearchOutlined: () => <span>search</span>,
}))

vi.mock('@/components/common', () => ({
  PageHeader: ({
    title,
    description,
    actions,
  }: {
    title: ReactNode
    description?: ReactNode
    actions?: ReactNode
  }) => (
    <section>
      <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/roles', () => ({
  listRoles: (params?: RoleListParams) => listRolesMock(params),
  deleteRole: (id: number) => deleteRoleMock(id),
  updateRoleStatus: (id: number, status: RoleStatus) => updateRoleStatusMock(id, status),
}))

vi.mock('./RoleFormModal', () => ({
  RoleFormModal: ({
    open,
    role,
    onSuccess,
    onClose,
  }: {
    open: boolean
    role: Role | null
    onSuccess: () => void
    onClose: () => void
  }) => (
    open ? (
      <div data-testid="role-form-modal">
        <span>{`role-form:${role?.code ?? 'create'}`}</span>
        <button type="button" onClick={onSuccess}>role form success</button>
        <button type="button" onClick={onClose}>role form close</button>
      </div>
    ) : null
  ),
}))

vi.mock('./RolePermissionsModal', () => ({
  RolePermissionsModal: ({
    open,
    role,
    onSuccess,
    onClose,
  }: {
    open: boolean
    role: Role | null
    onSuccess: () => void
    onClose: () => void
  }) => (
    open ? (
      <div data-testid="role-permissions-modal">
        <span>{`role-permissions:${role?.code ?? 'none'}`}</span>
        <button type="button" onClick={onSuccess}>role permissions success</button>
        <button type="button" onClick={onClose}>role permissions close</button>
      </div>
    ) : null
  ),
}))

function buildRole(
  id: number,
  name: string,
  code: string,
  status: RoleStatus,
  isSystem = false,
): Role {
  return {
    id,
    name,
    code,
    description: `${name} description`,
    is_system: isSystem,
    is_default: id === 2,
    status,
  }
}

describe('RolesPage', () => {
  let currentRoles: Role[]

  beforeEach(() => {
    currentRoles = [
      buildRole(1, 'Admin', 'admin', 1, true),
      buildRole(2, 'Editor', 'editor', 1),
      buildRole(3, 'Auditor', 'auditor', 0),
    ]

    listRolesMock.mockReset()
    deleteRoleMock.mockReset()
    updateRoleStatusMock.mockReset()
    messageSuccessMock.mockReset()
    messageErrorMock.mockReset()

    listRolesMock.mockImplementation(async (params?: RoleListParams) => {
      const keyword = (params?.keyword || '').toLowerCase()
      let items = currentRoles

      if (keyword) {
        items = items.filter((role) => (
          role.name.toLowerCase().includes(keyword) ||
          role.code.toLowerCase().includes(keyword)
        ))
      }

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

      return {
        items: items.map((role) => ({ ...role })),
        total: items.length,
        page: params?.page ?? 1,
        page_size: params?.page_size ?? 20,
      }
    })

    deleteRoleMock.mockImplementation(async (id: number) => {
      currentRoles = currentRoles.filter((role) => role.id !== id)
    })

    updateRoleStatusMock.mockImplementation(async (id: number, status: RoleStatus) => {
      currentRoles = currentRoles.map((role) => (
        role.id === id ? { ...role, status } : role
      ))
    })
  })

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

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

    render(<RolesPage />)

    expect(await screen.findByText('Admin')).toBeInTheDocument()
    expect(screen.getByText('Editor')).toBeInTheDocument()
    expect(listRolesMock).toHaveBeenLastCalledWith(expect.objectContaining({
      page: 1,
      page_size: 20,
      keyword: undefined,
      status: undefined,
    }))

    const keywordInput = screen.getByPlaceholderText('角色名称/代码')
    const statusSelect = screen.getByRole('combobox')

    await user.type(keywordInput, 'audit')
    await user.selectOptions(statusSelect, '0')
    await user.click(screen.getByRole('button', { name: '查询' }))

    await waitFor(() => expect(screen.queryByText('Admin')).not.toBeInTheDocument())
    expect(screen.getByText('Auditor')).toBeInTheDocument()
    expect(listRolesMock).toHaveBeenLastCalledWith(expect.objectContaining({
      keyword: 'audit',
      status: 0,
    }))

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

    await waitFor(() => expect(screen.getByText('Admin')).toBeInTheDocument())
    expect(screen.getByText('Editor')).toBeInTheDocument()
    expect(listRolesMock).toHaveBeenLastCalledWith(expect.objectContaining({
      keyword: undefined,
      status: undefined,
    }))

    const refreshCallCount = listRolesMock.mock.calls.length
    await user.click(screen.getByRole('button', { name: '刷新' }))
    await waitFor(() => expect(listRolesMock.mock.calls.length).toBeGreaterThan(refreshCallCount))

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

    const createCallCount = listRolesMock.mock.calls.length
    await user.click(screen.getByRole('button', { name: '创建角色' }))
    expect(screen.getByTestId('role-form-modal')).toHaveTextContent('role-form:create')

    await user.click(screen.getByRole('button', { name: 'role form success' }))
    await waitFor(() => expect(listRolesMock.mock.calls.length).toBeGreaterThan(createCallCount))
    await waitFor(() => expect(screen.queryByTestId('role-form-modal')).not.toBeInTheDocument())

    const editorRow = screen.getByTestId('table-row-2')

    const editCallCount = listRolesMock.mock.calls.length
    await user.click(within(editorRow).getByRole('button', { name: '编辑' }))
    expect(screen.getByTestId('role-form-modal')).toHaveTextContent('role-form:editor')

    await user.click(screen.getByRole('button', { name: 'role form success' }))
    await waitFor(() => expect(listRolesMock.mock.calls.length).toBeGreaterThan(editCallCount))
    await waitFor(() => expect(screen.queryByTestId('role-form-modal')).not.toBeInTheDocument())

    await user.click(within(editorRow).getByRole('button', { name: '权限' }))
    expect(screen.getByTestId('role-permissions-modal')).toHaveTextContent('role-permissions:editor')

    await user.click(screen.getByRole('button', { name: 'role permissions success' }))
    await waitFor(() => expect(screen.queryByTestId('role-permissions-modal')).not.toBeInTheDocument())
  })

  it('honors system-role protections, toggles status branches, and deletes non-system roles', async () => {
    const user = userEvent.setup()

    render(<RolesPage />)

    expect(await screen.findByText('Admin')).toBeInTheDocument()

    const adminRow = screen.getByTestId('table-row-1')
    expect(within(adminRow).getByRole('button', { name: '编辑' })).toBeDisabled()
    expect(within(adminRow).getByRole('button', { name: '禁用' })).toBeDisabled()
    expect(within(adminRow).getByRole('button', { name: '删除' })).toBeDisabled()

    const editorRow = screen.getByTestId('table-row-2')
    await user.click(within(editorRow).getByRole('button', { name: '禁用' }))
    await waitFor(() => expect(updateRoleStatusMock).toHaveBeenCalledWith(2, 0))
    expect(messageSuccessMock).toHaveBeenCalledWith('状态已更新')
    await waitFor(() => expect(within(screen.getByTestId('table-row-2')).getByRole('button', { name: '启用' })).toBeInTheDocument())

    const auditorRow = screen.getByTestId('table-row-3')
    await user.click(within(auditorRow).getByRole('button', { name: '启用' }))
    await waitFor(() => expect(updateRoleStatusMock).toHaveBeenCalledWith(3, 1))

    await user.click(within(screen.getByTestId('table-row-3')).getByRole('button', { name: '删除' }))
    await waitFor(() => expect(deleteRoleMock).toHaveBeenCalledWith(3))
    expect(messageSuccessMock).toHaveBeenCalledWith('角色已删除')
    await waitFor(() => expect(screen.queryByTestId('table-row-3')).not.toBeInTheDocument())
  })

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

    listRolesMock.mockReset()
    listRolesMock.mockRejectedValueOnce(new Error('roles failed'))
    listRolesMock.mockResolvedValue({
      items: currentRoles.map((role) => ({ ...role })),
      total: currentRoles.length,
      page: 1,
      page_size: 20,
    })

    render(<RolesPage />)

    expect(await screen.findByText('roles failed')).toBeInTheDocument()

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

    await waitFor(() => expect(screen.getByText('Admin')).toBeInTheDocument())
    expect(listRolesMock).toHaveBeenCalledTimes(2)
  })
})
