import { createInjector } from '@furystack/inject'
import { usingAsync } from '@furystack/utils'
import { afterEach, beforeEach, describe, expect, expectTypeOf, it, vi } from 'vitest'
import { initializeShadeRoot } from '../initialize.js'
import { LocationService } from '../services/location-service.js'
import { createComponent } from '../shade-component.js'
import { flushUpdates } from '../shade.js'
import type { TypedNestedRouteLinkProps } from './nested-route-link.js'
import { NestedRouteLink, createNestedRouteLink } from './nested-route-link.js'
import type { ConcatPaths, ExtractRouteParams, ExtractRoutePaths, UrlTree } from './nested-route-types.js'
import type { NestedRoute } from './nested-router.js'

// Minimal route type for type-level tests. Using Pick avoids the
// `children?: Record<string, NestedRoute<any, any, any>>` from NestedRoute<unknown>
// which would widen literal keys in intersections.
type TestRoute = Pick<NestedRoute<unknown, any, any>, 'component'>

describe('NestedRouteLink', () => {
  beforeEach(() => {
    document.body.innerHTML = '<div id="root"></div>'
  })
  afterEach(() => {
    document.body.innerHTML = ''
  })

  it('Should render a link with the correct href', async () => {
    await usingAsync(createInjector(), async (injector) => {
      const rootElement = document.getElementById('root') as HTMLDivElement

      initializeShadeRoot({
        injector,
        rootElement,
        jsxElement: (
          <NestedRouteLink id="link" path="/buttons">
            Buttons
          </NestedRouteLink>
        ),
      })
      await flushUpdates()
      expect(document.body.innerHTML).toBe(
        '<div id="root"><a is="nested-route-link" id="link" href="/buttons">Buttons</a></div>',
      )
    })
  })

  it('Should trigger SPA navigation on click', async () => {
    await usingAsync(createInjector(), async (injector) => {
      const rootElement = document.getElementById('root') as HTMLDivElement
      const onRouteChange = vi.fn()

      injector.get(LocationService).onLocationPathChanged.subscribe(onRouteChange)

      initializeShadeRoot({
        injector,
        rootElement,
        jsxElement: (
          <NestedRouteLink id="link" path="/buttons">
            Buttons
          </NestedRouteLink>
        ),
      })
      await flushUpdates()

      expect(onRouteChange).not.toBeCalled()
      document.getElementById('link')?.click()
      expect(onRouteChange).toBeCalledTimes(1)
    })
  })

  it('Should compile route params in the href', async () => {
    await usingAsync(createInjector(), async (injector) => {
      const rootElement = document.getElementById('root') as HTMLDivElement

      initializeShadeRoot({
        injector,
        rootElement,
        jsxElement: (
          <NestedRouteLink id="link" path="/users/:id" params={{ id: '42' }}>
            User 42
          </NestedRouteLink>
        ),
      })
      await flushUpdates()
      expect(document.body.innerHTML).toBe(
        '<div id="root"><a is="nested-route-link" id="link" href="/users/42">User 42</a></div>',
      )
    })
  })

  it('Should compile route params with multiple segments', async () => {
    await usingAsync(createInjector(), async (injector) => {
      const rootElement = document.getElementById('root') as HTMLDivElement

      initializeShadeRoot({
        injector,
        rootElement,
        jsxElement: (
          <NestedRouteLink id="link" path="/users/:userId/posts/:postId" params={{ userId: '1', postId: '99' }}>
            Post
          </NestedRouteLink>
        ),
      })
      await flushUpdates()
      expect(document.body.innerHTML).toBe(
        '<div id="root"><a is="nested-route-link" id="link" href="/users/1/posts/99">Post</a></div>',
      )
    })
  })

  it('Should append a serialized query string to the rendered href', async () => {
    await usingAsync(createInjector(), async (injector) => {
      const rootElement = document.getElementById('root') as HTMLDivElement

      initializeShadeRoot({
        injector,
        rootElement,
        jsxElement: (
          <NestedRouteLink id="link" path="/buttons" query={{ page: 2 }}>
            Buttons
          </NestedRouteLink>
        ),
      })
      await flushUpdates()

      const link = document.getElementById('link') as HTMLAnchorElement
      expect(link.getAttribute('href')?.startsWith('/buttons?')).toBe(true)
    })
  })

  it('Should append the hash segment to the rendered href', async () => {
    await usingAsync(createInjector(), async (injector) => {
      const rootElement = document.getElementById('root') as HTMLDivElement

      initializeShadeRoot({
        injector,
        rootElement,
        jsxElement: (
          <NestedRouteLink id="link" path="/buttons" hash="overview">
            Buttons
          </NestedRouteLink>
        ),
      })
      await flushUpdates()

      const link = document.getElementById('link') as HTMLAnchorElement
      expect(link.getAttribute('href')).toBe('/buttons#overview')
    })
  })
})

describe('Type utilities', () => {
  describe('ConcatPaths', () => {
    it('Should strip root "/" when concatenating', () => {
      expectTypeOf<ConcatPaths<'/', '/buttons'>>().toEqualTypeOf<'/buttons'>()
    })

    it('Should concatenate non-root parent paths', () => {
      expectTypeOf<ConcatPaths<'/layout-tests', '/appbar-only'>>().toEqualTypeOf<'/layout-tests/appbar-only'>()
    })

    it('Should handle deeply nested paths', () => {
      expectTypeOf<ConcatPaths<'/a/b', '/c'>>().toEqualTypeOf<'/a/b/c'>()
    })
  })

  describe('ExtractRouteParams', () => {
    it('Should return Record<string, never> for paths without params', () => {
      expectTypeOf<ExtractRouteParams<'/buttons'>>().toEqualTypeOf<Record<string, never>>()
    })

    it('Should extract a single param', () => {
      expectTypeOf<ExtractRouteParams<'/users/:id'>>().toEqualTypeOf<{ id: string }>()
    })

    it('Should extract multiple params', () => {
      expectTypeOf<ExtractRouteParams<'/users/:userId/posts/:postId'>>().toEqualTypeOf<{
        userId: string
        postId: string
      }>()
    })

    it('Should handle params at the beginning of the path', () => {
      expectTypeOf<ExtractRouteParams<'/:id'>>().toEqualTypeOf<{ id: string }>()
    })
  })

  describe('ExtractRoutePaths', () => {
    it('Should extract top-level paths', () => {
      type Routes = {
        '/a': TestRoute
        '/b': TestRoute
      }
      expectTypeOf<ExtractRoutePaths<Routes>>().toEqualTypeOf<'/a' | '/b'>()
    })

    it('Should extract nested child paths with root parent', () => {
      type Routes = {
        '/': TestRoute & {
          children: {
            '/buttons': TestRoute
            '/inputs': TestRoute
          }
        }
      }
      expectTypeOf<ExtractRoutePaths<Routes>>().toEqualTypeOf<'/' | '/buttons' | '/inputs'>()
    })

    it('Should extract nested child paths with non-root parent', () => {
      type Routes = {
        '/layout-tests': TestRoute & {
          children: {
            '/appbar-only': TestRoute
            '/auto-hide': TestRoute
          }
        }
      }
      expectTypeOf<ExtractRoutePaths<Routes>>().toEqualTypeOf<
        '/layout-tests' | '/layout-tests/appbar-only' | '/layout-tests/auto-hide'
      >()
    })

    it('Should handle mixed flat and nested routes', () => {
      type Routes = {
        '/standalone': TestRoute
        '/parent': TestRoute & {
          children: {
            '/child': TestRoute
          }
        }
      }
      expectTypeOf<ExtractRoutePaths<Routes>>().toEqualTypeOf<'/standalone' | '/parent' | '/parent/child'>()
    })
  })

  describe('UrlTree', () => {
    it('Should accept a flat object of valid paths', () => {
      type Paths = '/a' | '/b'
      const urls = {
        a: '/a',
        b: '/b',
      } satisfies UrlTree<Paths>
      expectTypeOf(urls).toExtend<UrlTree<Paths>>()
    })

    it('Should accept nested objects of valid paths', () => {
      type Paths = '/' | '/buttons' | '/layout-tests' | '/layout-tests/appbar-only'
      const urls = {
        home: '/',
        buttons: '/buttons',
        layoutTests: {
          index: '/layout-tests',
          appBarOnly: '/layout-tests/appbar-only',
        },
      } satisfies UrlTree<Paths>
      expectTypeOf(urls).toExtend<UrlTree<Paths>>()
    })
  })

  describe('TypedNestedRouteLinkProps', () => {
    it('Should make params optional for paths without parameters', () => {
      type Props = TypedNestedRouteLinkProps<'/buttons'>
      expectTypeOf<Props['path']>().toEqualTypeOf<'/buttons'>()
      expectTypeOf<Props>().toExtend<{ params?: Record<string, string> }>()
    })

    it('Should require params for parameterized paths', () => {
      type Props = TypedNestedRouteLinkProps<'/users/:id'>
      expectTypeOf<Props['path']>().toEqualTypeOf<'/users/:id'>()
      expectTypeOf<Props>().toExtend<{ params: { id: string } }>()
    })

    it('Should require all params for multi-param paths', () => {
      type Props = TypedNestedRouteLinkProps<'/users/:userId/posts/:postId'>
      expectTypeOf<Props>().toExtend<{ params: { userId: string; postId: string } }>()
    })
  })

  describe('NestedRouteLink param inference', () => {
    it('Should infer params as optional when path has no parameters', () => {
      expectTypeOf(NestedRouteLink).parameter(0).toHaveProperty('params')
      expectTypeOf(NestedRouteLink<'/buttons'>)
        .parameter(0)
        .toExtend<{ params?: Record<string, string> }>()
    })

    it('Should infer params as required when path has a parameter', () => {
      expectTypeOf(NestedRouteLink<'/users/:id'>)
        .parameter(0)
        .toExtend<{ params: { id: string } }>()
    })

    it('Should infer multiple params from path', () => {
      expectTypeOf(NestedRouteLink<'/users/:userId/posts/:postId'>)
        .parameter(0)
        .toExtend<{ params: { userId: string; postId: string } }>()
    })
  })

  describe('createNestedRouteLink', () => {
    it('Should constrain path to valid route paths', () => {
      type Routes = {
        '/': TestRoute & {
          children: {
            '/buttons': TestRoute
          }
        }
      }

      const AppLink = createNestedRouteLink<Routes>()
      expectTypeOf(AppLink).parameter(0).toHaveProperty('path')
    })

    it('Should reject invalid paths', () => {
      type Routes = {
        '/': TestRoute & {
          children: {
            '/buttons': TestRoute
          }
        }
      }

      const AppLink = createNestedRouteLink<Routes>()
      // @ts-expect-error -- '/nonexistent' is not a valid route path
      AppLink({ path: '/nonexistent' })
    })

    it('Should require params for parameterized routes in the tree', () => {
      type Routes = {
        '/': TestRoute & {
          children: {
            '/users/:userId': TestRoute
          }
        }
      }

      const AppLink = createNestedRouteLink<Routes>()
      expectTypeOf(AppLink<'/users/:userId'>)
        .parameter(0)
        .toExtend<{ params: { userId: string } }>()
    })

    it('Should require combined params from parent and child route segments', () => {
      type Routes = {
        '/users/:userId': TestRoute & {
          children: {
            '/posts/:postId': TestRoute
          }
        }
      }

      const AppLink = createNestedRouteLink<Routes>()
      expectTypeOf(AppLink<'/users/:userId/posts/:postId'>)
        .parameter(0)
        .toExtend<{ params: { userId: string; postId: string } }>()
    })

    it('Should accept routes with typed match parameters (NestedRoute<T>)', () => {
      const routes = {
        '/stacks/:stackName': {
          component: ({ match }) => <div>{match.params.stackName}</div>,
        },
      } satisfies Record<string, NestedRoute<{ stackName: string }>>

      const AppLink = createNestedRouteLink<typeof routes>()
      expectTypeOf(AppLink<'/stacks/:stackName'>)
        .parameter(0)
        .toExtend<{ params: { stackName: string } }>()
    })

    it('Should accept a mixed route tree with typed and untyped match parameters', () => {
      const usersRoute: NestedRoute<{ userId: string }> = {
        component: ({ match }) => <div>{match.params.userId}</div>,
      }
      const buttonsRoute: NestedRoute = {
        component: () => <div />,
      }

      const routes = {
        '/': {
          component: ({ outlet }) => outlet ?? <div />,
          children: {
            '/buttons': buttonsRoute,
            '/users/:userId': usersRoute,
          },
        },
      } satisfies Record<string, NestedRoute<any, any, any>>

      const AppLink = createNestedRouteLink<typeof routes>()
      expectTypeOf(AppLink).parameter(0).toHaveProperty('path')
      expectTypeOf(AppLink<'/users/:userId'>)
        .parameter(0)
        .toExtend<{ params: { userId: string } }>()
    })

    it('Should enforce a declared required query shape on the link', () => {
      const routes = {
        '/list': {
          component: () => null as unknown as JSX.Element,
          query: (raw): { page: number } | null => (typeof raw.page === 'number' ? { page: raw.page } : null),
        },
      } satisfies Record<string, NestedRoute<any, any, any>>

      const AppLink = createNestedRouteLink<typeof routes>()
      expectTypeOf(AppLink<'/list'>)
        .parameter(0)
        .toExtend<{ query: { page: number } }>()
    })

    it('Should narrow hash on the link to the declared literal tuple', () => {
      const routes = {
        '/tabs': {
          component: () => null as unknown as JSX.Element,
          hash: ['overview', 'details'] as const,
        },
      } satisfies Record<string, NestedRoute<any, any, any>>

      const AppLink = createNestedRouteLink<typeof routes>()
      expectTypeOf(AppLink<'/tabs'>)
        .parameter(0)
        .toExtend<{ hash?: 'overview' | 'details' }>()
    })
  })
})
