import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { act, render, screen, waitFor, fireEvent } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import {
  Outlet,
  RouterProvider,
  createRouter,
  createRootRoute,
  createRoute,
} from '@tanstack/react-router'
import { RaceHubPage } from '../pages/RaceHubPage'
import { MAX_BROWSER_TIMEOUT } from '../lib/schedule'
import type { ContextAvailability, DatasetInfo, Meeting, RaceHub, Session, Weekend, WeekendContext } from '../types'

vi.mock('../api', () => ({
  fetchRaceHub: vi.fn(),
  fetchSeasons: vi.fn(),
  fetchLocalMeetings: vi.fn(),
  fetchWeekend: vi.fn(),
  fetchWeekendContext: vi.fn(),
}))

import { fetchLocalMeetings, fetchRaceHub, fetchSeasons, fetchWeekend, fetchWeekendContext } from '../api'

const mockFetchRaceHub = vi.mocked(fetchRaceHub)
const mockFetchSeasons = vi.mocked(fetchSeasons)
const mockFetchLocalMeetings = vi.mocked(fetchLocalMeetings)
const mockFetchWeekend = vi.mocked(fetchWeekend)
const mockFetchWeekendContext = vi.mocked(fetchWeekendContext)

const meeting: Meeting = {
  meeting_key: 1229,
  meeting_name: 'Monaco Grand Prix',
  meeting_official_name: 'FORMULA 1 GRAND PRIX DE MONACO 2025',
  location: 'Monaco',
  country_name: 'Monaco',
  country_code: 'MON',
  country_flag: '',
  circuit_short_name: 'Monaco',
  date_start: '2025-05-23T00:00:00+00:00',
  date_end: '2025-05-25T00:00:00+00:00',
  year: 2025,
}

const raceSession: Session = {
  session_key: 9472,
  session_name: 'Race',
  session_type: 'Race',
  meeting_key: 1229,
  date_start: '2025-05-25T13:00:00+00:00',
  date_end: '2025-05-25T15:00:00+00:00',
  gmt_offset: '02:00:00',
}

const qualSession: Session = {
  session_key: 9471,
  session_name: 'Qualifying',
  session_type: 'Qualifying',
  meeting_key: 1229,
  date_start: '2025-05-24T14:00:00+00:00',
  date_end: '2025-05-24T15:00:00+00:00',
  gmt_offset: '02:00:00',
}

const fullDatasets: Record<string, DatasetInfo> = {
  meeting: { status: 'available', source: 'local', count: 1 },
  session: { status: 'available', source: 'local', count: 1 },
  drivers: { status: 'available', source: 'local', count: 20 },
  results: { status: 'available', source: 'local', count: 20 },
  starting_grid: { status: 'available', source: 'local', count: 20 },
  stints: { status: 'available', source: 'local', count: 30 },
  pit_stops: { status: 'available', source: 'local', count: 18 },
  positions: { status: 'available', source: 'local', count: 120 },
  race_control: { status: 'available', source: 'local', count: 5 },
  weather: { status: 'available', source: 'local', count: 4 },
  laps: { status: 'available', source: 'local', count: 200 },
}

const raceHub: RaceHub = {
  source: 'local',
  session_key: 9472,
  datasets: fullDatasets,
  meeting,
  session: raceSession,
  drivers: [
    {
      driver_number: 1,
      name_acronym: 'VER',
      full_name: 'Max Verstappen',
      first_name: 'Max',
      last_name: 'Verstappen',
      team_name: 'Red Bull Racing',
      team_colour: '3671C6',
      headshot_url: '',
      broadcast_name: 'M VERSTAPPEN',
      session_key: 9472,
      meeting_key: 1229,
    },
  ],
  results: [
    {
      driver_number: 1,
      position: 1,
      name_acronym: 'VER',
      full_name: 'Max Verstappen',
      team_name: 'Red Bull Racing',
      team_colour: '3671C6',
      dnf: false,
      dns: false,
      dsq: false,
      duration: 5500,
      gap_to_leader: null,
      number_of_laps: 78,
      points: 25,
      session_key: 9472,
      meeting_key: 1229,
    },
  ],
  starting_grid: [
    {
      driver_number: 1,
      position: 1,
      name_acronym: 'VER',
      full_name: 'Max Verstappen',
      team_name: 'Red Bull Racing',
      team_colour: '3671C6',
      session_key: 9472,
      meeting_key: 1229,
      lap_duration: 70.5,
    },
  ],
  stints: [],
  pit_stops: [],
  positions: [],
  race_control: [],
  weather: [
    {
      session_key: 9472,
      meeting_key: 1229,
      date: '2025-05-25T13:30:00+00:00',
      air_temperature: 22,
      track_temperature: 40,
      humidity: 50,
      pressure: 1010,
      rainfall: 0,
      wind_direction: 180,
      wind_speed: 1.2,
    },
  ],
  laps: [
    {
      session_key: 9472,
      driver_number: 1,
      meeting_key: 1229,
      lap_number: 42,
      date_start: '2025-05-25T14:00:00+00:00',
      lap_duration: 71.5,
      is_pit_out_lap: false,
    },
  ],
  chapters: [],
}

const weekend: Weekend = {
  source: 'local',
  meeting_key: 1229,
  meeting,
  default_session_key: 9472,
  sessions: [
    { session: qualSession, source: 'local', datasets: fullDatasets },
    { session: raceSession, source: 'local', datasets: fullDatasets },
  ],
}

const availability: ContextAvailability = {
  source: 'local',
  schedule: 'available',
  live_transport: 'unknown',
  live_session: 'inactive',
  archive: 'unavailable',
  local_analysis: 'complete',
  freshness: 'local',
  limitations: [],
}

const analysisContext: WeekendContext = {
  temporal_state: 'between_weekends',
  default_analysis_session: { session: raceSession, meeting, availability },
  race_hub_default_session: { session: raceSession, meeting, availability },
  race_hub_pre_session: false,
  championship_round: 1,
  total_championship_rounds: 24,
}

function renderRaceHub(sessionKey: number) {
  const queryClient = new QueryClient({
    defaultOptions: { queries: { retry: false } },
  })

  const rootRoute = createRootRoute({
    component: () => (
      <QueryClientProvider client={queryClient}>
        <Outlet />
      </QueryClientProvider>
    ),
  })
  const raceHubRoute = createRoute({
    getParentRoute: () => rootRoute,
    path: '/race-hub',
    validateSearch: (search: Record<string, unknown>) => {
      const sk = Number(search.session_key)
      return Number.isFinite(sk) && sk > 0 ? { session_key: sk } : {}
    },
    component: function RaceHubRouteComponent() {
      const { session_key } = raceHubRoute.useSearch()
      return <RaceHubPage sessionKey={session_key ?? 0} />
    },
  })
  window.history.pushState({}, '', sessionKey ? `/race-hub?session_key=${sessionKey}` : '/race-hub')
  const router = createRouter({
    routeTree: rootRoute.addChildren([raceHubRoute]),
    history: undefined,
  })

  return { queryClient, ...render(<RouterProvider router={router} />) }
}

describe('RaceHubPage', () => {
  beforeEach(() => {
    vi.useRealTimers()
    vi.clearAllMocks()
    mockFetchSeasons.mockResolvedValue([2025])
    mockFetchLocalMeetings.mockResolvedValue([meeting])
    mockFetchWeekend.mockResolvedValue(weekend)
    mockFetchRaceHub.mockResolvedValue(raceHub)
    mockFetchWeekendContext.mockResolvedValue(analysisContext)
  })

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

  it('renders the workspace identity band, session rail, and overview for a known session', async () => {
    renderRaceHub(9472)

    await waitFor(() => expect(screen.getByTestId('race-hub')).toBeInTheDocument())
    expect(screen.getByTestId('rh-identity')).toHaveTextContent('Monaco Grand Prix')
    expect(screen.getByTestId('rh-identity')).toHaveTextContent('MON')

    await waitFor(() =>
      expect(screen.getByTestId('rh-session-9472')).toBeInTheDocument(),
    )
    expect(screen.getByTestId('rh-session-9471')).toBeInTheDocument()

    // Overview is default
    expect(screen.getByTestId('rh-overview')).toBeInTheDocument()
    expect(screen.getByText('Winner')).toBeInTheDocument()
  })

  it('exposes Race Story sub-controls for classification, grid, and positions', async () => {
    renderRaceHub(9472)
    await waitFor(() => expect(screen.getByTestId('race-hub')).toBeInTheDocument())

    fireEvent.click(screen.getByRole('tab', { name: 'Race Story' }))

    expect(screen.getByText('VER')).toBeInTheDocument()
    
  })

  it('keeps Data Status accessible and free of inline CLI guidance', async () => {
    renderRaceHub(9472)
    await waitFor(() => expect(screen.getByTestId('race-hub')).toBeInTheDocument())

    fireEvent.click(screen.getByRole('tab', { name: 'Data Status' }))

    expect(screen.getByTestId('rh-data-status')).toBeInTheDocument()
    expect(screen.queryByText(/ingest-session/i)).not.toBeInTheDocument()
  })

  it('toggles the inline weekend switcher', async () => {
    renderRaceHub(9472)
    await waitFor(() => expect(screen.getByTestId('race-hub')).toBeInTheDocument())

    fireEvent.click(screen.getByTestId('rh-switch-weekend'))
    expect(await screen.findByTestId('rh-switcher')).toBeInTheDocument()
  })

  it('uses the server-selected completed analysis session for bare Race Hub without changing the URL', async () => {
    renderRaceHub(0)

    await waitFor(() => expect(mockFetchRaceHub).toHaveBeenCalledWith(9472))
    expect(mockFetchWeekendContext).toHaveBeenCalledTimes(1)
  })

  it('renders the intentional pre-session state without analysis cards', async () => {
    mockFetchWeekendContext.mockResolvedValue({
      ...analysisContext,
      race_hub_default_session: {
        session: { ...raceSession, session_key: 9473, session_name: 'Practice 1', session_type: 'Practice', date_start: '2099-05-23T13:00:00Z' },
        meeting,
        availability,
      },
      race_hub_pre_session: true,
      race_hub_refresh_at: '2099-05-23T13:00:00Z',
    })

    renderRaceHub(0)

    expect(await screen.findByTestId('race-hub-pre-session')).toBeInTheDocument()
    expect(screen.getByTestId('rh-pre-session-schedule')).toBeInTheDocument()
    expect(screen.queryByText('Winner')).not.toBeInTheDocument()
    expect(mockFetchRaceHub).not.toHaveBeenCalled()
  })

  it('opens the weekend switcher from pre-session and navigates to the selected explicit session', async () => {
    mockFetchWeekendContext.mockResolvedValue({
      ...analysisContext,
      race_hub_default_session: {
        session: { ...raceSession, session_key: 9473, session_name: 'Practice 1', session_type: 'Practice', date_start: '2099-05-23T13:00:00Z' },
        meeting,
        availability,
      },
      race_hub_pre_session: true,
      race_hub_refresh_at: '2099-05-23T13:00:00Z',
    })

    renderRaceHub(0)

    const switchWeekend = await screen.findByTestId('rh-switch-weekend')
    expect(switchWeekend).toHaveAttribute('aria-expanded', 'false')
    fireEvent.click(switchWeekend)
    expect(await screen.findByTestId('rh-switcher')).toBeInTheDocument()
    expect(switchWeekend).toHaveAttribute('aria-expanded', 'true')

    fireEvent.click(await screen.findByTestId('rh-switcher-session-9471'))
    await waitFor(() => expect(window.location.search).toBe('?session_key=9471'))
  })

  it('shows recovery instead of selecting an empty future session', async () => {
    mockFetchWeekendContext.mockResolvedValue({
      ...analysisContext,
      race_hub_default_session: undefined,
      race_hub_pre_session: false,
    })

    renderRaceHub(0)

    expect(await screen.findByTestId('race-hub-empty')).toHaveTextContent('No completed local analysis yet')
    expect(mockFetchRaceHub).not.toHaveBeenCalled()
  })

  it('hands a bare route from completed analysis to pre-session at the supplied refresh boundary', async () => {
    vi.useFakeTimers({ shouldAdvanceTime: true })
    const handoff = new Date(Date.now() + 10_000).toISOString()
    const pendingContext: WeekendContext = {
      ...analysisContext,
      race_hub_default_session: {
        session: { ...raceSession, session_key: 9473, session_name: 'Practice 1', session_type: 'Practice', date_start: handoff },
        meeting,
        availability,
      },
      race_hub_pre_session: true,
      race_hub_refresh_at: new Date(Date.now() + 16_000).toISOString(),
    }
    mockFetchWeekendContext
      .mockResolvedValueOnce({ ...analysisContext, race_hub_refresh_at: handoff })
      .mockResolvedValueOnce(pendingContext)

    renderRaceHub(0)
    await screen.findByTestId('race-hub')
    await act(async () => { await vi.advanceTimersByTimeAsync(10_000) })

    expect(await screen.findByTestId('race-hub-pre-session')).toBeInTheDocument()
    expect(mockFetchWeekendContext).toHaveBeenCalledTimes(2)
    expect(mockFetchRaceHub).toHaveBeenCalledWith(9472)
    expect(mockFetchRaceHub).not.toHaveBeenCalledWith(9473)
  })

  it('re-arms a bare route refresh after a capped browser timer', async () => {
    vi.useFakeTimers({ shouldAdvanceTime: true })
    mockFetchWeekendContext.mockResolvedValue({
      ...analysisContext,
      race_hub_refresh_at: new Date(Date.now() + MAX_BROWSER_TIMEOUT + 1_000).toISOString(),
    })

    renderRaceHub(0)
    await screen.findByTestId('race-hub')
    await act(async () => { await vi.advanceTimersByTimeAsync(MAX_BROWSER_TIMEOUT) })

    await waitFor(() => expect(mockFetchWeekendContext).toHaveBeenCalledTimes(2))
    await act(async () => { await vi.advanceTimersByTimeAsync(1_000) })
    await waitFor(() => expect(mockFetchWeekendContext).toHaveBeenCalledTimes(3))
  })

  it('keeps an explicit session URL stable across the canonical refresh boundary', async () => {
    vi.useFakeTimers()
    renderRaceHub(9472)

    await act(async () => { await vi.advanceTimersByTimeAsync(60_000) })

    expect(mockFetchWeekendContext).not.toHaveBeenCalled()
    expect(mockFetchRaceHub).toHaveBeenCalledWith(9472)
    expect(mockFetchRaceHub).not.toHaveBeenCalledWith(9473)
  })
})
