import { describe, expect, it, vi } from 'vitest'
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { MenuProvider } from './provider.js'
import type { EntityTarget, MenuClient, MenuItemNode, MenuTree } from './client.js'
import { MenuEditor } from './editor.js'

const menuRow = {
  id: 'm1',
  key: 'primary',
  label: 'Primary',
  createdAtMs: 0,
  updatedAtMs: 0,
}

const itemNode = (
  id: string,
  label: string,
  overrides: Partial<MenuItemNode> = {},
): MenuItemNode => ({
  id,
  menuId: 'm1',
  parentId: null,
  position: 0,
  depth: 0,
  label,
  targetKind: 'url',
  url: `/${label.toLowerCase()}`,
  targetEntityType: null,
  targetEntityId: null,
  openInNew: false,
  createdAtMs: 0,
  updatedAtMs: 0,
  children: [],
  ...overrides,
})

const treeFixture = (): MenuTree => ({
  menu: menuRow,
  items: [
    itemNode('i1', 'Home', { position: 0, url: '/' }),
    itemNode('i2', 'About', { position: 1, url: '/about' }),
  ],
})

const targets: EntityTarget[] = [
  { entityType: 'content', entityId: 'c1', label: 'Welcome post' },
]

function makeClient(overrides: Partial<MenuClient> = {}) {
  const tree = treeFixture()
  return {
    listMenus: vi.fn(async () => [menuRow]),
    getMenuTree: vi.fn(async () => tree),
    createMenu: vi.fn(),
    updateMenu: vi.fn(),
    deleteMenu: vi.fn(),
    addItem: vi.fn(async () => itemNode('i3', 'Contact', { position: 2, url: '/contact' })),
    updateItem: vi.fn(async (_id, patch) => ({ ...tree.items[0]!, ...patch, label: patch.label ?? 'Home' })),
    removeItem: vi.fn(async () => {}),
    moveItem: vi.fn(async (id) => tree.items.find((i) => i.id === id)!),
    reorder: vi.fn(async () => {}),
    ...overrides,
  } as unknown as MenuClient
}

function renderEditor(client = makeClient(), availableTargets: EntityTarget[] = targets) {
  render(
    <MenuProvider client={client}>
      <MenuEditor menuKey="primary" availableTargets={availableTargets} />
    </MenuProvider>,
  )
  return client
}

describe('MenuEditor', () => {
  it('renders the item tree with role=tree and treeitem', async () => {
    renderEditor()
    await waitFor(() => expect(screen.getByRole('tree')).toBeTruthy())
    expect(screen.getAllByRole('treeitem').length).toBe(2)
    expect(screen.getByLabelText('Home')).toBeTruthy()
    expect(screen.getByLabelText('About')).toBeTruthy()
  })

  it('keeps keyboard focus on move-up at the first-item boundary via aria-disabled', async () => {
    const client = makeClient()
    renderEditor(client)
    await waitFor(() => expect(screen.getByLabelText('Home')).toBeTruthy())
    const moveUp = screen.getByRole('button', { name: 'Move Home up' }) as HTMLButtonElement
    moveUp.focus()
    fireEvent.click(moveUp)
    expect(moveUp.getAttribute('aria-disabled')).toBe('true')
    expect(document.activeElement).toBe(moveUp)
    expect(document.body.contains(moveUp)).toBe(true)
    expect(client.reorder).not.toHaveBeenCalled()
    const labels = screen.getAllByLabelText(/^(Home|About)$/)
    expect((labels[0] as HTMLInputElement).getAttribute('aria-label')).toBe('Home')
    expect((labels[1] as HTMLInputElement).getAttribute('aria-label')).toBe('About')
  })

  it('keeps keyboard focus on move-down as the same item travels through repeated non-boundary reorders', async () => {
    const tree: MenuTree = {
      menu: menuRow,
      items: [
        itemNode('i1', 'Home', { position: 0, url: '/' }),
        itemNode('i2', 'About', { position: 1, url: '/about' }),
        itemNode('i3', 'Blog', { position: 2, url: '/blog' }),
        itemNode('i4', 'Contact', { position: 3, url: '/contact' }),
      ],
    }
    const client = makeClient({ getMenuTree: vi.fn(async () => tree) })
    renderEditor(client)
    await waitFor(() => expect(screen.getByLabelText('Home')).toBeTruthy())

    const labelOrder = () =>
      screen
        .getAllByLabelText(/^(Home|About|Blog|Contact)$/)
        .map((el) => (el as HTMLInputElement).getAttribute('aria-label'))

    const homeMoveDown = () =>
      screen.getByRole('button', { name: 'Move Home down' }) as HTMLButtonElement
    homeMoveDown().focus()
    expect(labelOrder().indexOf('Home')).toBe(0)

    fireEvent.click(homeMoveDown())
    await waitFor(() => expect(labelOrder().indexOf('Home')).toBe(1))
    expect(document.activeElement).toBe(homeMoveDown())

    fireEvent.click(homeMoveDown())
    await waitFor(() => expect(labelOrder().indexOf('Home')).toBe(2))
    expect(document.activeElement).toBe(homeMoveDown())

    fireEvent.click(homeMoveDown())
    await waitFor(() => expect(labelOrder().indexOf('Home')).toBe(3))
    expect(document.activeElement).toBe(homeMoveDown())
  })

  it('reorders items via keyboard-operable move-up and move-down buttons', async () => {
    const client = makeClient()
    renderEditor(client)
    await waitFor(() => expect(screen.getByLabelText('Home')).toBeTruthy())
    fireEvent.click(screen.getByRole('button', { name: 'Move Home down' }))
    await waitFor(() => expect(client.reorder).toHaveBeenCalledWith('m1', null, ['i2', 'i1']))
    const labels = screen.getAllByLabelText(/^(Home|About)$/)
    expect((labels[0] as HTMLInputElement).getAttribute('aria-label')).toBe('About')
    expect((labels[1] as HTMLInputElement).getAttribute('aria-label')).toBe('Home')
    fireEvent.click(screen.getByRole('button', { name: 'Move Home up' }))
    await waitFor(() => expect(client.reorder).toHaveBeenCalledWith('m1', null, ['i1', 'i2']))
  })

  it('exposes indent and outdent buttons with accessible names', async () => {
    const client = makeClient()
    renderEditor(client)
    await waitFor(() => expect(screen.getByLabelText('About')).toBeTruthy())
    expect(screen.getByRole('button', { name: 'Indent About' })).toBeTruthy()
    expect(screen.getByRole('button', { name: 'Outdent About' })).toBeTruthy()
    fireEvent.click(screen.getByRole('button', { name: 'Indent About' }))
    await waitFor(() =>
      expect(client.moveItem).toHaveBeenCalledWith('i2', { newParentId: 'i1', newPosition: 0 }),
    )
  })

  it('adds a new item through the add form', async () => {
    const client = makeClient()
    renderEditor(client)
    await waitFor(() => expect(screen.getByLabelText('Home')).toBeTruthy())
    fireEvent.change(screen.getByLabelText('New item label'), { target: { value: 'Contact' } })
    fireEvent.change(screen.getByLabelText('New item custom URL'), { target: { value: '/contact' } })
    fireEvent.click(screen.getByRole('button', { name: 'Add menu item' }))
    await waitFor(() => expect(client.addItem).toHaveBeenCalled())
    expect(client.addItem).toHaveBeenCalledWith('m1', {
      label: 'Contact',
      target: { kind: 'url', url: '/contact' },
      parentId: null,
    })
  })

  it('edits an item label via updateItem', async () => {
    const client = makeClient()
    renderEditor(client)
    await waitFor(() => expect(screen.getByLabelText('Home')).toBeTruthy())
    const input = screen.getByLabelText('Home') as HTMLInputElement
    fireEvent.change(input, { target: { value: 'Start' } })
    fireEvent.blur(input)
    await waitFor(() => expect(client.updateItem).toHaveBeenCalledWith('i1', { label: 'Start' }))
  })

  it('removes an item via removeItem', async () => {
    const client = makeClient()
    renderEditor(client)
    await waitFor(() => expect(screen.getByLabelText('Home')).toBeTruthy())
    fireEvent.click(screen.getByRole('button', { name: 'Remove Home' }))
    await waitFor(() => expect(client.removeItem).toHaveBeenCalledWith('i1'))
  })

  it('offers only injected availableTargets in the entity picker', async () => {
    renderEditor(makeClient(), targets)
    await waitFor(() => expect(screen.getByLabelText('Home')).toBeTruthy())
    const select = screen.getByLabelText('Home target') as HTMLSelectElement
    const optionLabels = Array.from(select.options).map((o) => o.textContent)
    expect(optionLabels).toEqual(['Custom URL', 'Welcome post'])
    expect(optionLabels).not.toContain('About')
    expect(optionLabels).not.toContain('Home')
  })

  it('includes a custom URL input for each item and the add form', async () => {
    renderEditor()
    await waitFor(() => expect(screen.getByLabelText('Home')).toBeTruthy())
    expect(screen.getByLabelText('Home custom URL')).toBeTruthy()
    expect(screen.getByLabelText('New item custom URL')).toBeTruthy()
  })

  it('surfaces hook errors in a focusable role=alert', async () => {
    const client = makeClient({
      reorder: vi.fn().mockRejectedValue(new Error('save failed')),
    })
    renderEditor(client)
    await waitFor(() => expect(screen.getByLabelText('Home')).toBeTruthy())
    fireEvent.click(screen.getByRole('button', { name: 'Move Home down' }))
    const alert = await screen.findByRole('alert')
    expect(alert.textContent).toMatch(/save failed/i)
    expect(alert.getAttribute('tabindex')).toBe('0')
  })
})
