import { describe, it, expect, vi } from 'vitest'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { TreeView } from './TreeView'
import type { TreeNode } from './TreeView'

const nodes: TreeNode[] = [
  {
    id: 'root1',
    label: 'Root One',
    children: [
      {
        id: 'child1',
        label: 'Child One',
        meta: 'meta-a',
        children: [
          { id: 'grandchild1', label: 'Grandchild One' },
          { id: 'grandchild2', label: 'Grandchild Two' },
        ],
      },
      { id: 'child2', label: 'Child Two', icon: <span>★</span> },
    ],
  },
  {
    id: 'root2',
    label: 'Root Two',
  },
]

describe('TreeView', () => {
  describe('rendering', () => {
    it('renders top-level nodes', () => {
      render(<TreeView nodes={nodes} />)
      expect(screen.getByText('Root One')).toBeInTheDocument()
      expect(screen.getByText('Root Two')).toBeInTheDocument()
    })

    it('does not render children of collapsed nodes', () => {
      render(<TreeView nodes={nodes} />)
      expect(screen.queryByText('Child One')).not.toBeInTheDocument()
      expect(screen.queryByText('Child Two')).not.toBeInTheDocument()
    })

    it('renders icon when provided', () => {
      render(<TreeView nodes={nodes} expandedIds={['root1']} onToggle={() => {}} />)
      expect(screen.getByText('★')).toBeInTheDocument()
    })

    it('renders meta text when provided', () => {
      render(<TreeView nodes={nodes} expandedIds={['root1']} onToggle={() => {}} />)
      expect(screen.getByText('meta-a')).toBeInTheDocument()
    })

    it('shows chevron on parent nodes', () => {
      render(<TreeView nodes={nodes} />)
      // Root One has children so should have a chevron
      const treeitem = screen.getByRole('treeitem', { name: /Root One/i })
      expect(treeitem.textContent).toContain('▸')
    })

    it('shows no chevron on leaf nodes', () => {
      render(<TreeView nodes={nodes} />)
      // Root Two is a leaf
      const treeitem = screen.getByRole('treeitem', { name: /Root Two/i })
      expect(treeitem.textContent).not.toContain('▸')
      expect(treeitem.textContent).not.toContain('▾')
    })
  })

  describe('ARIA attributes', () => {
    it('has role="tree" on root element', () => {
      render(<TreeView nodes={nodes} />)
      expect(screen.getByRole('tree')).toBeInTheDocument()
    })

    it('has role="treeitem" on each node row', () => {
      render(<TreeView nodes={nodes} />)
      const items = screen.getAllByRole('treeitem')
      expect(items.length).toBeGreaterThanOrEqual(2)
    })

    it('sets aria-expanded="false" on collapsed parent nodes', () => {
      render(<TreeView nodes={nodes} />)
      const treeitem = screen.getByRole('treeitem', { name: /Root One/i })
      expect(treeitem).toHaveAttribute('aria-expanded', 'false')
    })

    it('sets aria-expanded="true" on expanded parent nodes', () => {
      render(<TreeView nodes={nodes} expandedIds={['root1']} onToggle={() => {}} />)
      const treeitem = screen.getByRole('treeitem', { name: /Root One/i })
      expect(treeitem).toHaveAttribute('aria-expanded', 'true')
    })

    it('does not set aria-expanded on leaf nodes', () => {
      render(<TreeView nodes={nodes} />)
      const treeitem = screen.getByRole('treeitem', { name: /Root Two/i })
      expect(treeitem).not.toHaveAttribute('aria-expanded')
    })

    it('sets aria-selected on selected node', () => {
      render(<TreeView nodes={nodes} selectedId="root2" />)
      const treeitem = screen.getByRole('treeitem', { name: /Root Two/i })
      expect(treeitem).toHaveAttribute('aria-selected', 'true')
    })
  })

  describe('expand / collapse (uncontrolled)', () => {
    it('expands a collapsed parent on click', async () => {
      const user = userEvent.setup()
      render(<TreeView nodes={nodes} />)
      await user.click(screen.getByRole('treeitem', { name: /Root One/i }))
      expect(screen.getByText('Child One')).toBeInTheDocument()
      expect(screen.getByText('Child Two')).toBeInTheDocument()
    })

    it('collapses an expanded parent on second click', async () => {
      const user = userEvent.setup()
      render(<TreeView nodes={nodes} />)
      const rootOne = screen.getByRole('treeitem', { name: /Root One/i })
      await user.click(rootOne)
      expect(screen.getByText('Child One')).toBeInTheDocument()
      await user.click(rootOne)
      expect(screen.queryByText('Child One')).not.toBeInTheDocument()
    })

    it('shows expanded chevron when expanded', async () => {
      const user = userEvent.setup()
      render(<TreeView nodes={nodes} />)
      const rootOne = screen.getByRole('treeitem', { name: /Root One/i })
      await user.click(rootOne)
      expect(rootOne.textContent).toContain('▾')
    })
  })

  describe('expand / collapse (controlled)', () => {
    it('renders children based on controlled expandedIds', () => {
      render(
        <TreeView nodes={nodes} expandedIds={['root1']} onToggle={() => {}} />,
      )
      expect(screen.getByText('Child One')).toBeInTheDocument()
    })

    it('calls onToggle with node id when parent is clicked', async () => {
      const onToggle = vi.fn()
      const user = userEvent.setup()
      render(<TreeView nodes={nodes} expandedIds={[]} onToggle={onToggle} />)
      await user.click(screen.getByRole('treeitem', { name: /Root One/i }))
      expect(onToggle).toHaveBeenCalledWith('root1')
    })

    it('does not call onToggle when leaf is clicked', async () => {
      const onToggle = vi.fn()
      const user = userEvent.setup()
      render(<TreeView nodes={nodes} expandedIds={[]} onToggle={onToggle} />)
      await user.click(screen.getByRole('treeitem', { name: /Root Two/i }))
      expect(onToggle).not.toHaveBeenCalled()
    })
  })

  describe('selection', () => {
    it('calls onSelect with node id when a node is clicked', async () => {
      const onSelect = vi.fn()
      const user = userEvent.setup()
      render(<TreeView nodes={nodes} onSelect={onSelect} />)
      await user.click(screen.getByRole('treeitem', { name: /Root Two/i }))
      expect(onSelect).toHaveBeenCalledWith('root2')
    })

    it('calls onSelect for parent nodes too', async () => {
      const onSelect = vi.fn()
      const user = userEvent.setup()
      render(<TreeView nodes={nodes} onSelect={onSelect} />)
      await user.click(screen.getByRole('treeitem', { name: /Root One/i }))
      expect(onSelect).toHaveBeenCalledWith('root1')
    })

    it('marks the selected node visually', () => {
      render(<TreeView nodes={nodes} selectedId="root2" />)
      const treeitem = screen.getByRole('treeitem', { name: /Root Two/i })
      expect(treeitem.getAttribute('aria-selected')).toBe('true')
    })
  })

  describe('keyboard navigation', () => {
    it('moves focus down with ArrowDown', async () => {
      const user = userEvent.setup()
      render(<TreeView nodes={nodes} />)
      const rootOne = screen.getByRole('treeitem', { name: /Root One/i })
      rootOne.focus()
      await user.keyboard('{ArrowDown}')
      expect(screen.getByRole('treeitem', { name: /Root Two/i })).toHaveFocus()
    })

    it('moves focus up with ArrowUp', async () => {
      const user = userEvent.setup()
      render(<TreeView nodes={nodes} />)
      const rootTwo = screen.getByRole('treeitem', { name: /Root Two/i })
      rootTwo.focus()
      await user.keyboard('{ArrowUp}')
      expect(screen.getByRole('treeitem', { name: /Root One/i })).toHaveFocus()
    })

    it('expands a collapsed node with ArrowRight', async () => {
      const user = userEvent.setup()
      render(<TreeView nodes={nodes} />)
      const rootOne = screen.getByRole('treeitem', { name: /Root One/i })
      rootOne.focus()
      await user.keyboard('{ArrowRight}')
      expect(screen.getByText('Child One')).toBeInTheDocument()
    })

    it('moves to first child with ArrowRight when already expanded', async () => {
      const user = userEvent.setup()
      render(<TreeView nodes={nodes} />)
      const rootOne = screen.getByRole('treeitem', { name: /Root One/i })
      rootOne.focus()
      // First ArrowRight expands, second moves to first child
      await user.keyboard('{ArrowRight}')
      await user.keyboard('{ArrowRight}')
      expect(screen.getByRole('treeitem', { name: /Child One/i })).toHaveFocus()
    })

    it('collapses an expanded node with ArrowLeft', async () => {
      const user = userEvent.setup()
      render(<TreeView nodes={nodes} />)
      const rootOne = screen.getByRole('treeitem', { name: /Root One/i })
      rootOne.focus()
      await user.keyboard('{ArrowRight}')
      expect(screen.getByText('Child One')).toBeInTheDocument()
      await user.keyboard('{ArrowLeft}')
      expect(screen.queryByText('Child One')).not.toBeInTheDocument()
    })

    it('moves to parent with ArrowLeft on a child node', async () => {
      const user = userEvent.setup()
      render(<TreeView nodes={nodes} />)
      const rootOne = screen.getByRole('treeitem', { name: /Root One/i })
      rootOne.focus()
      // Expand root1 then move to child
      await user.keyboard('{ArrowRight}')
      await user.keyboard('{ArrowRight}')
      // Now at Child One
      expect(screen.getByRole('treeitem', { name: /Child One/i })).toHaveFocus()
      await user.keyboard('{ArrowLeft}')
      // Should move back to Root One
      expect(rootOne).toHaveFocus()
    })

    it('selects focused node with Enter', async () => {
      const onSelect = vi.fn()
      const user = userEvent.setup()
      render(<TreeView nodes={nodes} onSelect={onSelect} />)
      const rootOne = screen.getByRole('treeitem', { name: /Root One/i })
      rootOne.focus()
      await user.keyboard('{Enter}')
      expect(onSelect).toHaveBeenCalledWith('root1')
    })

    it('moves to first visible node with Home', async () => {
      const user = userEvent.setup()
      render(<TreeView nodes={nodes} />)
      const rootTwo = screen.getByRole('treeitem', { name: /Root Two/i })
      rootTwo.focus()
      await user.keyboard('{Home}')
      expect(screen.getByRole('treeitem', { name: /Root One/i })).toHaveFocus()
    })

    it('moves to last visible node with End', async () => {
      const user = userEvent.setup()
      render(<TreeView nodes={nodes} />)
      const rootOne = screen.getByRole('treeitem', { name: /Root One/i })
      rootOne.focus()
      await user.keyboard('{End}')
      expect(screen.getByRole('treeitem', { name: /Root Two/i })).toHaveFocus()
    })
  })

  describe('deep nesting', () => {
    it('renders grandchildren when both ancestor nodes are expanded', () => {
      render(
        <TreeView
          nodes={nodes}
          expandedIds={['root1', 'child1']}
          onToggle={() => {}}
        />,
      )
      expect(screen.getByText('Grandchild One')).toBeInTheDocument()
      expect(screen.getByText('Grandchild Two')).toBeInTheDocument()
    })

    it('does not render grandchildren when parent is collapsed', () => {
      render(
        <TreeView
          nodes={nodes}
          expandedIds={['root1']}
          onToggle={() => {}}
        />,
      )
      expect(screen.queryByText('Grandchild One')).not.toBeInTheDocument()
    })
  })
})
