import { createElement, type ComponentType, type ReactElement, type ReactNode } from 'react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { Navigate, type RouteObject } from 'react-router-dom'

type PageDefinition = {
  exportName: string
  routePath: string
  requireAdmin?: boolean
}

type RouterFixture = {
  lazyPage: typeof import('./router').lazyPage
  router: { routes: RouteObject[] }
}

type LazyType = {
  _init: (payload: unknown) => unknown
  _payload: unknown
}

const publicPages: PageDefinition[] = [
  { routePath: '/login', exportName: 'LoginPage' },
  { routePath: '/register', exportName: 'RegisterPage' },
  { routePath: '/bootstrap-admin', exportName: 'BootstrapAdminPage' },
  { routePath: '/activate-account', exportName: 'ActivateAccountPage' },
  { routePath: '/login/oauth/callback', exportName: 'OAuthCallbackPage' },
  { routePath: '/forgot-password', exportName: 'ForgotPasswordPage' },
  { routePath: '/reset-password', exportName: 'ResetPasswordPage' },
]

const protectedPages: PageDefinition[] = [
  { routePath: 'dashboard', exportName: 'DashboardPage', requireAdmin: true },
  { routePath: 'users', exportName: 'UsersPage', requireAdmin: true },
  { routePath: 'roles', exportName: 'RolesPage', requireAdmin: true },
  { routePath: 'permissions', exportName: 'PermissionsPage', requireAdmin: true },
  { routePath: 'logs/login', exportName: 'LoginLogsPage', requireAdmin: true },
  { routePath: 'logs/operation', exportName: 'OperationLogsPage', requireAdmin: true },
  { routePath: 'webhooks', exportName: 'WebhooksPage' },
  { routePath: 'import-export', exportName: 'ImportExportPage', requireAdmin: true },
  { routePath: 'profile', exportName: 'ProfilePage' },
  { routePath: 'profile/security', exportName: 'ProfileSecurityPage' },
]

const resetModulePaths = ['./router', 'react-router-dom']

afterEach(() => {
  vi.clearAllMocks()
  for (const modulePath of resetModulePaths) {
    vi.doUnmock(modulePath)
  }
  vi.resetModules()
})

function asRouteObject(route: unknown): RouteObject {
  return route as RouteObject
}

function asElement(node: ReactNode | undefined): ReactElement<{ children?: ReactNode }> | null {
  return node && typeof node === 'object' && 'type' in node ? (node as ReactElement<{ children?: ReactNode }>) : null
}

function asLazyType(type: unknown): LazyType {
  return type as LazyType
}

function getComponentName(type: unknown): string | undefined {
  return typeof type === 'function' ? type.name : undefined
}

function getRouteByPath(routes: RouteObject[], path: string): RouteObject {
  const route = routes.find((candidate) => candidate.path === path)
  expect(route).toBeDefined()
  return route as RouteObject
}

function expectLazyElement(node: ReactNode | undefined): LazyType {
  const element = asElement(node)
  expect(element).not.toBeNull()

  const lazyType = asLazyType(element?.type)
  expect(typeof lazyType).toBe('object')
  expect(typeof lazyType._init).toBe('function')

  return lazyType
}

async function resolveLazyType(lazyType: LazyType): Promise<ComponentType<object>> {
  for (;;) {
    try {
      return lazyType._init(lazyType._payload) as ComponentType<object>
    } catch (thrown) {
      if (thrown && typeof (thrown as PromiseLike<unknown>).then === 'function') {
        await thrown
        continue
      }

      throw thrown
    }
  }
}

async function expectResolvedLazyName(node: ReactNode | undefined, expectedName: string) {
  const resolved = await resolveLazyType(expectLazyElement(node))
  expect(resolved.name).toBe(expectedName)
}

async function loadRouterFixture(): Promise<RouterFixture> {
  for (const modulePath of resetModulePaths) {
    vi.doUnmock(modulePath)
  }
  vi.resetModules()

  const module = await import('./router')

  return {
    lazyPage: module.lazyPage,
    router: module.router as { routes: RouteObject[] },
  }
}

describe('router', () => {
  it('maps each route to the expected shell and lazy page component shape', async () => {
    const { router } = await loadRouterFixture()
    const rootRoute = asRouteObject(router.routes[0])
    const rootChildren = (rootRoute.children ?? []).map(asRouteObject)

    expect(getComponentName(asElement(rootRoute.element)?.type)).toBe('RootLayout')
    expect(
      rootChildren
        .map((route) => route.path)
        .filter((path): path is string => Boolean(path)),
    ).toEqual([
      '/login',
      '/register',
      '/bootstrap-admin',
      '/activate-account',
      '/login/oauth/callback',
      '/forgot-password',
      '/reset-password',
      '/',
      '*',
    ])

    for (const page of publicPages) {
      const route = getRouteByPath(rootChildren, page.routePath)
      await expectResolvedLazyName(route.element, page.exportName)
    }

    const protectedRoute = getRouteByPath(rootChildren, '/')
    const protectedElement = asElement(protectedRoute.element)
    const protectedChildren = (protectedRoute.children ?? []).map(asRouteObject)

    expect(getComponentName(protectedElement?.type)).toBe('RequireAuth')
    expect(getComponentName(asElement(protectedElement?.props.children)?.type)).toBe('AdminLayout')
    expect(
      protectedChildren
        .map((route) => route.path)
        .filter((path): path is string => Boolean(path)),
    ).toEqual([
      'dashboard',
      'users',
      'devices',
      'roles',
      'permissions',
      'logs/login',
      'logs/operation',
      'webhooks',
      'import-export',
      'settings',
      'profile',
      'profile/security',
    ])

    const indexRoute = protectedChildren.find((route) => route.index)
    const indexElement = asElement(indexRoute?.element)

    expect(indexRoute).toBeDefined()
    expect(indexElement?.type).toBe(Navigate)
    expect(indexElement?.props).toEqual(expect.objectContaining({ to: '/dashboard', replace: true }))

    for (const page of protectedPages.filter((candidate) => candidate.requireAdmin)) {
      const route = getRouteByPath(protectedChildren, page.routePath)
      const element = asElement(route.element)

      expect(getComponentName(element?.type)).toBe('RequireAdmin')
      await expectResolvedLazyName(element?.props.children, page.exportName)
    }

    for (const page of protectedPages.filter((candidate) => !candidate.requireAdmin)) {
      const route = getRouteByPath(protectedChildren, page.routePath)

      expect(getComponentName(asElement(route.element)?.type)).not.toBe('RequireAdmin')
      await expectResolvedLazyName(route.element, page.exportName)
    }

    const notFoundRoute = getRouteByPath(rootChildren, '*')
    await expectResolvedLazyName(notFoundRoute.element, 'NotFoundPage')
  })

  it('resolves valid lazy exports and rejects invalid lazy exports clearly', async () => {
    const { lazyPage } = await loadRouterFixture()

    const LoginPage: ComponentType<object> = () => null
    const validLazyPage = lazyPage(async () => ({ LoginPage }), 'LoginPage')
    const invalidLazyPage = lazyPage(async () => ({ LoginPage: 'not-a-component' }), 'LoginPage')

    await expect(resolveLazyType(expectLazyElement(createElement(validLazyPage)))).resolves.toBe(LoginPage)
    await expect(resolveLazyType(expectLazyElement(createElement(invalidLazyPage)))).rejects.toThrow(
      'lazy route export "LoginPage" is not a React component',
    )
  })
})
