import type { ReactNode } from 'react'
import { act, 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 { MemoryRouter, Route, Routes } from 'react-router-dom'

import { AuthContext, type AuthContextValue } from '@/app/providers/auth-context'
import { AdminLayout } from './AdminLayout'
import styles from './AdminLayout.module.css'

const logoutMock = vi.fn(async () => {})

function flattenChildren(children: ReactNode): string {
  if (children === null || children === undefined || typeof children === 'boolean') {
    return ''
  }

  if (typeof children === 'string' || typeof children === 'number') {
    return String(children)
  }

  if (Array.isArray(children)) {
    return children.map(flattenChildren).join(' ').trim()
  }

  if (typeof children === 'object' && 'props' in children) {
    return flattenChildren((children as { props?: { children?: ReactNode } }).props?.children)
  }

  return ''
}

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

  type MenuItem = {
    key?: string
    label?: ReactNode
    children?: MenuItem[]
    type?: string
    onClick?: () => void
  }

  const Layout = Object.assign(
    ({
      children,
      className,
    }: {
      children?: ReactNode
      className?: string
    }) => (
      <div data-testid="layout" className={className}>
        {children}
      </div>
    ),
    {
      Sider: ({
        children,
        className,
      }: {
        children?: ReactNode
        className?: string
      }) => (
        <aside data-testid="sider" className={className}>
          {children}
        </aside>
      ),
      Header: ({
        children,
        className,
      }: {
        children?: ReactNode
        className?: string
      }) => (
        <header data-testid="header" className={className}>
          {children}
        </header>
      ),
      Content: ({
        children,
        className,
      }: {
        children?: ReactNode
        className?: string
      }) => (
        <main data-testid="content" className={className}>
          {children}
        </main>
      ),
    },
  )

  function Menu({
    items = [],
    onClick,
    selectedKeys = [],
    defaultOpenKeys = [],
  }: {
    items?: MenuItem[]
    onClick?: (info: { key: string }) => void
    selectedKeys?: string[]
    defaultOpenKeys?: string[]
  }) {
    const [openKeys, setOpenKeys] = React.useState((defaultOpenKeys ?? []).map(String))

    React.useEffect(() => {
      setOpenKeys((defaultOpenKeys ?? []).map(String))
    }, [defaultOpenKeys])

    const renderItem = (item: MenuItem): ReactNode => {
      if (item.type === 'divider') {
        return <hr key="divider" />
      }

      const key = String(item.key ?? flattenChildren(item.label))
      const label = flattenChildren(item.label)
      const hasChildren = Boolean(item.children?.length)

      return (
        <div key={key}>
          <button
            type="button"
            data-testid={`menu-item-${key}`}
            onClick={() => {
              if (hasChildren) {
                setOpenKeys((current) => (
                  current.includes(key)
                    ? current.filter((value) => value !== key)
                    : [...current, key]
                ))
                return
              }

              onClick?.({ key })
            }}
          >
            {label}
          </button>
          {hasChildren && openKeys.includes(key) ? item.children?.map(renderItem) : null}
        </div>
      )
    }

    return (
      <div
        data-testid="menu"
        data-open-keys={openKeys.join(',')}
        data-selected-keys={(selectedKeys ?? []).join(',')}
      >
        {items.map((item) => renderItem(item as MenuItem))}
      </div>
    )
  }

  function Dropdown({
    children,
    menu,
  }: {
    children?: ReactNode
    menu?: { items?: MenuItem[] }
  }) {
    const [open, setOpen] = React.useState(false)

    return (
      <div>
        <button type="button" data-testid="dropdown-trigger" onClick={() => setOpen((value) => !value)}>
          {children}
        </button>
        {open ? (
          <div data-testid="dropdown-menu">
            {menu?.items?.map((item, index) => {
              if (!item || item.type === 'divider') {
                return <hr key={`dropdown-divider-${index}`} />
              }

              const key = String(item.key ?? index)
              return (
                <button
                  key={key}
                  type="button"
                  data-testid={`dropdown-item-${key}`}
                  onClick={() => {
                    item.onClick?.()
                    setOpen(false)
                  }}
                >
                  {flattenChildren(item.label)}
                </button>
              )
            })}
          </div>
        ) : null}
      </div>
    )
  }

  return {
    Avatar: ({
      src,
      style,
      icon,
      size,
    }: {
      src?: string | null
      style?: { backgroundColor?: string }
      icon?: ReactNode
      size?: number
    }) => (
      <div
        data-testid="avatar"
        data-src={src ?? ''}
        data-background={style?.backgroundColor ?? ''}
        data-size={String(size ?? '')}
      >
        {src ? <img alt="avatar" src={src} /> : icon}
      </div>
    ),
    Button: ({
      children,
      icon,
      onClick,
      htmlType,
      ...props
    }: {
      children?: ReactNode
      icon?: ReactNode
      onClick?: () => void
      htmlType?: 'button' | 'submit' | 'reset'
      [key: string]: unknown
    }) => (
      <button type={htmlType ?? 'button'} onClick={onClick} {...props}>
        {children ?? icon}
      </button>
    ),
    Drawer: ({
      open,
      title,
      children,
      onClose,
    }: {
      open?: boolean
      title?: ReactNode
      children?: ReactNode
      onClose?: () => void
    }) => (
      open ? (
        <div data-testid="drawer">
          <div data-testid="drawer-title">{title}</div>
          <button type="button" onClick={onClose}>close drawer</button>
          {children}
        </div>
      ) : null
    ),
    Dropdown,
    Layout,
    Menu,
    Spin: ({
      tip,
      size,
      children,
    }: {
      tip?: ReactNode
      size?: string
      children?: ReactNode
    }) => (
      <div aria-busy="true" data-testid="spin" data-tip={flattenChildren(tip)} data-size={size}>
        {children}
      </div>
    ),
  }
})

vi.mock('@ant-design/icons', () => ({
  ApiOutlined: () => <span>api-icon</span>,
  DashboardOutlined: () => <span>dashboard-icon</span>,
  FileTextOutlined: () => <span>file-text-icon</span>,
  LogoutOutlined: () => <span>logout-icon</span>,
  MenuFoldOutlined: () => <span>menu-fold-icon</span>,
  MenuOutlined: () => <span>menu-icon</span>,
  MenuUnfoldOutlined: () => <span>menu-unfold-icon</span>,
  SafetyOutlined: () => <span>safety-icon</span>,
  SettingOutlined: () => <span>setting-icon</span>,
  UserOutlined: () => <span>user-icon</span>,
}))

const baseAuthContextValue: AuthContextValue = {
  user: {
    id: 1,
    username: 'admin',
    email: 'admin@example.com',
    phone: '13800138000',
    nickname: 'admin-nickname',
    avatar: '',
    status: 1,
  },
  roles: [],
  isAdmin: true,
  isAuthenticated: true,
  isLoading: false,
  onLoginSuccess: async () => {},
  logout: () => logoutMock(),
  refreshUser: async () => {},
}

function setWindowWidth(width: number) {
  Object.defineProperty(window, 'innerWidth', {
    configurable: true,
    writable: true,
    value: width,
  })
}

function renderAdminLayout(
  authContextValue: Partial<AuthContextValue> = {},
  initialEntry: string = '/profile/security',
  layoutChildren?: ReactNode,
) {
  const value: AuthContextValue = {
    ...baseAuthContextValue,
    ...authContextValue,
  }

  return render(
    <MemoryRouter initialEntries={[initialEntry]}>
      <AuthContext.Provider value={value}>
        <Routes>
          <Route path="/" element={<AdminLayout>{layoutChildren}</AdminLayout>}>
            <Route path="dashboard" element={<div>Dashboard Page</div>} />
            <Route path="users" element={<div>Users Page</div>} />
            <Route path="roles" element={<div>Roles Page</div>} />
            <Route path="permissions" element={<div>Permissions Page</div>} />
            <Route path="logs/login" element={<div>Login Logs Page</div>} />
            <Route path="logs/operation" element={<div>Operation Logs Page</div>} />
            <Route path="webhooks" element={<div>Webhooks Page</div>} />
            <Route path="import-export" element={<div>Import Export Page</div>} />
            <Route path="profile" element={<div>Profile Page</div>} />
            <Route path="profile/security" element={<div>Security Page</div>} />
          </Route>
        </Routes>
      </AuthContext.Provider>
    </MemoryRouter>,
  )
}

describe('AdminLayout', () => {
  beforeEach(() => {
    logoutMock.mockClear()
    setWindowWidth(1280)
  })

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

  it('shows a loading state while the session is restoring', () => {
    renderAdminLayout({ isLoading: true })

    expect(screen.getByTestId('spin')).toHaveAttribute('data-tip')
    expect(screen.queryByText('Security Page')).not.toBeInTheDocument()
  })

  it('renders desktop admin navigation, breadcrumbs, collapse state, dropdown actions, and mobile drawer navigation', async () => {
    const user = userEvent.setup()
    const { container } = renderAdminLayout({ isAdmin: true }, '/profile/security')

    expect(container.querySelector(`.${styles.logo}`)).toHaveTextContent('用户管理系统')
    expect(container.querySelector(`.${styles.userName}`)).toHaveTextContent('admin-nickname')
    expect(screen.getAllByTestId('menu')[0]).toHaveAttribute('data-open-keys', 'profile')
    expect(screen.getByText('Security Page')).toBeInTheDocument()

    const breadcrumbLink = container.querySelector(`.${styles.breadcrumbLink}`)
    expect(breadcrumbLink).not.toBeNull()
    await user.click(breadcrumbLink as HTMLElement)
    await waitFor(() => expect(screen.getByText('Profile Page')).toBeInTheDocument())

    await user.click(screen.getByTestId('menu-item-access-control'))
    await user.click(screen.getByTestId('menu-item-/users'))
    await waitFor(() => expect(screen.getByText('Users Page')).toBeInTheDocument())

    await user.click(screen.getByTestId('dropdown-trigger'))
    await user.click(screen.getByTestId('dropdown-item-security'))
    await waitFor(() => expect(screen.getByText('Security Page')).toBeInTheDocument())

    await user.click(screen.getByTestId('dropdown-trigger'))
    await user.click(screen.getByTestId('dropdown-item-profile'))
    await waitFor(() => expect(screen.getByText('Profile Page')).toBeInTheDocument())

    await user.click(screen.getByTestId('dropdown-trigger'))
    await user.click(screen.getByTestId('dropdown-item-logout'))
    await waitFor(() => expect(logoutMock).toHaveBeenCalledTimes(1))

    const collapseButton = screen.getByText('menu-fold-icon').closest('button')
    expect(collapseButton).not.toBeNull()
    await user.click(collapseButton as HTMLButtonElement)

    expect(container.querySelector(`.${styles.logo}`)).toHaveTextContent('UMS')
    expect(screen.getAllByTestId('menu')[0]).toHaveAttribute('data-open-keys', '')
    expect(screen.getByText('menu-unfold-icon')).toBeInTheDocument()

    await act(async () => {
      setWindowWidth(375)
      window.dispatchEvent(new Event('resize'))
    })

    await waitFor(() => expect(screen.getByRole('button', { name: 'menu-icon' })).toBeInTheDocument())
    await user.click(screen.getByRole('button', { name: 'menu-icon' }))

    const drawer = screen.getByTestId('drawer')
    expect(within(drawer).getByTestId('drawer-title')).toHaveTextContent('UMS')

    await user.click(within(drawer).getByTestId('menu-item-/dashboard'))
    await waitFor(() => expect(screen.getByText('Dashboard Page')).toBeInTheDocument())
    expect(screen.queryByTestId('drawer')).not.toBeInTheDocument()
  })

  it('renders the reduced mobile menu for non-admin users and uses avatar and username fallbacks correctly', async () => {
    const user = userEvent.setup()

    setWindowWidth(375)

    const { container } = renderAdminLayout(
      {
        isAdmin: false,
        user: {
          id: 2,
          username: 'operator-name',
          email: 'operator@example.com',
          phone: '',
          nickname: '',
          avatar: 'https://example.com/avatar.png',
          status: 1,
        },
      },
      '/profile',
    )

    expect(screen.queryByTestId('menu-item-access-control')).not.toBeInTheDocument()
    expect(screen.queryByTestId('menu-item-logs')).not.toBeInTheDocument()
    expect(screen.getAllByTestId('menu')[0]).toHaveAttribute('data-open-keys', 'profile')
    expect(screen.getByTestId('avatar')).toHaveAttribute('data-src', 'https://example.com/avatar.png')
    expect(screen.getByTestId('avatar')).toHaveAttribute('data-background', '')
    expect(container.querySelector(`.${styles.userName}`)).toHaveTextContent('operator-name')

    await user.click(screen.getByRole('button', { name: 'menu-icon' }))
    const drawer = screen.getByTestId('drawer')
    await user.click(within(drawer).getByTestId('menu-item-/webhooks'))

    await waitFor(() => expect(screen.getByText('Webhooks Page')).toBeInTheDocument())
    expect(screen.queryByTestId('drawer')).not.toBeInTheDocument()
  })

  it('opens the logs group for audit routes and prefers explicit children over the outlet while keeping the default user fallback', async () => {
    const { container } = renderAdminLayout(
      {
        user: null,
      },
      '/logs/login',
      <div>Injected Layout Content</div>,
    )

    expect(screen.getByText('Injected Layout Content')).toBeInTheDocument()
    expect(screen.queryByText('Login Logs Page')).not.toBeInTheDocument()
    expect(container.querySelector(`.${styles.userName}`)?.textContent?.trim().length).toBeGreaterThan(0)

    expect(screen.getAllByTestId('menu')[0]).toHaveAttribute('data-selected-keys', '/logs/login')
    expect(container.querySelector(`.${styles.breadcrumb}`)).toHaveTextContent('审计日志')
  })
})
