import { afterEach, describe, it, expect, vi } from 'vitest'
import { fireEvent, render, screen } from '@testing-library/react'
import { LegendProvider } from '../../provider'
import { LegendGroup } from './legend-group'
import { LegendRow } from '../legend-row/legend-row'
import { LegendActions } from '../legend-actions/legend-actions'
import { LegendVisibilityToggle } from '../legend-visibility-toggle/legend-visibility-toggle'
import { LegendGroupMenu } from '../legend-group-menu/legend-group-menu'
import {
  LegendShowOnlyGroup,
  LegendShowAllGroups,
  LegendCollapseAllGroups,
  LegendZoomToGroup,
} from '../legend-group-menu/legend-group-menu-items'
import { LegendShowAllLayers } from '../legend-row-menu/legend-row-menu-items'
import {
  clearAllLegendStores,
  getLegendStore,
  type LegendGroupInput,
  type LegendLayerInput,
  type LegendVariable,
} from '../../stores'

const cat: LegendVariable = {
  type: 'category',
  items: [{ label: 'A', color: '#123456' }],
}

const groups: LegendGroupInput[] = [{ id: 'g1', label: 'Demographics' }]
const layers: LegendLayerInput[] = [
  { id: 'a', name: 'Alpha', variables: [cat], groupId: 'g1', order: 1 },
  { id: 'b', name: 'Beta', variables: [cat], groupId: 'g1', order: 0 },
]

afterEach(() => clearAllLegendStores())

/** Module-shaped composition: group eye in the header, explicit member rows. */
function renderGroup(id: string) {
  return render(
    <LegendProvider id={id} layers={layers} groups={groups}>
      <LegendGroup groupId='g1'>
        <LegendActions>
          <LegendVisibilityToggle />
        </LegendActions>
        {['b', 'a'].map((layerId) => (
          <LegendRow key={layerId} layerId={layerId}>
            <LegendActions>
              <LegendVisibilityToggle />
            </LegendActions>
          </LegendRow>
        ))}
      </LegendGroup>
    </LegendProvider>,
  )
}

describe('<LegendGroup> composition', () => {
  it('renders the label from the store, Actions in the header, rows in the body', () => {
    const { container } = renderGroup('g-comp')
    expect(screen.getByText('Demographics')).toBeTruthy()
    // The group eye landed in the group fade slot of the header…
    const fade = container.querySelector('.PsLegend-groupFade')!
    expect(fade.contains(screen.getByLabelText('Hide group'))).toBe(true)
    // …and both member rows render in the body.
    expect(screen.getByText('Alpha')).toBeTruthy()
    expect(screen.getByText('Beta')).toBeTruthy()
  })

  it('the group eye cascades visibility to every member layer', () => {
    renderGroup('g-vis')
    fireEvent.click(screen.getByLabelText('Hide group'))
    let state = getLegendStore('g-vis').getState()
    expect(state.layers.a!.visible).toBe(false)
    expect(state.layers.b!.visible).toBe(false)
    fireEvent.click(screen.getByLabelText('Show group'))
    state = getLegendStore('g-vis').getState()
    expect(state.layers.a!.visible).toBe(true)
    expect(state.layers.b!.visible).toBe(true)
  })

  it('clicking the title row toggles group collapse (accordion header)', () => {
    renderGroup('g-col')
    const header = screen.getByRole('button', { name: 'Demographics' })
    expect(header.getAttribute('aria-expanded')).toBe('true')
    fireEvent.click(header)
    expect(getLegendStore('g-col').getState().groups.g1!.collapsed).toBe(true)
    expect(header.getAttribute('aria-expanded')).toBe('false')
    // Keyboard parity: Enter re-expands.
    fireEvent.keyDown(header, { key: 'Enter' })
    expect(getLegendStore('g-col').getState().groups.g1!.collapsed).toBe(false)
  })

  it('shows the indeterminate icon (mixed) when only some layers are hidden', () => {
    renderGroup('g-mixed')
    // Hide one member row via its own (row-context) eye.
    fireEvent.click(screen.getAllByLabelText('Hide layer')[0]!)
    // Mixed: the group eye still offers "Hide group", is pinned active, and
    // renders the indeterminate glyph.
    const eye = screen.getByLabelText('Hide group')
    expect(eye.className).toContain('active')
    expect(
      eye.querySelector('[data-testid="VisibilityIndeterminateIcon"]'),
    ).toBeTruthy()
    // Clicking on mixed hides the remaining visible member.
    fireEvent.click(eye)
    const state = getLegendStore('g-mixed').getState()
    expect(state.layers.a!.visible).toBe(false)
    expect(state.layers.b!.visible).toBe(false)
    expect(screen.getByLabelText('Show group')).toBeTruthy()
  })

  it('marks the visibility toggle active while the group is hidden', () => {
    renderGroup('g-active')
    fireEvent.click(screen.getByLabelText('Hide group'))
    expect(screen.getByLabelText('Show group').className).toContain('active')
  })

  it('hiding the group collapses it, but the title row stays usable', () => {
    renderGroup('g-hide')
    fireEvent.click(screen.getByLabelText('Hide group'))
    expect(getLegendStore('g-hide').getState().groups.g1!.collapsed).toBe(true)
    // Groups are never force-collapsed by visibility — a hidden group can be
    // peeked into to re-show layers one by one.
    const header = screen.getByRole('button', { name: 'Demographics' })
    fireEvent.click(header)
    expect(getLegendStore('g-hide').getState().groups.g1!.collapsed).toBe(false)
  })

  it('renders no icon when the group has no icon configured', () => {
    renderGroup('g-icon-none')
    expect(screen.queryByTestId('LayersOutlinedIcon')).toBeNull()
    expect(screen.queryByTestId('my-icon')).toBeNull()
  })

  it('renders the custom icon when provided', () => {
    render(
      <LegendProvider
        id='g-icon-custom'
        layers={layers}
        groups={[
          {
            id: 'g1',
            label: 'Demographics',
            icon: <span data-testid='my-icon' />,
          },
        ]}
      >
        <LegendGroup groupId='g1' />
      </LegendProvider>,
    )
    expect(screen.getByTestId('my-icon')).toBeTruthy()
  })
})

describe('<LegendGroupMenu> + built-in items', () => {
  function renderWithMenu(id: string) {
    return render(
      <LegendProvider
        id={id}
        layers={[...layers, { id: 'solo', name: 'Solo', variables: [cat] }]}
        groups={[...groups, { id: 'g2', label: 'Business' }]}
      >
        <LegendGroup groupId='g1'>
          <LegendActions>
            <LegendVisibilityToggle />
            <LegendGroupMenu>
              <LegendShowOnlyGroup />
              <LegendShowAllGroups />
              <LegendCollapseAllGroups />
              <LegendZoomToGroup onZoomTo={zoomSpy} />
            </LegendGroupMenu>
          </LegendActions>
        </LegendGroup>
      </LegendProvider>,
    )
  }
  const zoomSpy = vi.fn()

  it('Show only this group hides other groups and ungrouped layers', () => {
    renderWithMenu('gm-only')
    fireEvent.click(screen.getByLabelText('Group options'))
    fireEvent.click(screen.getByText('Show only this group'))
    const state = getLegendStore('gm-only').getState()
    expect(state.layers.a!.visible).toBe(true)
    expect(state.layers.solo!.visible).toBe(false)
    // Menu closed after the pick.
    expect(screen.queryByRole('menuitem')).toBeNull()

    fireEvent.click(screen.getByLabelText('Group options'))
    fireEvent.click(screen.getByText('Show all groups'))
    expect(getLegendStore('gm-only').getState().layers.solo!.visible).toBe(true)
  })

  it('Collapse all groups toggles and flips its label', () => {
    renderWithMenu('gm-collapse')
    fireEvent.click(screen.getByLabelText('Group options'))
    fireEvent.click(screen.getByText('Collapse all groups'))
    const state = getLegendStore('gm-collapse').getState()
    expect(state.groups.g1!.collapsed).toBe(true)
    expect(state.groups.g2!.collapsed).toBe(true)
    // Reopen: the item now offers Expand.
    fireEvent.click(screen.getByLabelText('Group options'))
    fireEvent.click(screen.getByText('Expand all groups'))
    expect(getLegendStore('gm-collapse').getState().groups.g1!.collapsed).toBe(
      false,
    )
  })

  it('Zoom to group fires the composed handler with the group id', async () => {
    renderWithMenu('gm-zoom')
    fireEvent.click(screen.getByLabelText('Group options'))
    fireEvent.click(screen.getByText('Zoom to group'))
    expect(zoomSpy).toHaveBeenCalledWith('g1')
    await vi.waitFor(() => {
      expect(screen.queryByRole('menuitem')).toBeNull()
    })
  })

  it('Zoom to group keeps the menu open with loading feedback while pending', async () => {
    let resolveZoom!: () => void
    const pendingZoom = vi.fn(
      () =>
        new Promise<void>((resolve) => {
          resolveZoom = resolve
        }),
    )
    render(
      <LegendProvider id='gm-zoom-busy' layers={layers} groups={groups}>
        <LegendGroup groupId='g1'>
          <LegendActions>
            <LegendGroupMenu>
              <LegendZoomToGroup onZoomTo={pendingZoom} />
            </LegendGroupMenu>
          </LegendActions>
        </LegendGroup>
      </LegendProvider>,
    )
    fireEvent.click(screen.getByLabelText('Group options'))
    fireEvent.click(screen.getByText('Zoom to group'))
    expect(pendingZoom).toHaveBeenCalledWith('g1')
    const item = await screen.findByRole('menuitem', { name: /Loading/ })
    expect(item.getAttribute('aria-disabled')).toBe('true')
    expect(screen.getByRole('progressbar')).toBeDefined()

    resolveZoom()
    await vi.waitFor(() => {
      expect(screen.queryByRole('menuitem')).toBeNull()
    })
  })

  it('Show all layers composed directly in the group menu targets only that group', () => {
    render(
      <LegendProvider
        id='gm-showall-group'
        layers={[...layers, { id: 'solo', name: 'Solo', variables: [cat] }]}
        groups={groups}
      >
        <LegendGroup groupId='g1'>
          <LegendActions>
            <LegendVisibilityToggle />
            <LegendGroupMenu>
              <LegendShowAllLayers />
            </LegendGroupMenu>
          </LegendActions>
        </LegendGroup>
      </LegendProvider>,
    )
    getLegendStore('gm-showall-group').getState().setVisibility('a', false)
    getLegendStore('gm-showall-group').getState().setVisibility('b', false)
    getLegendStore('gm-showall-group').getState().setVisibility('solo', false)

    fireEvent.click(screen.getByLabelText('Group options'))
    fireEvent.click(screen.getByText('Show all layers'))
    const state = getLegendStore('gm-showall-group').getState()
    expect(state.layers.a!.visible).toBe(true)
    expect(state.layers.b!.visible).toBe(true)
    // Ungrouped sibling untouched — this instance only affects g1.
    expect(state.layers.solo!.visible).toBe(false)
  })
})

describe('panel-wide group actions with no groups', () => {
  it('ShowAllGroups / CollapseAllGroups render null when the legend has no groups', () => {
    render(
      <LegendProvider
        id='no-groups'
        layers={[{ id: 'solo', name: 'Solo', variables: [cat] }]}
        groups={[]}
      >
        <LegendShowAllGroups />
        <LegendCollapseAllGroups />
      </LegendProvider>,
    )
    expect(screen.queryByText('Show all groups')).toBeNull()
    expect(screen.queryByText('Collapse all groups')).toBeNull()
  })
})
