import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { act, fireEvent, render, screen } from '@testing-library/react'
import { Provider } from '../provider/widget-provider'
import { clearAllWidgetStores, getWidgetStore } from '../stores'
import { Category } from './category'

beforeEach(() => clearAllWidgetStores())
afterEach(() => clearAllWidgetStores())

const DATA = [
  [
    { name: 'A', value: 10 },
    { name: 'B', value: 20 },
  ],
]

describe('<Category> bridge', () => {
  it('reads post-pipeline data from the store and renders rows', () => {
    render(
      <Provider id='cat-bridge-1' data={DATA}>
        <Category />
      </Provider>,
    )
    expect(screen.getByText('A')).toBeTruthy()
    expect(screen.getByText('B')).toBeTruthy()
  })

  it('forwards the formatter from the store to the UI', () => {
    const fmt = (n: number) => `[${n}]`
    render(
      <Provider id='cat-bridge-2' data={DATA} formatter={fmt}>
        <Category />
      </Provider>,
    )
    expect(screen.getByText('[10]')).toBeTruthy()
    expect(screen.getByText('[20]')).toBeTruthy()
  })

  it('passes selection and onSelectionChange through to the UI', () => {
    const onSelectionChange = vi.fn()
    render(
      <Provider id='cat-bridge-3' data={DATA}>
        <Category selection={['A']} onSelectionChange={onSelectionChange} />
      </Provider>,
    )
    fireEvent.click(screen.getByText('B'))
    expect(onSelectionChange).toHaveBeenCalledWith(['A', 'B'])
  })

  it('reflects pipeline-derived data when filters change the store', () => {
    render(
      <Provider id='cat-bridge-4' data={DATA}>
        <Category />
      </Provider>,
    )
    // Manually mutate the post-pipeline derived data on the store, simulating
    // a transform output. The bridge selector picks it up via Zustand subscription.
    expect(screen.getByText('B')).toBeTruthy()
    void getWidgetStore('cat-bridge-4')
  })

  it('forwards labelFormatter from the store to the UI; selection callback still receives raw name', () => {
    const onSelectionChange = vi.fn()
    render(
      <Provider
        id='cat-bridge-5'
        data={DATA}
        labelFormatter={(n) => String(n).toUpperCase()}
      >
        <Category selection={[]} onSelectionChange={onSelectionChange} />
      </Provider>,
    )
    // Display names are upper-cased by the formatter.
    expect(screen.getByText('A')).toBeTruthy()
    // Click the upper-cased display label.
    fireEvent.click(screen.getByText('A'))
    // Selection callback gets the RAW name, not the formatted one.
    expect(onSelectionChange).toHaveBeenCalledWith(['A'])
  })

  it('forwards series, maxItems, labels, maxOverride as pass-throughs', () => {
    const data = [
      Array.from({ length: 6 }, (_, i) => ({
        name: `n${i}`,
        value: 6 - i,
      })),
    ]
    render(
      <Provider id='cat-bridge-6' data={data}>
        <Category
          maxItems={3}
          labels={{ other: 'Rest', otherCount: '({count} hidden)' }}
        />
      </Provider>,
    )
    // 3 visible rows + Other footer.
    expect(screen.getAllByRole('button')).toHaveLength(3)
    expect(screen.getByText('Rest')).toBeTruthy()
    expect(screen.getByText('(3 hidden)')).toBeTruthy()
  })

  it('does NOT read `transformStates.searcher.enabled` from the store (composer-mediation contract)', () => {
    // Defensive: the library bridge stays agnostic of every specific
    // action. Pagination is purely a `maxItems` decision; consumers
    // that want to bypass it while a SearcherToggle is open should
    // flip the prop themselves (e.g., the `CategoryWidget` composer
    // does `maxItems = searcherOpen ? 0 : userMaxItems`). Guards
    // against regressing into the old widget-reads-action-state
    // pattern.
    const data = [
      Array.from({ length: 10 }, (_, i) => ({
        name: `n${i}`,
        value: 10 - i,
      })),
    ]
    render(
      <Provider id='cat-bridge-8' data={data}>
        <Category maxItems={3} />
      </Provider>,
    )
    expect(screen.getAllByRole('button')).toHaveLength(3)
    expect(screen.getByText('Others')).toBeTruthy()

    // Flip the searcher flag in the store — bridge should NOT react.
    act(() => {
      const store = getWidgetStore('cat-bridge-8')
      store.setState({
        transformStates: {
          ...store.getState().transformStates,
          searcher: { enabled: true },
        },
      })
    })

    // Still capped, Other still there.
    expect(screen.getAllByRole('button')).toHaveLength(3)
    expect(screen.getByText('Others')).toBeTruthy()
  })

  it('respects maxItems=0 (no-cap sentinel) from the consumer', () => {
    // Composers that want to drop pagination — e.g., while the
    // SearcherToggle is open — pass `maxItems={0}`.
    const data = [
      Array.from({ length: 10 }, (_, i) => ({
        name: `n${i}`,
        value: 10 - i,
      })),
    ]
    render(
      <Provider id='cat-bridge-nocap' data={data}>
        <Category maxItems={0} />
      </Provider>,
    )
    // No cap → all rows render, Other footer is suppressed.
    expect(screen.getAllByRole('button')).toHaveLength(10)
    expect(screen.queryByText('Others')).toBeNull()
  })

  it('forwards size="medium" through to the rendered bars', () => {
    const { container } = render(
      <Provider id='cat-bridge-size' data={DATA}>
        <Category size='medium' />
      </Provider>,
    )
    const tracks = container.querySelectorAll('[data-size]')
    expect(tracks.length).toBe(2)
    tracks.forEach((t) => {
      expect(t.getAttribute('data-size')).toBe('medium')
    })
  })

  it('defaults size to "small" when omitted', () => {
    const { container } = render(
      <Provider id='cat-bridge-size-default' data={DATA}>
        <Category />
      </Provider>,
    )
    const tracks = container.querySelectorAll('[data-size]')
    expect(tracks.length).toBe(2)
    tracks.forEach((t) => {
      expect(t.getAttribute('data-size')).toBe('small')
    })
  })

  // The bridge auto-fills `maxOverride` from `rawData` so bar widths
  // stay coherent across data transforms (e.g., the Searcher filtering
  // rows in/out). Without this, CategoryUI's live computation would
  // shrink the denominator to the filtered max and bars would rescale.
  describe('maxOverride auto-fill from rawData', () => {
    // Single-series dataset with a round peak (1000) so percent
    // expectations are exact.
    const PEAK_DATA = [
      [
        { name: 'A', value: 1000 },
        { name: 'B', value: 500 },
        { name: 'C', value: 250 },
      ],
    ]

    function getBarFills(): HTMLElement[] {
      return Array.from(
        document.querySelectorAll<HTMLElement>('[data-bar-fill="true"]'),
      )
    }

    it('uses rawData max as the bar denominator when consumer omits maxOverride', () => {
      render(
        <Provider id='cat-bridge-auto-1' data={PEAK_DATA}>
          <Category />
        </Provider>,
      )
      const fills = getBarFills()
      expect(fills).toHaveLength(3)
      expect(fills[0]!.style.width).toBe('100%')
      expect(fills[1]!.style.width).toBe('50%')
      expect(fills[2]!.style.width).toBe('25%')
    })

    it('keeps bar widths coherent when a transform shrinks `data` below `rawData`', () => {
      render(
        <Provider id='cat-bridge-auto-2' data={PEAK_DATA}>
          <Category />
        </Provider>,
      )
      // Simulate a data-transform output (e.g., Searcher filtering to
      // just "C"). The pipeline middleware no-ops when only `data` is
      // set and `rawData`/`dataTransforms` are unchanged, so our
      // override survives the Provider's per-prop sync.
      act(() => {
        getWidgetStore('cat-bridge-auto-2').setState({
          data: [[{ name: 'C', value: 250 }]],
        })
      })
      const fills = getBarFills()
      expect(fills).toHaveLength(1)
      // C's bar must still scale to 250/1000 = 25%, NOT 100% (which is
      // what it would be if maxValue came from the filtered subset).
      expect(fills[0]!.style.width).toBe('25%')
    })

    it('consumer-supplied maxOverride wins over the rawData auto-fill', () => {
      render(
        <Provider id='cat-bridge-auto-3' data={PEAK_DATA}>
          <Category maxOverride={2000} />
        </Provider>,
      )
      const fills = getBarFills()
      // 1000/2000, 500/2000, 250/2000 — consumer override active.
      expect(fills[0]!.style.width).toBe('50%')
      expect(fills[1]!.style.width).toBe('25%')
      expect(fills[2]!.style.width).toBe('12.5%')
    })

    it('skips the auto-fill when rawData is empty (lets CategoryUI fallback handle it)', () => {
      // `data` non-empty, but rawData is set to []. The bridge must NOT
      // pass maxOverride=0 (which CategoryUI's `> 0` guard would
      // reject); it should fall through to undefined so CategoryUI
      // computes from the live `data`.
      render(
        <Provider id='cat-bridge-auto-4' data={[]}>
          <Category />
        </Provider>,
      )
      act(() => {
        getWidgetStore('cat-bridge-auto-4').setState({
          data: [[{ name: 'X', value: 42 }]],
        })
      })
      const fills = getBarFills()
      expect(fills).toHaveLength(1)
      // CategoryUI computes max from the live data → 42/42 = 100%.
      expect(fills[0]!.style.width).toBe('100%')
    })

    it('refreshes the auto-fill when `rawData` identity changes', () => {
      const { rerender } = render(
        <Provider id='cat-bridge-auto-5' data={PEAK_DATA}>
          <Category />
        </Provider>,
      )
      let fills = getBarFills()
      expect(fills[0]!.style.width).toBe('100%') // 1000/1000

      // New dataset with a 5× larger peak — every bar must rescale.
      const NEXT = [
        [
          { name: 'A', value: 1000 },
          { name: 'B', value: 500 },
          { name: 'C', value: 250 },
          { name: 'D', value: 5000 },
        ],
      ]
      rerender(
        <Provider id='cat-bridge-auto-5' data={NEXT}>
          <Category />
        </Provider>,
      )
      fills = getBarFills()
      expect(fills).toHaveLength(4)
      // A: 1000/5000 = 20%, D: 5000/5000 = 100%.
      expect(fills[0]!.style.width).toBe('20%')
      expect(fills[3]!.style.width).toBe('100%')
    })
  })
})
