// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

'use strict'

const assert = require('node:assert')
const command = require('selenium-webdriver/lib/command')
const error = require('selenium-webdriver/lib/error')
const input = require('selenium-webdriver/lib/input')
const { WebElement } = require('selenium-webdriver/lib/webdriver')

describe('input.Actions', function () {
  class StubExecutor {
    constructor(...responses) {
      this.responses = responses
      this.commands = []
    }

    execute(command) {
      const name = command.getName()
      const parameters = command.getParameters()
      this.commands.push({ name, parameters })
      return this.responses.shift() || Promise.reject(new Error('unexpected command: ' + command.getName()))
    }
  }

  describe('perform()', function () {
    it('omits idle devices', async function () {
      let executor = new StubExecutor(Promise.resolve(), Promise.resolve(), Promise.resolve(), Promise.resolve())

      await new input.Actions(executor).perform()
      assert.deepStrictEqual(executor.commands, [])

      await new input.Actions(executor).pause().perform()
      assert.deepStrictEqual(executor.commands, [])

      await new input.Actions(executor).pause(1).perform()
      assert.deepStrictEqual(executor.commands, [
        {
          name: command.Name.ACTIONS,
          parameters: {
            actions: [
              {
                id: 'default keyboard',
                type: 'key',
                actions: [{ type: 'pause', duration: 1 }],
              },
              {
                id: 'default mouse',
                type: 'pointer',
                parameters: { pointerType: 'mouse' },
                actions: [{ type: 'pause', duration: 1 }],
              },
              {
                id: 'default wheel',
                type: 'wheel',
                actions: [{ type: 'pause', duration: 1 }],
              },
            ],
          },
        },
      ])

      executor.commands.length = 0
      let actions = new input.Actions(executor)
      await actions.pause(1, actions.keyboard()).perform()
      assert.deepStrictEqual(executor.commands, [
        {
          name: command.Name.ACTIONS,
          parameters: {
            actions: [
              {
                id: 'default keyboard',
                type: 'key',
                actions: [{ type: 'pause', duration: 1 }],
              },
            ],
          },
        },
      ])
    })

    it('can be called multiple times', async function () {
      const executor = new StubExecutor(Promise.resolve(), Promise.resolve())
      const actions = new input.Actions(executor).keyDown(input.Key.SHIFT)

      const expected = {
        name: command.Name.ACTIONS,
        parameters: {
          actions: [
            {
              id: 'default keyboard',
              type: 'key',
              actions: [{ type: 'keyDown', value: input.Key.SHIFT }],
            },
          ],
        },
      }

      await actions.perform()
      assert.deepStrictEqual(executor.commands, [expected])

      await actions.perform()
      assert.deepStrictEqual(executor.commands, [expected, expected])
    })
  })

  describe('pause()', function () {
    it('defaults to all devices', async function () {
      const executor = new StubExecutor(Promise.resolve())

      await new input.Actions(executor).pause(3).perform()

      assert.deepStrictEqual(executor.commands, [
        {
          name: command.Name.ACTIONS,
          parameters: {
            actions: [
              {
                id: 'default keyboard',
                type: 'key',
                actions: [{ type: 'pause', duration: 3 }],
              },
              {
                id: 'default mouse',
                type: 'pointer',
                parameters: { pointerType: 'mouse' },
                actions: [{ type: 'pause', duration: 3 }],
              },
              {
                id: 'default wheel',
                type: 'wheel',
                actions: [
                  {
                    duration: 3,
                    type: 'pause',
                  },
                ],
              },
            ],
          },
        },
      ])
    })

    it('duration defaults to 0', async function () {
      const executor = new StubExecutor(Promise.resolve())

      await new input.Actions(executor).pause().pause(3).perform()

      assert.deepStrictEqual(executor.commands, [
        {
          name: command.Name.ACTIONS,
          parameters: {
            actions: [
              {
                id: 'default keyboard',
                type: 'key',
                actions: [
                  { type: 'pause', duration: 0 },
                  { type: 'pause', duration: 3 },
                ],
              },
              {
                id: 'default mouse',
                type: 'pointer',
                parameters: { pointerType: 'mouse' },
                actions: [
                  { type: 'pause', duration: 0 },
                  { type: 'pause', duration: 3 },
                ],
              },
              {
                id: 'default wheel',
                type: 'wheel',
                actions: [
                  { type: 'pause', duration: 0 },
                  { type: 'pause', duration: 3 },
                ],
              },
            ],
          },
        },
      ])
    })

    it('single device w/ synchronization', async function () {
      const executor = new StubExecutor(Promise.resolve())
      const actions = new input.Actions(executor)

      await actions.pause(100, actions.keyboard()).pause(100, actions.mouse()).perform()

      assert.deepStrictEqual(executor.commands, [
        {
          name: command.Name.ACTIONS,
          parameters: {
            actions: [
              {
                id: 'default keyboard',
                type: 'key',
                actions: [
                  { type: 'pause', duration: 100 },
                  { type: 'pause', duration: 0 },
                ],
              },
              {
                id: 'default mouse',
                type: 'pointer',
                parameters: { pointerType: 'mouse' },
                actions: [
                  { type: 'pause', duration: 0 },
                  { type: 'pause', duration: 100 },
                ],
              },
            ],
          },
        },
      ])
    })

    it('single device w/o synchronization', async function () {
      const executor = new StubExecutor(Promise.resolve())
      const actions = new input.Actions(executor, { async: true })

      await actions.pause(100, actions.keyboard()).pause(100, actions.mouse()).perform()

      assert.deepStrictEqual(executor.commands, [
        {
          name: command.Name.ACTIONS,
          parameters: {
            actions: [
              {
                id: 'default keyboard',
                type: 'key',
                actions: [{ type: 'pause', duration: 100 }],
              },
              {
                id: 'default mouse',
                type: 'pointer',
                parameters: { pointerType: 'mouse' },
                actions: [{ type: 'pause', duration: 100 }],
              },
            ],
          },
        },
      ])
    })

    it('pause a single device multiple times by specifying it multiple times', async function () {
      const executor = new StubExecutor(Promise.resolve())
      const actions = new input.Actions(executor)

      await actions.pause(100, actions.keyboard(), actions.keyboard()).perform()

      assert.deepStrictEqual(executor.commands, [
        {
          name: command.Name.ACTIONS,
          parameters: {
            actions: [
              {
                id: 'default keyboard',
                type: 'key',
                actions: [
                  { type: 'pause', duration: 100 },
                  { type: 'pause', duration: 100 },
                ],
              },
            ],
          },
        },
      ])
    })
  })

  describe('keyDown()', function () {
    it('sends normalized code point', async function () {
      let executor = new StubExecutor(Promise.resolve())

      await new input.Actions(executor).keyDown('\u0041\u030a').perform()
      assert.deepStrictEqual(executor.commands, [
        {
          name: command.Name.ACTIONS,
          parameters: {
            actions: [
              {
                id: 'default keyboard',
                type: 'key',
                actions: [{ type: 'keyDown', value: '\u00c5' }],
              },
            ],
          },
        },
      ])
    })

    it('rejects keys that are not a single code point', function () {
      const executor = new StubExecutor(Promise.resolve())
      const actions = new input.Actions(executor)
      assert.throws(() => actions.keyDown('\u1E9B\u0323'), error.InvalidArgumentError)
    })
  })

  describe('keyUp()', function () {
    it('sends normalized code point', async function () {
      let executor = new StubExecutor(Promise.resolve())

      await new input.Actions(executor).keyUp('\u0041\u030a').perform()
      assert.deepStrictEqual(executor.commands, [
        {
          name: command.Name.ACTIONS,
          parameters: {
            actions: [
              {
                id: 'default keyboard',
                type: 'key',
                actions: [{ type: 'keyUp', value: '\u00c5' }],
              },
            ],
          },
        },
      ])
    })

    it('rejects keys that are not a single code point', function () {
      const executor = new StubExecutor(Promise.resolve())
      const actions = new input.Actions(executor)
      assert.throws(() => actions.keyUp('\u1E9B\u0323'), error.InvalidArgumentError)
    })
  })

  describe('sendKeys()', function () {
    it('sends down/up for single key', async function () {
      const executor = new StubExecutor(Promise.resolve())
      const actions = new input.Actions(executor)

      await actions.sendKeys('a').perform()
      assert.deepStrictEqual(executor.commands, [
        {
          name: command.Name.ACTIONS,
          parameters: {
            actions: [
              {
                id: 'default keyboard',
                type: 'key',
                actions: [
                  { type: 'keyDown', value: 'a' },
                  { type: 'keyUp', value: 'a' },
                ],
              },
            ],
          },
        },
      ])
    })

    it('sends down/up for vararg keys', async function () {
      const executor = new StubExecutor(Promise.resolve())
      const actions = new input.Actions(executor)

      await actions.sendKeys('a', 'b').perform()
      assert.deepStrictEqual(executor.commands, [
        {
          name: command.Name.ACTIONS,
          parameters: {
            actions: [
              {
                id: 'default keyboard',
                type: 'key',
                actions: [
                  { type: 'keyDown', value: 'a' },
                  { type: 'keyUp', value: 'a' },
                  { type: 'keyDown', value: 'b' },
                  { type: 'keyUp', value: 'b' },
                ],
              },
            ],
          },
        },
      ])
    })

    it('sends down/up for multichar strings in varargs', async function () {
      const executor = new StubExecutor(Promise.resolve())
      const actions = new input.Actions(executor)

      await actions.sendKeys('a', 'bc', 'd').perform()
      assert.deepStrictEqual(executor.commands, [
        {
          name: command.Name.ACTIONS,
          parameters: {
            actions: [
              {
                id: 'default keyboard',
                type: 'key',
                actions: [
                  { type: 'keyDown', value: 'a' },
                  { type: 'keyUp', value: 'a' },
                  { type: 'keyDown', value: 'b' },
                  { type: 'keyUp', value: 'b' },
                  { type: 'keyDown', value: 'c' },
                  { type: 'keyUp', value: 'c' },
                  { type: 'keyDown', value: 'd' },
                  { type: 'keyUp', value: 'd' },
                ],
              },
            ],
          },
        },
      ])
    })

    it('synchronizes with other devices', async function () {
      const executor = new StubExecutor(Promise.resolve())
      const actions = new input.Actions(executor)

      await actions.sendKeys('ab').pause(100, actions.mouse()).perform()
      assert.deepStrictEqual(executor.commands, [
        {
          name: command.Name.ACTIONS,
          parameters: {
            actions: [
              {
                id: 'default keyboard',
                type: 'key',
                actions: [
                  { type: 'keyDown', value: 'a' },
                  { type: 'keyUp', value: 'a' },
                  { type: 'keyDown', value: 'b' },
                  { type: 'keyUp', value: 'b' },
                  { type: 'pause', duration: 0 },
                ],
              },
              {
                id: 'default mouse',
                type: 'pointer',
                parameters: { pointerType: 'mouse' },
                actions: [
                  { type: 'pause', duration: 0 },
                  { type: 'pause', duration: 0 },
                  { type: 'pause', duration: 0 },
                  { type: 'pause', duration: 0 },
                  { type: 'pause', duration: 100 },
                ],
              },
            ],
          },
        },
      ])
    })

    it('without device synchronization', async function () {
      const executor = new StubExecutor(Promise.resolve())
      const actions = new input.Actions(executor, { async: true })

      await actions.sendKeys('ab').pause(100, actions.mouse()).perform()
      assert.deepStrictEqual(executor.commands, [
        {
          name: command.Name.ACTIONS,
          parameters: {
            actions: [
              {
                id: 'default keyboard',
                type: 'key',
                actions: [
                  { type: 'keyDown', value: 'a' },
                  { type: 'keyUp', value: 'a' },
                  { type: 'keyDown', value: 'b' },
                  { type: 'keyUp', value: 'b' },
                ],
              },
              {
                id: 'default mouse',
                type: 'pointer',
                parameters: { pointerType: 'mouse' },
                actions: [{ type: 'pause', duration: 100 }],
              },
            ],
          },
        },
      ])
    })

    it('string length > 500', async function () {
      const executor = new StubExecutor(Promise.resolve())
      const actions = new input.Actions(executor, { async: true })
      let str = ''
      for (let i = 0; i < 501; i++) {
        str += i
      }
      const executionResult = await actions
        .sendKeys(str)
        .perform()
        .then(() => true)
        .catch(() => false)
      assert.strictEqual(executionResult, true)
    })
  })

  describe('click()', function () {
    it('clicks immediately if no element provided', async function () {
      const executor = new StubExecutor(Promise.resolve())
      const actions = new input.Actions(executor)

      await actions.click().perform()
      assert.deepStrictEqual(executor.commands, [
        {
          name: command.Name.ACTIONS,
          parameters: {
            actions: [
              {
                id: 'default mouse',
                type: 'pointer',
                parameters: { pointerType: 'mouse' },
                actions: [
                  {
                    type: 'pointerDown',
                    button: input.Button.LEFT,
                    altitudeAngle: 0,
                    azimuthAngle: 0,
                    width: 0,
                    height: 0,
                    pressure: 0,
                    tangentialPressure: 0,
                    tiltX: 0,
                    tiltY: 0,
                    twist: 0,
                  },
                  { type: 'pointerUp', button: input.Button.LEFT },
                ],
              },
            ],
          },
        },
      ])
    })

    it('moves to target element before clicking', async function () {
      const executor = new StubExecutor(Promise.resolve())
      const actions = new input.Actions(executor)

      const fakeElement = {}

      await actions.click(fakeElement).perform()
      assert.deepStrictEqual(executor.commands, [
        {
          name: command.Name.ACTIONS,
          parameters: {
            actions: [
              {
                id: 'default mouse',
                type: 'pointer',
                parameters: { pointerType: 'mouse' },
                actions: [
                  {
                    type: 'pointerMove',
                    origin: fakeElement,
                    duration: 100,
                    x: 0,
                    y: 0,
                    altitudeAngle: 0,
                    azimuthAngle: 0,
                    width: 0,
                    height: 0,
                    pressure: 0,
                    tangentialPressure: 0,
                    tiltX: 0,
                    tiltY: 0,
                    twist: 0,
                  },
                  {
                    type: 'pointerDown',
                    button: input.Button.LEFT,
                    altitudeAngle: 0,
                    azimuthAngle: 0,
                    width: 0,
                    height: 0,
                    pressure: 0,
                    tangentialPressure: 0,
                    tiltX: 0,
                    tiltY: 0,
                    twist: 0,
                  },
                  { type: 'pointerUp', button: input.Button.LEFT },
                ],
              },
            ],
          },
        },
      ])
    })

    it('synchronizes with other devices', async function () {
      const executor = new StubExecutor(Promise.resolve())
      const actions = new input.Actions(executor)

      const fakeElement = {}

      await actions.click(fakeElement).sendKeys('a').perform()
      assert.deepStrictEqual(executor.commands, [
        {
          name: command.Name.ACTIONS,
          parameters: {
            actions: [
              {
                id: 'default keyboard',
                type: 'key',
                actions: [
                  { type: 'pause', duration: 0 },
                  { type: 'pause', duration: 0 },
                  { type: 'pause', duration: 0 },
                  { type: 'keyDown', value: 'a' },
                  { type: 'keyUp', value: 'a' },
                ],
              },
              {
                id: 'default mouse',
                type: 'pointer',
                parameters: { pointerType: 'mouse' },
                actions: [
                  {
                    type: 'pointerMove',
                    origin: fakeElement,
                    duration: 100,
                    x: 0,
                    y: 0,
                    altitudeAngle: 0,
                    azimuthAngle: 0,
                    width: 0,
                    height: 0,
                    pressure: 0,
                    tangentialPressure: 0,
                    tiltX: 0,
                    tiltY: 0,
                    twist: 0,
                  },
                  {
                    type: 'pointerDown',
                    button: input.Button.LEFT,
                    altitudeAngle: 0,
                    azimuthAngle: 0,
                    width: 0,
                    height: 0,
                    pressure: 0,
                    tangentialPressure: 0,
                    tiltX: 0,
                    tiltY: 0,
                    twist: 0,
                  },
                  { type: 'pointerUp', button: input.Button.LEFT },
                  { type: 'pause', duration: 0 },
                  { type: 'pause', duration: 0 },
                ],
              },
            ],
          },
        },
      ])
    })
  })

  describe('dragAndDrop', function () {
    it('dragAndDrop(fromEl, toEl)', async function () {
      const executor = new StubExecutor(Promise.resolve())
      const actions = new input.Actions(executor)
      const e1 = new WebElement(null, 'abc123')
      const e2 = new WebElement(null, 'def456')

      await actions.dragAndDrop(e1, e2).perform()

      assert.deepStrictEqual(executor.commands, [
        {
          name: command.Name.ACTIONS,
          parameters: {
            actions: [
              {
                id: 'default mouse',
                type: 'pointer',
                parameters: { pointerType: 'mouse' },
                actions: [
                  {
                    type: 'pointerMove',
                    duration: 100,
                    origin: e1,
                    x: 0,
                    y: 0,
                    altitudeAngle: 0,
                    azimuthAngle: 0,
                    width: 0,
                    height: 0,
                    pressure: 0,
                    tangentialPressure: 0,
                    tiltX: 0,
                    tiltY: 0,
                    twist: 0,
                  },
                  {
                    type: 'pointerDown',
                    button: input.Button.LEFT,
                    altitudeAngle: 0,
                    azimuthAngle: 0,
                    width: 0,
                    height: 0,
                    pressure: 0,
                    tangentialPressure: 0,
                    tiltX: 0,
                    tiltY: 0,
                    twist: 0,
                  },
                  {
                    type: 'pointerMove',
                    duration: 100,
                    origin: e2,
                    x: 0,
                    y: 0,
                    altitudeAngle: 0,
                    azimuthAngle: 0,
                    width: 0,
                    height: 0,
                    pressure: 0,
                    tangentialPressure: 0,
                    tiltX: 0,
                    tiltY: 0,
                    twist: 0,
                  },
                  { type: 'pointerUp', button: input.Button.LEFT },
                ],
              },
            ],
          },
        },
      ])
    })

    it('dragAndDrop(el, offset)', async function () {
      const executor = new StubExecutor(Promise.resolve())
      const actions = new input.Actions(executor)
      const e1 = new WebElement(null, 'abc123')

      await actions.dragAndDrop(e1, { x: 30, y: 40 }).perform()

      assert.deepStrictEqual(executor.commands, [
        {
          name: command.Name.ACTIONS,
          parameters: {
            actions: [
              {
                id: 'default mouse',
                type: 'pointer',
                parameters: { pointerType: 'mouse' },
                actions: [
                  {
                    type: 'pointerMove',
                    duration: 100,
                    origin: e1,
                    x: 0,
                    y: 0,
                    altitudeAngle: 0,
                    azimuthAngle: 0,
                    width: 0,
                    height: 0,
                    pressure: 0,
                    tangentialPressure: 0,
                    tiltX: 0,
                    tiltY: 0,
                    twist: 0,
                  },
                  {
                    type: 'pointerDown',
                    button: input.Button.LEFT,
                    altitudeAngle: 0,
                    azimuthAngle: 0,
                    width: 0,
                    height: 0,
                    pressure: 0,
                    tangentialPressure: 0,
                    tiltX: 0,
                    tiltY: 0,
                    twist: 0,
                  },
                  {
                    type: 'pointerMove',
                    duration: 100,
                    origin: input.Origin.POINTER,
                    x: 30,
                    y: 40,
                    altitudeAngle: 0,
                    azimuthAngle: 0,
                    width: 0,
                    height: 0,
                    pressure: 0,
                    tangentialPressure: 0,
                    tiltX: 0,
                    tiltY: 0,
                    twist: 0,
                  },
                  { type: 'pointerUp', button: input.Button.LEFT },
                ],
              },
            ],
          },
        },
      ])
    })

    it('throws if target is invalid', async function () {
      const executor = new StubExecutor(Promise.resolve())
      const actions = new input.Actions(executor)
      const e = new WebElement(null, 'abc123')

      assert.throws(() => actions.dragAndDrop(e), error.InvalidArgumentError)
      assert.throws(() => actions.dragAndDrop(e, null), error.InvalidArgumentError)
      assert.throws(() => actions.dragAndDrop(e, {}), error.InvalidArgumentError)
      assert.throws(() => actions.dragAndDrop(e, { x: 0 }), error.InvalidArgumentError)
      assert.throws(() => actions.dragAndDrop(e, { y: 0 }), error.InvalidArgumentError)
      assert.throws(() => actions.dragAndDrop(e, { x: 0, y: 'a' }), error.InvalidArgumentError)
      assert.throws(() => actions.dragAndDrop(e, { x: 'a', y: 0 }), error.InvalidArgumentError)
    })
  })
})
