import { html, fixture, assert, nextFrame, aTimeout } from '@open-wc/testing'
import sinon from 'sinon'
import { ContextualMenu } from '../../src/elements/contextual-menu/internals/ContextualMenu.js'
import {
  ContextMenuCommand,
  EnabledMenuOptions,
  CommandExecuteOptions,
} from '../../src/elements/contextual-menu/internals/types.js'

import '../../src/elements/contextual-menu/contextual-menu.js'

const commands = [
  {
    target: 'all',
    label: 'test',
    title: 'test-title',
    execute: (): void => {
      // ..
    },
  },
  {
    target: 'all',
    label: 'disabled',
    title: 'disabled-title',
    enabled: false,
    execute: (): void => {
      // ..
    },
  },
  {
    target: 'target',
    label: 'Target only',
    title: 'test title',
    execute: (): void => {
      // console.log(args);
    },
  },
  {
    target: 'custom',
    label: 'Custom only',
    title: 'custom title',
    execute: (): void => {
      // console.log(args);
    },
  },
  {
    target: 'all',
    label: 'visible',
    title: 'visible-title',
    visible: (): boolean => false,
    execute: (): void => {
      // ..
    },
  },
]

describe('ContextualMenu', () => {
  describe('constructor()', () => {
    let workspace: HTMLDivElement
    beforeEach(async () => {
      workspace = await fixture(html`<div></div>`)
    })

    it('sets the "workspace" property', async () => {
      const menu = new ContextualMenu(workspace)
      assert.isTrue(menu.workspace === workspace)
    })

    it('initializes the store', async () => {
      const menu = new ContextualMenu(workspace)
      assert.typeOf(menu.store, 'object')
    })

    it('has empty commands', async () => {
      const menu = new ContextualMenu(workspace)
      assert.deepEqual(menu.commands, [])
    })

    it('is not "connected"', async () => {
      const menu = new ContextualMenu(workspace)
      assert.isFalse(menu.connected)
    })

    it('sets the default "options"', async () => {
      const menu = new ContextualMenu(workspace)
      assert.deepEqual(menu.options, {})
    })

    it('sets the passed "options"', async () => {
      const menu = new ContextualMenu(workspace, { cancelNativeWhenHandled: true })
      assert.deepEqual(menu.options, { cancelNativeWhenHandled: true })
    })
  })

  describe('elementToTarget()', () => {
    let workspace: HTMLDivElement
    let menu: ContextualMenu
    before(async () => {
      workspace = await fixture(html`<div></div>`)
      menu = new ContextualMenu(workspace)
    })

    it('returns "root" for the workspace', () => {
      const result = menu.elementToTarget(workspace)
      assert.equal(result, 'root')
    })

    it('returns the local name of the element only', () => {
      const node = document.createElement('div')
      const result = menu.elementToTarget(node)
      assert.equal(result, 'div')
    })

    it('returns the local name and the classes', () => {
      const node = document.createElement('div')
      node.className = 'a b c'
      const result = menu.elementToTarget(node)
      assert.equal(result, 'div.a.b.c')
    })

    it('ignores non-elements', () => {
      const node = document.createTextNode('test')
      // @ts-expect-error Testing invalid types
      const result = menu.elementToTarget(node)
      assert.isUndefined(result)
    })
  })

  describe('triggering the menu', () => {
    let workspace: HTMLDivElement
    let menu: ContextualMenu
    beforeEach(async () => {
      workspace = await fixture(html`<div><span></span></div>`)
      menu = new ContextualMenu(workspace)
      menu.connect()
    })

    afterEach(() => {
      menu.disconnect()
    })

    it('adds the menu on the root click', () => {
      const [cmd] = commands
      menu.registerCommands([cmd])
      workspace.dispatchEvent(
        new MouseEvent('contextmenu', {
          clientX: 1,
          clientY: 1,
        })
      )
      const instance = workspace.querySelector('.context-menu')
      assert.ok(instance)
    })

    it('does not add the menu when no commands', () => {
      workspace.dispatchEvent(
        new MouseEvent('contextmenu', {
          clientX: 1,
          clientY: 1,
        })
      )
      const instance = workspace.querySelector('.context-menu')
      assert.notOk(instance)
    })

    it('does not add the menu when no target', () => {
      const cmd = commands[2]
      menu.registerCommands([cmd])
      workspace.dispatchEvent(
        new MouseEvent('contextmenu', {
          clientX: 1,
          clientY: 1,
        })
      )
      const instance = workspace.querySelector('.context-menu')
      assert.notOk(instance)
    })

    it('renders menu for a target', () => {
      menu.registerCommands([...commands])
      menu.elementToTarget = (): string => 'target'
      const span = workspace.querySelector('span')!
      span.dispatchEvent(
        new MouseEvent('contextmenu', {
          clientX: 1,
          clientY: 1,
          bubbles: true,
          cancelable: true,
        })
      )
      const instance = workspace.querySelector('.context-menu')
      assert.ok(instance)
    })

    it('has all target commands', async () => {
      menu.registerCommands([...commands])
      menu.elementToTarget = (): string => 'target'
      const span = workspace.querySelector('span')!
      span.dispatchEvent(
        new MouseEvent('contextmenu', {
          clientX: 1,
          clientY: 1,
          bubbles: true,
          cancelable: true,
        })
      )
      await nextFrame()
      const menuElement = workspace.querySelector('contextual-menu')!
      const instances = menuElement.shadowRoot!.querySelectorAll(':host > *')
      assert.lengthOf(instances, 4)
    })

    it('cancels the native event', () => {
      menu.registerCommands([...commands])
      menu.elementToTarget = (): string => 'target'
      const span = workspace.querySelector('span')!
      const e = new MouseEvent('contextmenu', {
        clientX: 1,
        clientY: 1,
        bubbles: true,
        cancelable: true,
      })
      span.dispatchEvent(e)
      assert.isTrue(e.defaultPrevented)
    })

    it('cancels the native event with cancelNativeWhenHandled', () => {
      menu.registerCommands([...commands])
      menu.elementToTarget = (): string => 'target'
      menu.options.cancelNativeWhenHandled = true
      const span = workspace.querySelector('span')!
      const e = new MouseEvent('contextmenu', {
        clientX: 1,
        clientY: 1,
        bubbles: true,
        cancelable: true,
      })
      span.dispatchEvent(e)
      assert.isTrue(e.defaultPrevented)
    })

    it('does not cancel the native event with cancelNativeWhenHandled', () => {
      const [cmd] = commands
      menu.registerCommands([cmd])
      menu.elementToTarget = (): string | undefined => undefined
      menu.options.cancelNativeWhenHandled = true
      const span = workspace.querySelector('span')!
      const e = new MouseEvent('contextmenu', {
        clientX: 1,
        clientY: 1,
        bubbles: true,
        cancelable: true,
      })
      span.dispatchEvent(e)
      assert.isFalse(e.defaultPrevented)
    })
  })

  describe('disabled command', () => {
    let workspace: HTMLDivElement
    let menu: ContextualMenu
    beforeEach(async () => {
      workspace = await fixture(html`<div></div>`)
      menu = new ContextualMenu(workspace)
      menu.connect()
    })

    it('renders command disabled', async () => {
      menu.registerCommands([...commands])
      workspace.dispatchEvent(
        new MouseEvent('contextmenu', {
          clientX: 1,
          clientY: 1,
        })
      )
      await nextFrame()
      const menuElement = workspace.querySelector('contextual-menu')!
      const item = menuElement.shadowRoot!.querySelector('.disabled')!
      assert.isTrue(item.classList.contains('disabled'))
    })
  })

  describe('visible command', () => {
    let workspace: HTMLDivElement
    let menu: ContextualMenu
    beforeEach(async () => {
      workspace = await fixture(html`<div></div>`)
      menu = new ContextualMenu(workspace)
      menu.connect()
    })

    it('renders command hidden', async () => {
      menu.registerCommands([...commands])
      workspace.dispatchEvent(
        new MouseEvent('contextmenu', {
          clientX: 1,
          clientY: 1,
        })
      )
      await nextFrame()
      const menuElement = workspace.querySelector('contextual-menu')!
      const item = menuElement.shadowRoot!.querySelector('.hidden')!
      assert.isTrue(item.hasAttribute('disabled'))
    })
  })

  describe('triggering the menu from the custom event', () => {
    let workspace: HTMLDivElement
    let menu: ContextualMenu
    beforeEach(async () => {
      workspace = await fixture(html`<div><span></span></div>`)
      menu = new ContextualMenu(workspace)
      menu.connect()
      menu.registerCommands(commands)
    })

    it('renders the menu with the default values', () => {
      workspace.dispatchEvent(
        new CustomEvent('custommenu', {
          detail: {
            name: 'custom',
          },
        })
      )
      const instance = workspace.querySelector('.context-menu')!
      assert.ok(instance)
      const styles = getComputedStyle(instance)
      assert.equal(styles.top, '0px', 'has default y position')
      assert.equal(styles.left, '0px', 'has default x position')
    })

    it('renders the menu with the passed coordinates', () => {
      workspace.dispatchEvent(
        new CustomEvent('custommenu', {
          detail: {
            name: 'custom',
            x: 20,
            y: 40,
          },
        })
      )
      const instance = workspace.querySelector('.context-menu')!
      const styles = getComputedStyle(instance)
      assert.equal(styles.top, '40px', 'has default y position')
      assert.equal(styles.left, '20px', 'has default x position')
    })

    it('passes the custom data to the execute function', async () => {
      const customData = {
        test: true,
      }
      let args: CommandExecuteOptions | undefined
      const cmd: ContextMenuCommand = {
        target: 'custom',
        label: 'Custom 2',
        execute: (arg: CommandExecuteOptions): void => {
          args = arg
        },
      }
      menu.addCommand(cmd)
      workspace.dispatchEvent(
        new CustomEvent('custommenu', {
          detail: {
            name: 'custom',
            customData,
          },
        })
      )
      await nextFrame()
      const menuElement = workspace.querySelector('contextual-menu')!
      const items = menuElement.shadowRoot!.querySelectorAll('ui-list-item')
      const item = items[items.length - 1] as HTMLElement
      item.click()
      await nextFrame()
      assert.deepEqual(args?.customData, customData)
    })

    it('passes the custom data to the enabled function', async () => {
      const customData = {
        test: true,
      }
      let args: EnabledMenuOptions | undefined
      const cmd: ContextMenuCommand = {
        target: 'custom',
        label: 'Custom 2',
        execute: (): void => {
          // ...
        },
        enabled: (arg: EnabledMenuOptions): boolean => {
          args = arg
          return true
        },
      }
      menu.addCommand(cmd)
      workspace.dispatchEvent(
        new CustomEvent('custommenu', {
          detail: {
            name: 'custom',
            customData,
          },
        })
      )
      await nextFrame()
      const menuElement = workspace.querySelector('contextual-menu')!
      const items = menuElement.shadowRoot!.querySelectorAll('ui-list-item')!
      const item = items[3] as HTMLElement
      item.click()
      await nextFrame()
      assert.deepEqual(args?.customData, customData)
    })
  })

  describe('closing the menu via click', () => {
    let workspace: HTMLDivElement
    let menu: ContextualMenu
    beforeEach(async () => {
      workspace = await fixture(html`<div><span></span></div>`)
      menu = new ContextualMenu(workspace)
      menu.registerCommands(commands)
      menu.connect()
      workspace.dispatchEvent(
        new CustomEvent('custommenu', {
          detail: {
            name: 'custom',
            x: 1,
            y: 1,
          },
        })
      )
      await nextFrame()
      assert.ok(menu.currentMenu, 'has the menu')
    })

    it('destroys the menu when clicking outside the menu', () => {
      document.body.click()
      assert.isUndefined(menu.currentMenu)
    })

    it('ignores when clicking inside the menu', () => {
      const menuElement = workspace.querySelector('contextual-menu')!
      menuElement.click()
      assert.ok(menu.currentMenu)
    })

    it('ignores when menu does not exist', () => {
      menu.destroy()
      document.body.click()
      assert.isUndefined(menu.currentMenu)
    })
  })

  describe('closing the menu via keyboard events', () => {
    let workspace: HTMLDivElement
    let menu: ContextualMenu
    beforeEach(async () => {
      workspace = await fixture(html`<div><span></span></div>`)
      menu = new ContextualMenu(workspace)
      menu.registerCommands(commands)
      menu.connect()
      workspace.dispatchEvent(
        new CustomEvent('custommenu', {
          detail: {
            name: 'custom',
            x: 1,
            y: 1,
          },
        })
      )
      await nextFrame()
      assert.ok(menu.currentMenu, 'has the menu')
    })

    it('destroys the menu when Escape key', () => {
      document.body.dispatchEvent(
        new KeyboardEvent('keydown', {
          bubbles: true,
          composed: true,
          key: 'Escape',
        })
      )
      assert.isUndefined(menu.currentMenu)
    })

    it('ignores other keys', () => {
      document.body.dispatchEvent(
        new KeyboardEvent('keydown', {
          bubbles: true,
          composed: true,
          key: 'Enter',
        })
      )
      assert.ok(menu.currentMenu)
    })

    it('ignores when no menu', () => {
      menu.destroy()
      document.body.dispatchEvent(
        new KeyboardEvent('keydown', {
          bubbles: true,
          composed: true,
          key: 'Escape',
        })
      )
      assert.isUndefined(menu.currentMenu)
    })
  })

  describe('nested commands', () => {
    const nested: ContextMenuCommand[] = [
      {
        target: 'root',
        label: 'Font size',
        id: 'font-size',
        children: [
          {
            label: '0.75 rem',
            id: '0.75rem',
          },
          {
            label: '1 rem',
            id: '1rem',
          },
          {
            label: '1.25 rem',
            id: '1.25rem',
          },
          {
            label: '1.5 rem',
            id: '1.5rem',
          },
          {
            label: '2 rem',
            id: '2rem',
          },
        ],
      },
    ]

    let workspace: HTMLDivElement
    let menu: ContextualMenu
    beforeEach(async () => {
      workspace = await fixture(html`<div><span></span></div>`)
      menu = new ContextualMenu(workspace)
      menu.registerCommands(nested)
      menu.connect()
    })

    it('processes child commands', () => {
      const [childItem] = menu.commands[0].children!
      assert.equal(childItem.constructor.name, 'MenuItem')
    })

    it('dispatches the "execute" event when no "execute" function in the commands chain', async () => {
      const spy = sinon.spy()
      menu.addEventListener('execute', spy)
      workspace.dispatchEvent(
        new MouseEvent('contextmenu', {
          clientX: 1,
          clientY: 1,
        })
      )
      await nextFrame()

      const item = menu.currentMenu!.shadowRoot!.querySelector(`ui-list-item[data-cmd="font-size"]`) as HTMLElement
      menu.currentMenu!.subMenuTimeout = 1
      item.dispatchEvent(
        new MouseEvent('mousemove', {
          bubbles: true,
          cancelable: true,
          composed: true,
          clientX: 0,
          clientY: 0,
        })
      )
      // for the menu debouncer
      await aTimeout(1)
      // for the menu to render
      await nextFrame()

      const sub = menu.currentMenu!.shadowRoot!.querySelector('contextual-menu')!
      const subItem = sub.shadowRoot!.querySelector(`ui-list-item`)!
      subItem.click()
      await nextFrame()
      assert.isTrue(spy.calledOnce, 'The event is dispatched')
      const { detail } = spy.args[0][0]
      assert.equal(detail.id, '0.75rem', 'has the id')
      assert.isTrue(detail.store === menu.store, 'has the store')
      assert.isTrue(detail.target === workspace, 'has the target')
      assert.isTrue(detail.root === workspace, 'has the root')
      assert.deepEqual(detail.clickPoint, { x: 1, y: 1 }, 'has the clickPoint')
      assert.equal(detail.selectedSubcommand, 0, 'has the selectedSubcommand')
      assert.isTrue(detail.item === sub.commands![0], 'has the item')
    })

    it('executes parent item "execute" function', async () => {
      let called = false
      let context: CommandExecuteOptions<unknown> | undefined
      menu.commands[0].execute = (ctx: CommandExecuteOptions<unknown>): void => {
        called = true
        context = ctx
      }
      workspace.dispatchEvent(
        new MouseEvent('contextmenu', {
          clientX: 1,
          clientY: 1,
        })
      )
      await nextFrame()

      const item = menu.currentMenu!.shadowRoot!.querySelector(`ui-list-item[data-cmd="font-size"]`) as HTMLElement
      menu.currentMenu!.subMenuTimeout = 1
      item.dispatchEvent(
        new MouseEvent('mousemove', {
          bubbles: true,
          cancelable: true,
          composed: true,
          clientX: 0,
          clientY: 0,
        })
      )
      // for the menu debouncer
      await aTimeout(1)
      // for the menu to render
      await nextFrame()
      const sub = menu.currentMenu!.shadowRoot!.querySelector('contextual-menu')!
      const subItem = sub.shadowRoot!.querySelector(`ui-list-item`)!
      subItem.click()
      await nextFrame()
      assert.isTrue(called, 'The function was called')
      assert.equal(context!.id, 'font-size', 'has the id')
      assert.isTrue(context!.store === menu.store, 'has the store')
      assert.isTrue(context!.target === workspace, 'has the target')
      assert.isTrue(context!.root === workspace, 'has the root')
      assert.deepEqual(context!.clickPoint, { x: 1, y: 1 }, 'has the clickPoint')
      assert.equal(context!.selectedSubcommand, 0, 'has the selectedSubcommand')
      assert.isTrue(context!.item === sub.commands![0], 'has the item')
    })
  })

  describe('the "execute" event', () => {
    let workspace: HTMLDivElement
    let menu: ContextualMenu
    beforeEach(async () => {
      workspace = await fixture(html`<div></div>`)
      menu = new ContextualMenu(workspace)
      menu.connect()
    })

    it('dispatches the event when no execute function', async () => {
      const spy = sinon.spy()
      menu.addEventListener('execute', spy)
      // @ts-expect-error For testing
      menu.registerCommands([{ label: 'test', target: 'all' }])
      workspace.dispatchEvent(
        new MouseEvent('contextmenu', {
          clientX: 1,
          clientY: 1,
        })
      )
      await nextFrame()
      const instance = menu.currentMenu!
      const item = instance.shadowRoot!.querySelector('ui-list-item') as HTMLElement
      item.click()
      await nextFrame()
      assert.isTrue(spy.called, 'the event is dispatched')
    })
  })

  describe('listCommands()', () => {
    let workspace: HTMLDivElement
    let target: HTMLDivElement
    let menu: ContextualMenu
    beforeEach(async () => {
      workspace = await fixture(html`<div></div>`)
      target = await fixture(html`<div class="container"><span class="label"></span></div>`)
      menu = new ContextualMenu(workspace)
    })

    it('list commands for "all" only', () => {
      menu.registerCommands([
        {
          target: 'all',
          id: '1',
        },
        {
          target: 'something',
          id: '2',
        },
        {
          target: 'all',
          id: '3',
        },
      ])
      const result = menu.listCommands('all', target)
      assert.lengthOf(result, 2, 'returns 2 items')
      assert.equal(result[0].id, '1', 'has the first item')
      assert.equal(result[1].id, '3', 'has the last item')
    })

    it('list commands for array target', () => {
      menu.registerCommands([
        {
          target: 'other',
          id: '1',
        },
        {
          target: ['something', 'other'],
          id: '2',
        },
        {
          target: 'other',
          id: '3',
        },
      ])
      const result = menu.listCommands('something', target)
      assert.lengthOf(result, 1, 'returns 1 item')
      assert.equal(result[0].id, '2', 'has the item')
    })

    it('returns specific target', () => {
      menu.registerCommands([
        {
          target: 'other',
          id: '1',
        },
        {
          target: 'something',
          id: '2',
        },
        {
          target: 'other',
          id: '3',
        },
      ])
      const result = menu.listCommands('something', target)
      assert.lengthOf(result, 1, 'returns 1 item')
      assert.equal(result[0].id, '2', 'has the item')
    })

    it('returns matched selector', () => {
      menu.registerCommands([
        {
          target: 'other',
          id: '1',
        },
        {
          target: 'div.container > span.label',
          id: '2',
        },
        {
          target: 'other',
          id: '3',
        },
      ])
      const result = menu.listCommands('something', target.children[0] as HTMLSpanElement)
      assert.lengthOf(result, 1, 'returns 1 item')
      assert.equal(result[0].id, '2', 'has the item')
    })
  })
})
