import { describe, it, expect, vi } from 'vitest'
import { fireEvent, render, screen } from '@testing-library/react'
import { RangeUI } from './range-ui'

describe('<RangeUI>', () => {
  it('renders a two-thumb slider per item', () => {
    render(<RangeUI items={[{ min: 0, max: 100, value: [20, 80] }]} />)
    const sliders = screen.getAllByRole('slider')
    expect(sliders).toHaveLength(2)
    expect(sliders[0]?.getAttribute('aria-valuenow')).toBe('20')
    expect(sliders[1]?.getAttribute('aria-valuenow')).toBe('80')
  })

  it('defaults the value to [min, max] when omitted', () => {
    render(<RangeUI items={[{ min: 10, max: 90 }]} />)
    const sliders = screen.getAllByRole('slider')
    expect(sliders[0]?.getAttribute('aria-valuenow')).toBe('10')
    expect(sliders[1]?.getAttribute('aria-valuenow')).toBe('90')
  })

  it('renders editable min/max text inputs below the slider', () => {
    render(<RangeUI items={[{ min: 0, max: 100, value: [20, 80] }]} />)
    const min = screen.getByLabelText<HTMLInputElement>('Minimum value')
    const max = screen.getByLabelText<HTMLInputElement>('Maximum value')
    expect(min).toBeTruthy()
    expect(max).toBeTruthy()
    // The displayed value passes through the formatter (default toString).
    expect(min.value).toBe('20')
    expect(max.value).toBe('80')
  })

  it('runs the formatter through the text inputs', () => {
    render(
      <RangeUI
        items={[{ min: 0, max: 1000, value: [100, 900] }]}
        formatter={(n) => `$${n}`}
      />,
    )
    const min = screen.getByLabelText<HTMLInputElement>('Minimum value')
    const max = screen.getByLabelText<HTMLInputElement>('Maximum value')
    expect(min.value).toBe('$100')
    expect(max.value).toBe('$900')
  })

  it('renders a caption below the inputs when `note` is provided', () => {
    render(
      <RangeUI
        items={[{ min: 0, max: 100, value: [25, 75], note: 'Filter window' }]}
      />,
    )
    expect(screen.getByText('Filter window')).toBeTruthy()
  })

  it('renders multiple sliders stacked', () => {
    render(
      <RangeUI
        items={[
          { min: 0, max: 100, value: [10, 50], note: 'A' },
          { min: 0, max: 100, value: [25, 75], note: 'B' },
          { min: 0, max: 50, value: [10, 30], note: 'C' },
        ]}
      />,
    )
    expect(screen.getByText('A')).toBeTruthy()
    expect(screen.getByText('B')).toBeTruthy()
    expect(screen.getByText('C')).toBeTruthy()
    // Three two-thumb sliders → 6 thumbs total.
    expect(screen.getAllByRole('slider')).toHaveLength(6)
  })

  it('fires onChange with the item index and a clamped [low, high] tuple', () => {
    const onChange = vi.fn()
    render(
      <RangeUI
        items={[
          { min: 0, max: 100, value: [10, 90] },
          { min: 0, max: 100, value: [20, 80] },
        ]}
        onChange={onChange}
      />,
    )
    // Drag the second item's lower thumb to 42.
    const sliders = screen.getAllByRole('slider')
    fireEvent.change(sliders[2]!, { target: { value: '42' } })
    expect(onChange).toHaveBeenCalled()
    const call = onChange.mock.calls[0] as [number, readonly [number, number]]
    expect(call[0]).toBe(1)
    expect(Array.isArray(call[1])).toBe(true)
    expect(call[1]).toHaveLength(2)
  })

  it('respects per-item disabled on both slider and inputs', () => {
    render(
      <RangeUI
        items={[{ min: 0, max: 100, value: [10, 90], disabled: true }]}
      />,
    )
    const sliders = screen.getAllByRole('slider')
    expect(sliders[0]?.hasAttribute('disabled')).toBe(true)
    expect(sliders[1]?.hasAttribute('disabled')).toBe(true)
    expect(
      screen.getByLabelText('Minimum value').hasAttribute('disabled'),
    ).toBe(true)
    expect(
      screen.getByLabelText('Maximum value').hasAttribute('disabled'),
    ).toBe(true)
  })

  it('commits a typed value on Enter (fires both onChange and onChangeCommitted), clamped to [min, max]', () => {
    // A text-input edit is both a value change and a final commit, so it
    // notifies `onChange` AND `onChangeCommitted` — consumers that only wired
    // `onChange` (the pre-`onChangeCommitted` API) still update.
    const onChange = vi.fn()
    const onChangeCommitted = vi.fn()
    render(
      <RangeUI
        items={[{ min: 0, max: 100, value: [10, 90] }]}
        onChange={onChange}
        onChangeCommitted={onChangeCommitted}
      />,
    )
    const min = screen.getByLabelText('Minimum value')
    // Type 150 — should clamp to max (100), then preserve ordering (low <= high).
    fireEvent.change(min, { target: { value: '150' } })
    fireEvent.keyDown(min, { key: 'Enter' })
    expect(onChangeCommitted).toHaveBeenCalled()
    expect(onChange).toHaveBeenCalled()
    const last = onChangeCommitted.mock.calls.at(-1) as [
      number,
      readonly [number, number],
    ]
    expect(last[0]).toBe(0)
    expect(last[1][0]).toBeLessThanOrEqual(last[1][1])
    expect(last[1][1]).toBeLessThanOrEqual(100)
  })
})
