import { assert, fixture, html, nextFrame, oneEvent } from '@open-wc/testing'
import sinon from 'sinon'
import Menu from '../../../src/md/menu/internal/Menu.js'
import UiMenuItem from '../../../src/md/menu/internal/MenuItem.js'
import UiSubMenu from '../../../src/md/menu/internal/SubMenu.js'

import '../../../src/md/menu/ui-menu.js'
import '../../../src/md/menu/ui-menu-item.js'
import '../../../src/md/menu/ui-sub-menu.js'
import '../../../src/md/icons/ui-icon.js'
import '../../../src/md/button/ui-button.js'

describe('md', () => {
  describe('Menu', () => {
    async function basicFixture(): Promise<Menu> {
      return fixture(html`
        <ui-menu id="test-menu">
          <ui-menu-item>Item 1</ui-menu-item>
          <ui-menu-item>Item 2</ui-menu-item>
          <ui-menu-item disabled>Item 3 (Disabled)</ui-menu-item>
        </ui-menu>
      `)
    }

    async function submenuFixture(): Promise<Menu> {
      return fixture(html`
        <ui-menu id="main-menu">
          <ui-menu-item id="file-item" submenu="file-submenu">
            <span slot="start"><ui-icon>folder</ui-icon></span>
            <span>File</span>
          </ui-menu-item>
          <ui-menu-item>Item 2</ui-menu-item>

          <ui-sub-menu id="file-submenu" anchor="file-item">
            <ui-menu-item>New File</ui-menu-item>
            <ui-menu-item>Open File</ui-menu-item>
            <ui-menu-item id="export-item" submenu="export-submenu">Export</ui-menu-item>

            <ui-sub-menu id="export-submenu" anchor="export-item">
              <ui-menu-item>Export as PDF</ui-menu-item>
              <ui-menu-item>Export as PNG</ui-menu-item>
            </ui-sub-menu>
          </ui-sub-menu>
        </ui-menu>
      `)
    }

    async function withTriggerFixture(): Promise<HTMLElement> {
      return fixture(html`
        <div>
          <ui-button id="trigger" popovertarget="menu">Open Menu</ui-button>
          <ui-menu id="menu">
            <ui-menu-item>Item 1</ui-menu-item>
            <ui-menu-item>Item 2</ui-menu-item>
          </ui-menu>
        </div>
      `)
    }

    describe('Basic functionality', () => {
      it('should create menu element', async () => {
        const element = await basicFixture()
        assert.instanceOf(element, Menu)
        assert.equal(element.tagName.toLowerCase(), 'ui-menu')
      })

      it('should have correct default properties', async () => {
        const element = await basicFixture()
        assert.isFalse(element.open)
        assert.isFalse(element.disabled)
        assert.isNull(element.activeSubMenu)
      })

      it('should set correct ARIA attributes', async () => {
        const element = await basicFixture()
        assert.equal(element.getAttribute('role'), 'menu')
        assert.equal(element.getAttribute('aria-expanded'), 'false')
        assert.equal(element.tabIndex, -1)
      })

      it('should set popover attribute if not present', async () => {
        const element = await basicFixture()
        assert.equal(element.getAttribute('popover'), 'auto')
      })

      it('should generate ID if not present', async () => {
        const element: Menu = await fixture(html`<ui-menu><ui-menu-item>Test</ui-menu-item></ui-menu>`)
        assert.isString(element.id)
        assert.isTrue(element.id.length > 0)
      })
    })

    describe('Show/Hide functionality', () => {
      it('should show menu', async () => {
        const element = await basicFixture()
        const showSpy = sinon.spy(element, 'showPopover')

        element.show()
        await nextFrame()

        assert.isTrue(element.open)
        assert.equal(element.getAttribute('aria-expanded'), 'true')
        assert.equal(element.tabIndex, 0)
        assert.isTrue(showSpy.calledOnce)
      })

      it('should hide menu', async () => {
        const element = await basicFixture()
        const hideSpy = sinon.spy(element, 'hidePopover')

        element.show()
        await nextFrame()
        element.hide()
        await nextFrame()

        assert.isFalse(element.open)
        assert.equal(element.getAttribute('aria-expanded'), 'false')
        assert.equal(element.tabIndex, -1)
        assert.isTrue(hideSpy.calledOnce)
      })

      it('should dispatch open event when shown', async () => {
        const element = await basicFixture()

        setTimeout(() => element.show())
        const event = await oneEvent(element, 'open')

        assert.instanceOf(event, CustomEvent)
        assert.isFalse(event.bubbles)
        assert.isTrue(event.composed)
      })

      it('should dispatch close event when hidden', async () => {
        const element = await basicFixture()
        element.show()
        await nextFrame()

        setTimeout(() => element.hide())
        const event = await oneEvent(element, 'close')

        assert.instanceOf(event, CustomEvent)
        assert.isFalse(event.bubbles)
        assert.isTrue(event.composed)
      })
    })

    describe('Popover integration', () => {
      it('should toggle popover state', async () => {
        const element = await basicFixture()
        const superToggleSpy = sinon.spy(Object.getPrototypeOf(Object.getPrototypeOf(element)), 'togglePopover')

        const result = element.togglePopover()
        await nextFrame()

        assert.isTrue(element.open)
        assert.equal(element.getAttribute('aria-expanded'), 'true')
        assert.equal(element.tabIndex, 0)
        assert.isTrue(superToggleSpy.calledOnce)
        assert.isTrue(result)
      })

      it('should handle beforetoggle event', async () => {
        const element = await basicFixture()
        element.show()
        await nextFrame()

        // Simulate beforetoggle event as if popover was closed externally
        const toggleEvent = Object.assign(new Event('beforetoggle'), {
          newState: 'closed',
        }) as ToggleEvent

        element.dispatchEvent(toggleEvent)
        await nextFrame()

        assert.isFalse(element.open)
      })
    })

    describe('Keyboard navigation', () => {
      it('should close menu on Escape key', async () => {
        const element = await basicFixture()
        element.show()
        await nextFrame()

        const event = new KeyboardEvent('keydown', { key: 'Escape' })
        element.dispatchEvent(event)
        await nextFrame()

        assert.isFalse(element.open)
      })

      it('should not handle keys when menu is closed', async () => {
        const element = await basicFixture()
        const hideSpy = sinon.spy(element, 'hide')

        const event = new KeyboardEvent('keydown', { key: 'Escape' })
        element.dispatchEvent(event)
        await nextFrame()

        assert.isFalse(hideSpy.called)
      })

      it('should open submenu on ArrowRight', async () => {
        const element = await submenuFixture()
        const menuItem = element.querySelector('#file-item') as UiMenuItem
        const openSubMenuSpy = sinon.spy(menuItem, 'openSubMenu')

        element.show()
        await nextFrame()

        // Set the active item first
        element.activeListItem = menuItem

        const event = new KeyboardEvent('keydown', { key: 'ArrowRight' })
        element.dispatchEvent(event)

        assert.isTrue(openSubMenuSpy.calledOnce)
      })

      it('should call closeSubMenu on ArrowLeft', async () => {
        const element = await submenuFixture()
        const closeSubMenuSpy = sinon.spy(element, 'closeSubMenu')
        element.show()
        await nextFrame()

        const event = new KeyboardEvent('keydown', { key: 'ArrowLeft' })
        element.dispatchEvent(event)

        assert.isTrue(closeSubMenuSpy.calledOnce)
      })

      it('should delegate other keys to parent UiList', async () => {
        const element = await basicFixture()
        element.show()
        await nextFrame()

        // Test that arrow down moves to next item (handled by parent UiList)
        const firstItem = element.querySelector('ui-menu-item') as UiMenuItem
        element.activeListItem = firstItem

        const event = new KeyboardEvent('keydown', { key: 'ArrowDown' })
        element.dispatchEvent(event)
        await nextFrame()

        // Verify that navigation occurred (the active item should have changed)
        // This tests the integration with the parent UiList keyboard handling
        assert.isNotNull(element.activeListItem)
      })
    })

    describe('Submenu management', () => {
      it('should handle submenu behavior through keyboard navigation', async () => {
        const element = await submenuFixture()
        const menuItem = element.querySelector('#file-item') as UiMenuItem

        element.show()
        await nextFrame()

        // Set active item and trigger submenu open via keyboard
        element.activeListItem = menuItem
        const rightArrowEvent = new KeyboardEvent('keydown', { key: 'ArrowRight' })
        element.dispatchEvent(rightArrowEvent)

        // Check that submenu was opened
        await nextFrame()
        // The submenu should be set as active
        assert.isNotNull(element.activeSubMenu)
      })

      it('should handle submenu closing through keyboard navigation', async () => {
        const element = await submenuFixture()
        const submenu = element.querySelector('#file-submenu') as UiSubMenu

        element.show()
        await nextFrame()

        // Set active submenu
        element.setActiveSubMenu(submenu)

        // Close via keyboard
        const leftArrowEvent = new KeyboardEvent('keydown', { key: 'ArrowLeft' })
        element.dispatchEvent(leftArrowEvent)

        assert.isNull(element.activeSubMenu)
      })

      it('should close active submenu', async () => {
        const element = await submenuFixture()
        const submenu = element.querySelector('#file-submenu') as UiSubMenu
        const hideSpy = sinon.spy(submenu, 'hide')

        element.setActiveSubMenu(submenu)
        element.closeSubMenu()

        assert.isTrue(hideSpy.calledOnce)
        assert.isNull(element.activeSubMenu)
      })

      it('should set active submenu and add event listener', async () => {
        const element = await submenuFixture()
        const submenu = element.querySelector('#file-submenu') as UiSubMenu
        const addListenerSpy = sinon.spy(submenu, 'addEventListener')

        element.setActiveSubMenu(submenu)

        assert.equal(element.activeSubMenu, submenu)
        assert.isTrue(addListenerSpy.calledWith('select'))
      })

      it('should handle submenu selection', async () => {
        const element = await submenuFixture()
        const submenu = element.querySelector('#file-submenu') as UiSubMenu
        let selectionHandled = false

        // Listen for selection on the main menu
        element.addEventListener('select', () => {
          selectionHandled = true
        })

        element.setActiveSubMenu(submenu)

        // Simulate submenu selection
        const selectEvent = new CustomEvent('select', {
          detail: { item: submenu.querySelector('ui-menu-item'), index: 0 },
        })
        submenu.dispatchEvent(selectEvent)

        assert.isTrue(selectionHandled)
      })
    })

    describe('Menu item selection', () => {
      it('should hide menu and notify selection', async () => {
        const element = await basicFixture()
        const menuItem = element.querySelector('ui-menu-item') as UiMenuItem
        const hideSpy = sinon.spy(element, 'hide')
        let selectionNotified = false

        // Listen for selection event instead of spying on protected method
        element.addEventListener('select', () => {
          selectionNotified = true
        })

        element.show()
        await nextFrame()

        element.notifySelect(menuItem, 0)

        assert.isTrue(hideSpy.calledOnce)
        assert.isTrue(selectionNotified)
        assert.isFalse(element.open)
      })

      it('should dispatch select event when menu item is selected', async () => {
        const element = await basicFixture()
        const menuItem = element.querySelector('ui-menu-item') as UiMenuItem

        element.show()
        await nextFrame()

        setTimeout(() => menuItem.click())
        const event = (await oneEvent(element, 'select')) as CustomEvent<{ item: UiMenuItem; index: number }>

        assert.instanceOf(event, CustomEvent)
        assert.equal(event.detail.item, menuItem)
        assert.equal(event.detail.index, 0)
      })
    })

    describe('Disabled state', () => {
      it('should set disabled attribute and call setDisabled', async () => {
        const element = await basicFixture()

        element.disabled = true
        await nextFrame()

        assert.isTrue(element.hasAttribute('disabled'))
        assert.isTrue(element.disabled)
      })

      it('should reflect disabled property to attribute', async () => {
        const element = await basicFixture()

        element.disabled = true
        await nextFrame()

        assert.equal(element.getAttribute('disabled'), '')

        element.disabled = false
        await nextFrame()

        assert.isFalse(element.hasAttribute('disabled'))
      })
    })

    describe('Slot changes', () => {
      it('should handle slot content changes', async () => {
        const element = await basicFixture()

        // Add a new menu item
        const newItem = document.createElement('ui-menu-item')
        newItem.textContent = 'New Item'
        element.appendChild(newItem)

        // Simulate slot change
        const slot = element.shadowRoot!.querySelector('slot')!
        slot.dispatchEvent(new Event('slotchange'))

        // Verify the new item is now part of the menu
        const menuItems = element.querySelectorAll('ui-menu-item')
        assert.equal(menuItems.length, 4) // 3 original + 1 new
      })
    })

    describe('Integration with popover trigger', () => {
      it('should work with popovertarget attribute', async () => {
        const container = await withTriggerFixture()
        const trigger = container.querySelector('#trigger') as HTMLElement
        const menu = container.querySelector('#menu') as Menu

        assert.equal(trigger.getAttribute('popovertarget'), 'menu')
        assert.equal(menu.id, 'menu')

        // Test that clicking trigger opens menu
        trigger.click()
        await nextFrame()

        assert.isTrue(menu.open)
      })
    })

    describe('Focus management', () => {
      it('should focus menu when shown', async () => {
        const element = await basicFixture()
        const focusSpy = sinon.spy(element, 'focus')

        element.show()
        await nextFrame()

        assert.isTrue(focusSpy.calledOnce)
        assert.equal(element.tabIndex, 0)
      })

      it('should set tabindex correctly based on open state', async () => {
        const element = await basicFixture()

        assert.equal(element.tabIndex, -1)

        element.show()
        await nextFrame()

        assert.equal(element.tabIndex, 0)

        element.hide()
        await nextFrame()

        assert.equal(element.tabIndex, -1)
      })
    })

    describe('Rendering', () => {
      it('should render slot for menu items', async () => {
        const element = await basicFixture()
        await nextFrame()

        const slot = element.shadowRoot!.querySelector('slot')
        assert.isNotNull(slot)

        const menuItems = element.querySelectorAll('ui-menu-item')
        assert.equal(menuItems.length, 3)
      })

      it('should render with menu-container class', async () => {
        const element = await basicFixture()
        await nextFrame()

        const container = element.shadowRoot!.querySelector('.menu-container')
        assert.isNotNull(container)
      })
    })

    describe('Edge cases', () => {
      it('should handle closeSubMenu when no submenu is active', async () => {
        const element = await basicFixture()

        // Should not throw
        element.closeSubMenu()

        assert.isNull(element.activeSubMenu)
      })

      it('should handle keydown events that are already prevented', async () => {
        const element = await basicFixture()
        const hideSpy = sinon.spy(element, 'hide')
        element.show()
        await nextFrame()

        const event = new KeyboardEvent('keydown', { key: 'Escape' })
        event.preventDefault()
        Object.defineProperty(event, 'defaultPrevented', { value: true })

        element.dispatchEvent(event)

        // Should not handle the event since it's already prevented
        assert.isFalse(hideSpy.called)
      })
    })
  })
})
