import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import { formatNumber } from '@carto/ps-utils'
import { LegendItemUI } from './legend-item-ui'
import { LegendRampUI } from '../legend-ramp/legend-ramp-ui'
import { LegendProportionUI } from '../legend-proportion/legend-proportion-ui'
import type {
  LegendCategoryVariable,
  LegendIconVariable,
  LegendProportionVariable,
  LegendRampVariable,
} from '../../stores'

describe('<LegendItemUI> dispatch', () => {
  it('renders the category renderer', () => {
    const data: LegendCategoryVariable = {
      type: 'category',
      items: [{ label: 'A', color: '#000' }],
    }
    render(<LegendItemUI data={data} />)
    expect(screen.getByText('A')).toBeTruthy()
  })

  it('renders the icon renderer', () => {
    const data: LegendIconVariable = {
      type: 'icon',
      items: [{ label: 'Store', icon: 'x.svg' }],
    }
    const { container } = render(<LegendItemUI data={data} />)
    expect(screen.getByText('Store')).toBeTruthy()
    expect(container.querySelector('img')).toBeTruthy()
  })

  it('exposes the orientation on the list (default vertical, opt-in horizontal)', () => {
    const category: LegendCategoryVariable = {
      type: 'category',
      items: [
        { label: 'A', color: '#111' },
        { label: 'B', color: '#222' },
      ],
    }
    const { container, rerender } = render(<LegendItemUI data={category} />)
    expect(
      container.querySelector('[data-orientation="vertical"]'),
    ).toBeTruthy()

    rerender(<LegendItemUI data={{ ...category, orientation: 'horizontal' }} />)
    expect(
      container.querySelector('[data-orientation="horizontal"]'),
    ).toBeTruthy()
    // Both items still render in horizontal mode.
    expect(screen.getByText('A')).toBeTruthy()
    expect(screen.getByText('B')).toBeTruthy()

    // Icon legend honors the same flag.
    const icon: LegendIconVariable = {
      type: 'icon',
      orientation: 'horizontal',
      items: [{ label: 'Store', icon: 'x.svg' }],
    }
    rerender(<LegendItemUI data={icon} />)
    expect(
      container.querySelector('[data-orientation="horizontal"]'),
    ).toBeTruthy()
    expect(screen.getByText('Store')).toBeTruthy()
  })

  it('renders category and icon values verbatim — the consumer pre-formats', () => {
    // A plain pre-formatted string (e.g. a percent) renders as-is.
    const category: LegendCategoryVariable = {
      type: 'category',
      items: [{ label: 'A', color: '#000', value: '12%' }],
    }
    const { rerender } = render(<LegendItemUI data={category} />)
    expect(screen.getByText('12%')).toBeTruthy()

    // An arbitrary node is allowed too.
    rerender(
      <LegendItemUI
        data={{
          type: 'category',
          items: [{ label: 'A', color: '#000', value: <strong>1.2K</strong> }],
        }}
      />,
    )
    expect(screen.getByText('1.2K').tagName).toBe('STRONG')

    const icon: LegendIconVariable = {
      type: 'icon',
      items: [{ label: 'Store', icon: 'x.svg', value: '5.6M' }],
    }
    rerender(<LegendItemUI data={icon} />)
    expect(screen.getByText('5.6M')).toBeTruthy()
  })

  it('passes the variable shape + pattern through to the swatch', () => {
    const data: LegendCategoryVariable = {
      type: 'category',
      shape: 'square',
      fillPattern: 'dots',
      items: [
        { label: 'A', color: '#111' },
        { label: 'B', color: '#222' },
      ],
    }
    const { container } = render(<LegendItemUI data={data} />)
    const swatches = container.querySelectorAll('svg')
    expect(swatches).toHaveLength(2)
    expect(swatches[0]!.getAttribute('data-shape')).toBe('square')
    expect(container.querySelector('pattern')).toBeTruthy()
  })

  it('items override the variable-level swatch style (mixed list)', () => {
    const data: LegendCategoryVariable = {
      type: 'category',
      shape: 'square',
      fillPattern: 'solid', // variable default
      items: [
        { label: 'Solid', color: '#111' },
        { label: 'Dotted', color: '#222', fillPattern: 'dots' },
        { label: 'Line', color: '#333', shape: 'line' },
        { label: 'Outline', color: '#444', isStrokeColor: true },
      ],
    }
    const { container } = render(<LegendItemUI data={data} />)
    const swatches = Array.from(container.querySelectorAll('svg'))
    expect(swatches.map((s) => s.getAttribute('data-shape'))).toEqual([
      'square',
      'square',
      'line',
      'square',
    ])
    // Only the dots-override item emits a fill <pattern>.
    expect(container.querySelectorAll('pattern')).toHaveLength(1)
    expect(swatches[1]!.querySelector('pattern')).toBeTruthy()
    // The isStrokeColor-override item strokes instead of fills.
    const outlineRect = swatches[3]!.querySelector('rect')!
    expect(outlineRect.getAttribute('fill')).toBe('none')
    expect(outlineRect.getAttribute('stroke')).toBe('#444')
  })
})

describe('<LegendRampUI> ordering (regression)', () => {
  it('boundary contract: N+1 stops label the lines between N colors, in order', () => {
    const data: LegendRampVariable = {
      type: 'ramp',
      isContinuous: false,
      colors: ['#aaa', '#bbb', '#ccc'],
      stops: [
        { label: 'a', value: 0 },
        { label: 'b', value: 1 },
        { label: 'c', value: 2 },
        { label: 'd', value: 3 },
      ],
    }
    render(<LegendRampUI data={data} />)
    // One step per color; each step's accessible name is its boundary range,
    // in low→high order — colors and labels never reversed.
    const steps = screen.getAllByRole('img')
    expect(steps.map((s) => s.getAttribute('aria-label'))).toEqual([
      'a – b',
      'b – c',
      'c – d',
    ])
    // Every boundary label renders; the outer two clamp inside the bar
    // (no transform / right-aligned) while interior ones stay centered.
    for (const label of ['a', 'b', 'c', 'd']) {
      expect(screen.getByText(label)).toBeTruthy()
    }
    expect(screen.getByText('a').style.transform).toBe('')
    expect(screen.getByText('b').style.transform).toBe('translateX(-50%)')
    expect(screen.getByText('c').style.transform).toBe('translateX(-50%)')
    expect(screen.getByText('d').style.transform).toBe('translateX(-100%)')
    expect(screen.getByText('d').style.left).toBe('100%')
  })

  it('invalid discrete (stops ≠ colors + 1) renders an error, not a bar', () => {
    const data: LegendRampVariable = {
      type: 'ramp',
      isContinuous: false,
      colors: ['#aaa', '#bbb', '#ccc'], // 3 colors → needs 4 stops
      stops: [
        { label: 'low', value: 0 },
        { label: 'mid', value: 1 },
        { label: 'high', value: 2 },
      ],
      attribute: 'income',
    }
    render(<LegendRampUI data={data} />)
    // The incoherent state shows an error message with the expected counts…
    expect(screen.getByText(/needs 4 stops .* but got 3/)).toBeTruthy()
    // …the attribute header stays for context…
    expect(screen.getByText('income')).toBeTruthy()
    // …and no bar/steps render.
    expect(screen.queryAllByRole('img')).toHaveLength(0)
    expect(screen.queryByText('low')).toBeNull()
  })

  it('renders the avg marker at its linear position with a formatted tooltip', () => {
    const data: LegendRampVariable = {
      type: 'ramp',
      isContinuous: true,
      colors: ['#aaa', '#ccc'],
      stops: [
        { label: '0', value: 0 },
        { label: '100', value: 100 },
      ],
      avg: 25,
    }
    render(<LegendRampUI data={data} />)
    // MUI Tooltip exposes its (string) title as the child's accessible name.
    const line = screen.getByLabelText('25')
    expect(line.style.left).toBe('25%')
  })

  it('clamps an out-of-range avg to the bar edge; hides it without a scale', () => {
    const base: LegendRampVariable = {
      type: 'ramp',
      isContinuous: true,
      colors: ['#aaa', '#ccc'],
      stops: [
        { label: '0', value: 0 },
        { label: '100', value: 100 },
      ],
      avg: 500,
    }
    const { rerender } = render(<LegendRampUI data={base} />)
    expect(screen.getByLabelText('500').style.left).toBe('100%')

    // Zero-width range → no scale → no marker.
    rerender(
      <LegendRampUI
        data={{
          ...base,
          stops: [
            { label: 'x', value: 5 },
            { label: 'x', value: 5 },
          ],
        }}
      />,
    )
    expect(screen.queryByLabelText('500')).toBeNull()
  })

  it('renders a continuous gradient bar instead of steps', () => {
    const data: LegendRampVariable = {
      type: 'ramp',
      isContinuous: true,
      colors: ['#aaa', '#ccc'],
      stops: [
        { label: '0', value: 0 },
        { label: '10', value: 10 },
      ],
    }
    render(<LegendRampUI data={data} />)
    // The gradient bar is the only role=img element; no discrete steps.
    const imgs = screen.getAllByRole('img')
    expect(imgs).toHaveLength(1)
    expect(imgs[0]!.getAttribute('aria-label')).toBe('Color gradient')
  })
})

describe('<LegendProportionUI>', () => {
  // The MAX/MIN labels render the prefix and the formatted value as separate
  // text nodes, so match on the element's full textContent.
  const fullText =
    (text: string) =>
    (_content: string, el: Element | null): boolean =>
      el?.textContent === text
  const compact = (value: number): string => formatNumber(value, 'en-US')

  it('labels the breaks with MAX/MIN prefixes via the supplied formatValue', () => {
    const data: LegendProportionVariable = {
      type: 'proportion',
      min: 13_000,
      max: 36_000_000,
      stops: [36_000_000, 310_000, 110_000, 13_000],
      formatValue: compact,
    }
    const { container } = render(<LegendProportionUI data={data} />)
    expect(screen.getByText(fullText('MAX: 36M'))).toBeTruthy()
    expect(screen.getByText(fullText('MIN: 13K'))).toBeTruthy()
    // Middle breaks are bare (single text node).
    expect(screen.getByText('310K')).toBeTruthy()
    expect(screen.getByText('110K')).toBeTruthy()
    // One decorative circle per break; no dashed tick connectors.
    expect(container.querySelectorAll('[aria-hidden="true"]')).toHaveLength(4)
  })

  it('defaults to String(value) when no formatValue is given', () => {
    const data: LegendProportionVariable = {
      type: 'proportion',
      min: 0,
      max: 36_000_000,
      stops: [36_000_000, 13_000],
    }
    render(<LegendProportionUI data={data} />)
    expect(screen.getByText(fullText('MAX: 36000000'))).toBeTruthy()
    expect(screen.getByText(fullText('MIN: 13000'))).toBeTruthy()
  })

  it('clamps `steps` to 2–4 and regenerates the values dynamically', () => {
    const data: LegendProportionVariable = {
      type: 'proportion',
      min: 0,
      max: 100,
      steps: 6, // clamped to 4
    }
    const { container, rerender } = render(<LegendProportionUI data={data} />)
    expect(container.querySelectorAll('[aria-hidden="true"]')).toHaveLength(4)

    rerender(<LegendProportionUI data={{ ...data, steps: 1 }} />) // clamped to 2
    expect(container.querySelectorAll('[aria-hidden="true"]')).toHaveLength(2)
    // Values follow the step count: only the endpoints remain.
    expect(screen.getByText(fullText('MAX: 100'))).toBeTruthy()
    expect(screen.getByText(fullText('MIN: 0'))).toBeTruthy()

    rerender(<LegendProportionUI data={{ ...data, steps: 3 }} />)
    expect(container.querySelectorAll('[aria-hidden="true"]')).toHaveLength(3)
    expect(screen.getByText('50')).toBeTruthy() // dynamic middle break
  })

  it('caps explicit stops at the 4 largest; circles match labels 1:1', () => {
    const data: LegendProportionVariable = {
      type: 'proportion',
      min: 0,
      max: 500,
      stops: [500, 400, 300, 200, 100], // 5 stops → 4 largest kept
      steps: 2, // ignored when stops are provided
    }
    const { container } = render(<LegendProportionUI data={data} />)
    expect(container.querySelectorAll('[aria-hidden="true"]')).toHaveLength(4)
    expect(screen.getByText(fullText('MAX: 500'))).toBeTruthy()
    expect(screen.getByText(fullText('MIN: 200'))).toBeTruthy() // smallest shown
    expect(screen.queryByText('100')).toBeNull() // dropped
  })

  it('falls back to evenly-spaced breaks from min/max when stops omitted', () => {
    const data: LegendProportionVariable = {
      type: 'proportion',
      min: 0,
      max: 1000,
      steps: 3,
      formatValue: compact,
    }
    render(<LegendProportionUI data={data} />)
    expect(screen.getByText(fullText('MAX: 1K'))).toBeTruthy()
    expect(screen.getByText(fullText('MIN: 0'))).toBeTruthy()
  })

  it('renders the "Radius range by" attribute header when set', () => {
    const data: LegendProportionVariable = {
      type: 'proportion',
      min: 1,
      max: 10,
      attribute: 'revenue',
    }
    render(<LegendProportionUI data={data} />)
    expect(screen.getByText('Radius range by')).toBeTruthy()
    expect(screen.getByText('revenue')).toBeTruthy()
  })
})
