import { fireEvent, render, screen, within } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { REAL_TRACE_BANDS, REAL_TRACE_LANES, TRACE_START_MS } from './fixtures/swimlane-trace'
import { SwimlaneTrace } from './SwimlaneTrace'

describe('SwimlaneTrace', () => {
  it('positions segments and the NOW playhead on the shared absolute clock', () => {
    render(
      <SwimlaneTrace
        lanes={REAL_TRACE_LANES}
        nowMs={TRACE_START_MS + 555_000}
        bands={REAL_TRACE_BANDS}
        zoom="fit"
        onZoom={vi.fn()}
        onSegmentClick={vi.fn()}
      />,
    )

    const first = screen.getByTestId('trace-segment-t1-0')
    expect(parseFloat(first.style.left)).toBeCloseTo(0, 4)
    expect(parseFloat(first.style.width)).toBeCloseTo((420_000 / 1_110_000) * 100, 4)
    expect(parseFloat(screen.getByTestId('trace-now-playhead').style.left)).toBeCloseTo(50, 4)
  })

  it('constrains a condition band to its declared lanes', () => {
    render(
      <SwimlaneTrace
        lanes={REAL_TRACE_LANES}
        nowMs={null}
        bands={REAL_TRACE_BANDS}
        zoom="fit"
        onZoom={vi.fn()}
        onSegmentClick={vi.fn()}
      />,
    )

    expect(within(screen.getByTestId('trace-lane-t1')).getByText('codex rate limited')).toBeInTheDocument()
    expect(within(screen.getByTestId('trace-lane-t5')).queryByText('codex rate limited')).not.toBeInTheDocument()
    expect(screen.queryByTestId('trace-now-playhead')).not.toBeInTheDocument()
  })

  it('reports zoom and segment interactions', () => {
    const onZoom = vi.fn()
    const onSegmentClick = vi.fn()
    render(
      <SwimlaneTrace
        lanes={REAL_TRACE_LANES}
        nowMs={null}
        bands={[]}
        zoom="fit"
        onZoom={onZoom}
        onSegmentClick={onSegmentClick}
      />,
    )

    fireEvent.click(screen.getByRole('button', { name: 'Last hour' }))
    fireEvent.click(screen.getByTestId('trace-segment-t1-0'))
    expect(onZoom).toHaveBeenCalledWith('hour')
    expect(onSegmentClick).toHaveBeenCalledWith('t1', 0)
  })

  it('uses the canonical status category to color status-bearing segments', () => {
    render(
      <SwimlaneTrace
        lanes={[{ taskId: 'failed-phase', name: 'Gate', wave: 1, segments: [{ t0: 10, durMs: 20, cat: 'fail', status: 'fail' }] }]}
        nowMs={null}
        bands={[]}
        zoom="fit"
        onZoom={vi.fn()}
        onSegmentClick={vi.fn()}
      />,
    )

    expect(screen.getByTestId('trace-segment-failed-phase-0')).toHaveClass('bg-danger')
  })

  it('renders a time axis beneath the plot column with wall-clock labels', () => {
    render(
      <SwimlaneTrace
        lanes={REAL_TRACE_LANES}
        nowMs={null}
        bands={[]}
        zoom="fit"
        onZoom={vi.fn()}
        onSegmentClick={vi.fn()}
      />,
    )

    const axis = screen.getByTestId('trace-time-axis')
    const lanePlot = within(screen.getByTestId('trace-lane-t1')).getByText('Negative DB guards').parentElement
      ?.nextElementSibling
    expect(axis).toHaveClass('border-l')
    expect(lanePlot).toHaveClass('border-l')
    expect(axis.closest('.grid')?.className).toContain('grid-cols-[180px_minmax(0,1fr)]')
    expect(screen.getByText('Time (UTC)')).toHaveAttribute('id')
    expect(screen.getByRole('group', { name: 'Time (UTC)' })).toHaveAttribute(
      'aria-describedby',
      expect.stringMatching(/-range$/),
    )
    expect(screen.getByText(/UTC wall-clock range from .+ to .+/)).toBeInTheDocument()
    const ticks = within(axis).getAllByTestId('trace-time-axis-tick')
    expect(ticks.length).toBeGreaterThan(1)
    expect(ticks[0]).toHaveAttribute('data-epoch-ms', String(TRACE_START_MS))
  })

  it('keeps the NOW playhead scoped to lane rows, not the time axis', () => {
    render(
      <SwimlaneTrace
        lanes={REAL_TRACE_LANES}
        nowMs={TRACE_START_MS + 555_000}
        bands={[]}
        zoom="fit"
        onZoom={vi.fn()}
        onSegmentClick={vi.fn()}
      />,
    )

    const lanesPlot = screen.getByTestId('trace-lanes-plot')
    const playhead = screen.getByTestId('trace-now-playhead')
    const axis = screen.getByTestId('trace-time-axis')

    expect(lanesPlot).toContainElement(playhead)
    expect(lanesPlot).not.toContainElement(axis)
    expect(lanesPlot.nextElementSibling).toContainElement(axis)
  })

  it('labels short spans with second precision', () => {
    const start = Date.UTC(2026, 7, 9, 12, 0, 0)
    render(
      <SwimlaneTrace
        lanes={[{ taskId: 'short', name: 'Burst', wave: 1, segments: [{ t0: start, durMs: 45_000, cat: 'gate0' }] }]}
        nowMs={null}
        bands={[]}
        zoom="fit"
        onZoom={vi.fn()}
        onSegmentClick={vi.fn()}
      />,
    )

    const labels = within(screen.getByTestId('trace-time-axis'))
      .getAllByTestId('trace-time-axis-tick')
      .map((tick) => tick.textContent)
    expect(labels.some((label) => /:\d{2}:\d{2}/.test(label ?? ''))).toBe(true)
  })

  it('labels multi-day spans with calendar context', () => {
    const start = Date.UTC(2026, 7, 1, 8, 0, 0)
    render(
      <SwimlaneTrace
        lanes={[
          {
            taskId: 'long',
            name: 'Multi-day',
            wave: 1,
            segments: [{ t0: start, durMs: 3 * 86_400_000, cat: 'llm-implement' }],
          },
        ]}
        nowMs={null}
        bands={[]}
        zoom="fit"
        onZoom={vi.fn()}
        onSegmentClick={vi.fn()}
      />,
    )

    const labels = within(screen.getByTestId('trace-time-axis'))
      .getAllByTestId('trace-time-axis-tick')
      .map((tick) => tick.textContent)
    expect(labels.some((label) => /Aug/.test(label ?? ''))).toBe(true)
  })

  it('does not render a time axis when lanes have no temporal data', () => {
    render(
      <SwimlaneTrace
        lanes={[
          { taskId: 'empty', name: 'Waiting', wave: 1, segments: [] },
          { taskId: 'also-empty', name: 'Queued', wave: 2, segments: [] },
        ]}
        nowMs={null}
        bands={[]}
        zoom="fit"
        onZoom={vi.fn()}
        onSegmentClick={vi.fn()}
      />,
    )

    expect(screen.getByTestId('trace-lane-empty')).toBeInTheDocument()
    expect(screen.getByTestId('trace-lane-also-empty')).toBeInTheDocument()
    expect(screen.queryByTestId('trace-time-axis')).not.toBeInTheDocument()
    expect(screen.queryByText('Time (UTC)')).not.toBeInTheDocument()
  })

  it('shares one horizontal overflow container across lanes and the axis', () => {
    render(
      <SwimlaneTrace
        lanes={REAL_TRACE_LANES}
        nowMs={null}
        bands={[]}
        zoom="fit"
        onZoom={vi.fn()}
        onSegmentClick={vi.fn()}
      />,
    )

    const scrollContainer = screen.getByTestId('trace-scroll-container')
    const lanesPlot = screen.getByTestId('trace-lanes-plot')
    const axis = screen.getByTestId('trace-time-axis')

    expect(scrollContainer).toHaveClass('overflow-x-auto')
    expect(scrollContainer).toContainElement(lanesPlot)
    expect(scrollContainer).toContainElement(axis)
    expect(scrollContainer.firstElementChild).toHaveClass('min-w-4xl')
  })

  it('exposes ISO UTC tooltips on axis ticks', () => {
    const start = Date.UTC(2026, 7, 9, 12, 0, 0)
    render(
      <SwimlaneTrace
        lanes={[{ taskId: 'short', name: 'Burst', wave: 1, segments: [{ t0: start, durMs: 45_000, cat: 'gate0' }] }]}
        nowMs={null}
        bands={[]}
        zoom="fit"
        onZoom={vi.fn()}
        onSegmentClick={vi.fn()}
      />,
    )

    const firstTick = within(screen.getByTestId('trace-time-axis')).getAllByTestId('trace-time-axis-tick')[0]!
    expect(firstTick).toHaveAttribute('data-tipb', new Date(start).toISOString())
  })

  it('exposes exact ISO UTC timestamps in the screen-reader range', () => {
    const start = Date.UTC(2026, 7, 9, 12, 0, 0)
    const end = start + 45_000
    render(
      <SwimlaneTrace
        lanes={[{ taskId: 'short', name: 'Burst', wave: 1, segments: [{ t0: start, durMs: 45_000, cat: 'gate0' }] }]}
        nowMs={null}
        bands={[]}
        zoom="fit"
        onZoom={vi.fn()}
        onSegmentClick={vi.fn()}
      />,
    )

    const range = screen.getByText(/UTC wall-clock range from/)
    expect(range.textContent).toContain(new Date(start).toISOString())
    expect(range.textContent).toContain(new Date(end).toISOString())
    const ticks = within(screen.getByTestId('trace-time-axis')).getAllByTestId('trace-time-axis-tick')
    expect(range.textContent).toContain(ticks[0]?.textContent?.trim())
    expect(range.textContent).toContain(ticks.at(-1)?.textContent?.trim())
  })

  it('anchors axis endpoint epochs to fit bounds for a completed trace', () => {
    render(
      <SwimlaneTrace
        lanes={REAL_TRACE_LANES}
        nowMs={null}
        bands={[]}
        zoom="fit"
        onZoom={vi.fn()}
        onSegmentClick={vi.fn()}
      />,
    )

    const ticks = within(screen.getByTestId('trace-time-axis')).getAllByTestId('trace-time-axis-tick')
    expect(ticks[0]).toHaveAttribute('data-epoch-ms', String(TRACE_START_MS))
    expect(ticks.at(-1)).toHaveAttribute('data-epoch-ms', String(TRACE_START_MS + 1_110_000))
  })

  it('anchors axis endpoint epochs to the last-hour window', () => {
    const nowMs = TRACE_START_MS + 555_000
    render(
      <SwimlaneTrace
        lanes={REAL_TRACE_LANES}
        nowMs={nowMs}
        bands={[]}
        zoom="hour"
        onZoom={vi.fn()}
        onSegmentClick={vi.fn()}
      />,
    )

    const ticks = within(screen.getByTestId('trace-time-axis')).getAllByTestId('trace-time-axis-tick')
    expect(ticks[0]).toHaveAttribute('data-epoch-ms', String(nowMs - 3_600_000))
    expect(ticks.at(-1)).toHaveAttribute('data-epoch-ms', String(nowMs))
  })

  it('anchors axis endpoint epochs to the live window', () => {
    const nowMs = TRACE_START_MS + 555_000
    render(
      <SwimlaneTrace
        lanes={REAL_TRACE_LANES}
        nowMs={nowMs}
        bands={[]}
        zoom="live"
        onZoom={vi.fn()}
        onSegmentClick={vi.fn()}
      />,
    )

    const ticks = within(screen.getByTestId('trace-time-axis')).getAllByTestId('trace-time-axis-tick')
    expect(ticks[0]).toHaveAttribute('data-epoch-ms', String(nowMs - 900_000))
    expect(ticks.at(-1)).toHaveAttribute('data-epoch-ms', String(nowMs))
  })

  it('updates axis endpoint epochs when zoom changes on a running trace', () => {
    const nowMs = TRACE_START_MS + 555_000
    const { rerender } = render(
      <SwimlaneTrace
        lanes={REAL_TRACE_LANES}
        nowMs={nowMs}
        bands={[]}
        zoom="fit"
        onZoom={vi.fn()}
        onSegmentClick={vi.fn()}
      />,
    )

    const axisTicks = () =>
      within(screen.getByTestId('trace-time-axis')).getAllByTestId('trace-time-axis-tick')

    expect(axisTicks()[0]).toHaveAttribute('data-epoch-ms', String(TRACE_START_MS))
    expect(axisTicks().at(-1)).toHaveAttribute('data-epoch-ms', String(TRACE_START_MS + 1_110_000))

    rerender(
      <SwimlaneTrace
        lanes={REAL_TRACE_LANES}
        nowMs={nowMs}
        bands={[]}
        zoom="hour"
        onZoom={vi.fn()}
        onSegmentClick={vi.fn()}
      />,
    )
    expect(axisTicks()[0]).toHaveAttribute('data-epoch-ms', String(nowMs - 3_600_000))
    expect(axisTicks().at(-1)).toHaveAttribute('data-epoch-ms', String(nowMs))

    rerender(
      <SwimlaneTrace
        lanes={REAL_TRACE_LANES}
        nowMs={nowMs}
        bands={[]}
        zoom="live"
        onZoom={vi.fn()}
        onSegmentClick={vi.fn()}
      />,
    )
    expect(axisTicks()[0]).toHaveAttribute('data-epoch-ms', String(nowMs - 900_000))
    expect(axisTicks().at(-1)).toHaveAttribute('data-epoch-ms', String(nowMs))
  })

  it('anchors the NOW label inward at the end bound and on the start half of the plot', () => {
    const nowMs = TRACE_START_MS + 555_000

    const { rerender } = render(
      <SwimlaneTrace
        lanes={REAL_TRACE_LANES}
        nowMs={nowMs}
        bands={[]}
        zoom="hour"
        onZoom={vi.fn()}
        onSegmentClick={vi.fn()}
      />,
    )

    const endBoundPlayhead = screen.getByTestId('trace-now-playhead')
    const endBoundLabel = screen.getByTestId('trace-now-label')
    expect(parseFloat(endBoundPlayhead.style.left)).toBeCloseTo(100, 4)
    expect(endBoundLabel).toHaveClass('right-1')
    expect(endBoundLabel).not.toHaveClass('left-1')

    rerender(
      <SwimlaneTrace
        lanes={REAL_TRACE_LANES}
        nowMs={nowMs}
        bands={[]}
        zoom="live"
        onZoom={vi.fn()}
        onSegmentClick={vi.fn()}
      />,
    )
    expect(parseFloat(screen.getByTestId('trace-now-playhead').style.left)).toBeCloseTo(100, 4)
    expect(screen.getByTestId('trace-now-label')).toHaveClass('right-1')

    rerender(
      <SwimlaneTrace
        lanes={REAL_TRACE_LANES}
        nowMs={TRACE_START_MS + 100_000}
        bands={[]}
        zoom="fit"
        onZoom={vi.fn()}
        onSegmentClick={vi.fn()}
      />,
    )

    const startHalfPlayhead = screen.getByTestId('trace-now-playhead')
    const startHalfLabel = screen.getByTestId('trace-now-label')
    expect(parseFloat(startHalfPlayhead.style.left)).toBeLessThan(50)
    expect(startHalfLabel).toHaveClass('left-1')
    expect(startHalfLabel).not.toHaveClass('right-1')
  })

  it('does not render the NOW playhead when nowMs is before the displayed interval', () => {
    render(
      <SwimlaneTrace
        lanes={REAL_TRACE_LANES}
        nowMs={TRACE_START_MS - 60_000}
        bands={[]}
        zoom="fit"
        onZoom={vi.fn()}
        onSegmentClick={vi.fn()}
      />,
    )

    expect(screen.queryByTestId('trace-now-playhead')).not.toBeInTheDocument()
  })

  it('does not render the NOW playhead when nowMs is after the displayed interval', () => {
    render(
      <SwimlaneTrace
        lanes={REAL_TRACE_LANES}
        nowMs={TRACE_START_MS + 2_000_000}
        bands={[]}
        zoom="fit"
        onZoom={vi.fn()}
        onSegmentClick={vi.fn()}
      />,
    )

    expect(screen.queryByTestId('trace-now-playhead')).not.toBeInTheDocument()
  })

  it('disables Follow live and coerces zoom=live to fit on a completed trace', () => {
    render(
      <SwimlaneTrace
        lanes={REAL_TRACE_LANES}
        nowMs={null}
        bands={[]}
        zoom="live"
        onZoom={vi.fn()}
        onSegmentClick={vi.fn()}
      />,
    )

    expect(screen.getByRole('button', { name: 'Follow live' })).toBeDisabled()
    expect(screen.getByRole('button', { name: 'Fit run' })).toHaveAttribute('aria-pressed', 'true')
    expect(screen.getByRole('button', { name: 'Follow live' })).toHaveAttribute('aria-pressed', 'false')
    const ticks = within(screen.getByTestId('trace-time-axis')).getAllByTestId('trace-time-axis-tick')
    expect(ticks[0]).toHaveAttribute('data-epoch-ms', String(TRACE_START_MS))
    expect(ticks.at(-1)).toHaveAttribute('data-epoch-ms', String(TRACE_START_MS + 1_110_000))
  })
})
