// 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 testutil = require('./testutil')
const promise = require('selenium-webdriver/lib/promise')

// Aliases for readability.
const NativePromise = Promise
const StubError = testutil.StubError
const assertIsStubError = testutil.assertIsStubError
const callbackPair = testutil.callbackPair
const throwStubError = testutil.throwStubError
const fail = () => assert.fail()

// Refer to promise_aplus_test for promise compliance with standard behavior.
describe('promise', function () {
  let app, uncaughtExceptions

  beforeEach(function setUp() {
    if (promise.USE_PROMISE_MANAGER) {
      promise.LONG_STACK_TRACES = false
      uncaughtExceptions = []

      app = promise.controlFlow()
      app.on(promise.ControlFlow.EventType.UNCAUGHT_EXCEPTION, (e) => uncaughtExceptions.push(e))
    }
  })

  afterEach(function tearDown() {
    if (promise.USE_PROMISE_MANAGER) {
      app.reset()
      promise.setDefaultFlow(new promise.ControlFlow())
      assert.deepStrictEqual([], uncaughtExceptions, 'Did not expect any uncaught exceptions')
      promise.LONG_STACK_TRACES = false
    }
  })

  it('isPromise', () => {
    const v = () => {}
    const x = new Promise(v, v)
    const p = createRejectedPromise('reject')
    const q = Promise.resolve('resolved')
    const t = {
      then() {},
    }
    const f = () => {}
    f.then = () => {}
    assert.equal(true, promise.isPromise(x))
    assert.equal(true, promise.isPromise(p))
    assert.equal(true, promise.isPromise(q))
    assert.equal(true, promise.isPromise(t))
    assert.equal(true, promise.isPromise(f))
    assert.equal(false, promise.isPromise(0))
    assert.equal(false, promise.isPromise(false))
    assert.equal(false, promise.isPromise(true))
    assert.equal(false, promise.isPromise(null))
    assert.equal(false, promise.isPromise(undefined))
    assert.equal(false, promise.isPromise(''))
    assert.equal(false, promise.isPromise('promise'))
    assert.equal(false, promise.isPromise(v))
  })

  function defer() {
    let d = {}
    let promise = new Promise((resolve, reject) => {
      Object.assign(d, { resolve, reject })
    })
    d.promise = promise
    return d
  }

  function createRejectedPromise(reason) {
    var p = Promise.reject(reason)
    p.catch(function () {}) // Silence unhandled rejection handlers.
    return p
  }

  describe('fullyResolved', function () {
    it('primitives', function () {
      function runTest(value) {
        return promise.fullyResolved(value).then((resolved) => assert.strictEqual(value, resolved))
      }

      return runTest(true)
        .then(() => runTest(function () {}))
        .then(() => runTest(null))
        .then(() => runTest(123))
        .then(() => runTest('foo bar'))
        .then(() => runTest(undefined))
    })

    it('arrayOfPrimitives', function () {
      var fn = function () {}
      const array = [true, fn, null, 123, '', undefined, 1]
      return promise.fullyResolved(array).then(function (resolved) {
        assert.strictEqual(array, resolved)
        assert.deepStrictEqual([true, fn, null, 123, '', undefined, 1], resolved)
      })
    })

    it('nestedArrayOfPrimitives', function () {
      var fn = function () {}
      const array = [true, [fn, null, 123], '', undefined]
      return promise.fullyResolved(array).then(function (resolved) {
        assert.strictEqual(array, resolved)
        assert.deepStrictEqual([true, [fn, null, 123], '', undefined], resolved)
        assert.deepStrictEqual([fn, null, 123], resolved[1])
      })
    })

    it('arrayWithPromisedPrimitive', function () {
      return promise.fullyResolved([Promise.resolve(123)]).then(function (resolved) {
        assert.deepStrictEqual([123], resolved)
      })
    })

    it('promiseResolvesToPrimitive', function () {
      return promise.fullyResolved(Promise.resolve(123)).then((resolved) => assert.strictEqual(123, resolved))
    })

    it('promiseResolvesToArray', function () {
      var fn = function () {}
      const array = [true, [fn, null, 123], '', undefined]
      const aPromise = Promise.resolve(array)

      var result = promise.fullyResolved(aPromise)
      return result.then(function (resolved) {
        assert.strictEqual(array, resolved)
        assert.deepStrictEqual([true, [fn, null, 123], '', undefined], resolved)
        assert.deepStrictEqual([fn, null, 123], resolved[1])
      })
    })

    it('promiseResolvesToArrayWithPromises', function () {
      var nestedPromise = Promise.resolve(123)
      const aPromise = Promise.resolve([true, nestedPromise])
      return promise.fullyResolved(aPromise).then(function (resolved) {
        assert.deepStrictEqual([true, 123], resolved)
      })
    })

    it('rejectsIfArrayPromiseRejects', function () {
      var nestedPromise = createRejectedPromise(new StubError())
      const aPromise = Promise.resolve([true, nestedPromise])

      return promise.fullyResolved(aPromise).then(assert.fail, assertIsStubError)
    })

    it('rejectsOnFirstArrayRejection', function () {
      var e1 = new Error('foo')
      var e2 = new Error('bar')
      const aPromise = Promise.resolve([createRejectedPromise(e1), createRejectedPromise(e2)])

      return promise.fullyResolved(aPromise).then(assert.fail, function (error) {
        assert.strictEqual(e1, error)
      })
    })

    it('rejectsIfNestedArrayPromiseRejects', function () {
      const aPromise = Promise.resolve([Promise.resolve([createRejectedPromise(new StubError())])])

      return promise.fullyResolved(aPromise).then(assert.fail, assertIsStubError)
    })

    it('simpleHash', function () {
      var hash = { a: 123 }
      return promise.fullyResolved(hash).then(function (resolved) {
        assert.strictEqual(hash, resolved)
        assert.deepStrictEqual(hash, { a: 123 })
      })
    })

    it('nestedHash', function () {
      var nestedHash = { foo: 'bar' }
      var hash = { a: 123, b: nestedHash }

      return promise.fullyResolved(hash).then(function (resolved) {
        assert.strictEqual(hash, resolved)
        assert.deepStrictEqual({ a: 123, b: { foo: 'bar' } }, resolved)
        assert.strictEqual(nestedHash, resolved['b'])
      })
    })

    it('promiseResolvesToSimpleHash', function () {
      var hash = { a: 123 }
      const aPromise = Promise.resolve(hash)

      return promise.fullyResolved(aPromise).then((resolved) => assert.strictEqual(hash, resolved))
    })

    it('promiseResolvesToNestedHash', function () {
      var nestedHash = { foo: 'bar' }
      var hash = { a: 123, b: nestedHash }
      const aPromise = Promise.resolve(hash)

      return promise.fullyResolved(aPromise).then(function (resolved) {
        assert.strictEqual(hash, resolved)
        assert.strictEqual(nestedHash, resolved['b'])
        assert.deepStrictEqual(hash, { a: 123, b: { foo: 'bar' } })
      })
    })

    it('promiseResolvesToHashWithPromises', function () {
      const aPromise = Promise.resolve({
        a: Promise.resolve(123),
      })

      return promise.fullyResolved(aPromise).then(function (resolved) {
        assert.deepStrictEqual({ a: 123 }, resolved)
      })
    })

    it('rejectsIfHashPromiseRejects', function () {
      const aPromise = Promise.resolve({
        a: createRejectedPromise(new StubError()),
      })

      return promise.fullyResolved(aPromise).then(assert.fail, assertIsStubError)
    })

    it('rejectsIfNestedHashPromiseRejects', function () {
      const aPromise = Promise.resolve({
        a: { b: createRejectedPromise(new StubError()) },
      })

      return promise.fullyResolved(aPromise).then(assert.fail, assertIsStubError)
    })

    it('instantiatedObject', function () {
      function Foo() {
        this.bar = 'baz'
      }

      var foo = new Foo()

      return promise.fullyResolved(foo).then(function (resolvedFoo) {
        assert.strictEqual(foo, resolvedFoo)
        assert.ok(resolvedFoo instanceof Foo)
        assert.deepStrictEqual(new Foo(), resolvedFoo)
      })
    })

    it('withEmptyArray', function () {
      return promise.fullyResolved([]).then(function (resolved) {
        assert.deepStrictEqual([], resolved)
      })
    })

    it('withEmptyHash', function () {
      return promise.fullyResolved({}).then(function (resolved) {
        assert.deepStrictEqual({}, resolved)
      })
    })

    it('arrayWithPromisedHash', function () {
      var obj = { foo: 'bar' }
      const array = [Promise.resolve(obj)]

      return promise.fullyResolved(array).then(function (resolved) {
        assert.deepStrictEqual(resolved, [obj])
      })
    })
  })

  describe('finally', function () {
    it('successful callback does not suppress original error', async () => {
      let p = Promise.reject(new StubError())
      let called = false

      try {
        await promise.finally(p, function () {
          called = true
        })
        fail('should have thrown')
      } catch (e) {
        assertIsStubError(e)
        assert.ok(called)
      }
    })

    it('failing callback suppresses original error', async () => {
      let p = Promise.reject(Error('original'))

      try {
        await promise.finally(p, throwStubError)
        fail('should have thrown')
      } catch (e) {
        assertIsStubError(e)
      }
    })

    it('callback throws after fulfilled promise', async () => {
      try {
        await promise.finally(Promise.resolve(), throwStubError)
        fail('should have thrown')
      } catch (e) {
        assertIsStubError(e)
      }
    })

    it('callback returns rejected promise', async () => {
      try {
        await promise.finally(Promise.resolve(), () => Promise.reject(new StubError()))
        fail('should have thrown')
      } catch (e) {
        assertIsStubError(e)
      }
    })

    it('returned promise resolves with callback result', async () => {
      let value = await promise.finally(Promise.resolve(1), () => 2)
      assert.strictEqual(value, 2)
    })
  })

  describe('checkedNodeCall', function () {
    it('functionThrows', function () {
      return promise.checkedNodeCall(throwStubError).then(assert.fail, assertIsStubError)
    })

    it('functionReturnsAnError', function () {
      return promise
        .checkedNodeCall(function (callback) {
          callback(new StubError())
        })
        .then(assert.fail, assertIsStubError)
    })

    it('functionReturnsSuccess', function () {
      var success = 'success!'
      return promise
        .checkedNodeCall(function (callback) {
          callback(null, success)
        })
        .then((value) => assert.strictEqual(success, value))
    })

    it('functionReturnsAndThrows', function () {
      var error = new Error('boom')
      var error2 = new Error('boom again')
      return promise
        .checkedNodeCall(function (callback) {
          callback(error)
          throw error2
        })
        .then(assert.fail, (e) => assert.strictEqual(error, e))
    })

    it('functionThrowsAndReturns', function () {
      var error = new Error('boom')
      var error2 = new Error('boom again')
      return promise
        .checkedNodeCall(function (callback) {
          setTimeout(() => callback(error), 10)
          throw error2
        })
        .then(assert.fail, (e) => assert.strictEqual(error2, e))
    })
  })

  describe('map', function () {
    it('(base case)', function () {
      const a = [1, 2, 3]
      return promise
        .map(a, function (value, index, a2) {
          assert.strictEqual(a, a2)
          assert.strictEqual('number', typeof index, 'not a number')
          return value + 1
        })
        .then(function (value) {
          assert.deepStrictEqual([2, 3, 4], value)
        })
    })

    it('omitsDeleted', function () {
      const a = [0, 1, 2, 3, 4, 5, 6]
      delete a[1]
      delete a[3]
      delete a[4]
      delete a[6]

      const expected = [0, NaN, 4, NaN, NaN, 25, NaN]

      return promise
        .map(a, function (value) {
          return value * value
        })
        .then(function (value) {
          assert.deepStrictEqual(expected, value)
        })
    })

    it('emptyArray', function () {
      return promise
        .map([], function (value) {
          return value + 1
        })
        .then(function (value) {
          assert.deepStrictEqual([], value)
        })
    })

    it('inputIsPromise', function () {
      var input = defer()
      var result = promise.map(input.promise, function (value) {
        return value + 1
      })

      var pair = callbackPair(function (value) {
        assert.deepStrictEqual([2, 3, 4], value)
      })
      result = result.then(pair.callback, pair.errback)

      setTimeout(function () {
        pair.assertNeither()
        input.resolve([1, 2, 3])
      }, 10)

      return result
    })

    it('waitsForFunctionResultToResolve', function () {
      var innerResults = [defer(), defer()]

      var result = promise.map([1, 2], function (_value, index) {
        return innerResults[index].promise
      })

      var pair = callbackPair(function (value) {
        assert.deepStrictEqual(['a', 'b'], value)
      })
      result = result.then(pair.callback, pair.errback)

      return NativePromise.resolve()
        .then(function () {
          pair.assertNeither()
          innerResults[0].resolve('a')
        })
        .then(function () {
          pair.assertNeither()
          innerResults[1].resolve('b')
          return result
        })
        .then(pair.assertCallback)
    })

    it('rejectsPromiseIfFunctionThrows', function () {
      return promise.map([1], throwStubError).then(assert.fail, assertIsStubError)
    })

    it('rejectsPromiseIfFunctionReturnsRejectedPromise', function () {
      return promise
        .map([1], function () {
          return createRejectedPromise(new StubError())
        })
        .then(assert.fail, assertIsStubError)
    })

    it('stopsCallingFunctionIfPreviousIterationFailed', function () {
      var count = 0
      return promise
        .map([1, 2, 3, 4], function () {
          count++
          if (count === 3) {
            throw new StubError()
          }
        })
        .then(assert.fail, function (e) {
          assertIsStubError(e)
          assert.strictEqual(3, count)
        })
    })

    it('rejectsWithFirstRejectedPromise', function () {
      var innerResult = [
        Promise.resolve(),
        createRejectedPromise(new StubError()),
        createRejectedPromise(Error('should be ignored')),
      ]
      var count = 0
      return promise
        .map([1, 2, 3, 4], function (_value, index) {
          count += 1
          return innerResult[index]
        })
        .then(assert.fail, function (e) {
          assertIsStubError(e)
          assert.strictEqual(2, count)
        })
    })

    it('preservesOrderWhenMapReturnsPromise', function () {
      var deferreds = [defer(), defer(), defer(), defer()]
      var result = promise.map(deferreds, function (value) {
        return value.promise
      })

      var pair = callbackPair(function (value) {
        assert.deepStrictEqual([0, 1, 2, 3], value)
      })
      result = result.then(pair.callback, pair.errback)

      return Promise.resolve()
        .then(function () {
          pair.assertNeither()
          for (let i = deferreds.length; i > 0; i -= 1) {
            deferreds[i - 1].resolve(i - 1)
          }
          return result
        })
        .then(pair.assertCallback)
    })
  })

  describe('filter', function () {
    it('basicFiltering', function () {
      const a = [0, 1, 2, 3]
      return promise
        .filter(a, function (val, index, a2) {
          assert.strictEqual(a, a2)
          assert.strictEqual('number', typeof index, 'not a number')
          return val > 1
        })
        .then(function (val) {
          assert.deepStrictEqual([2, 3], val)
        })
    })

    it('omitsDeleted', function () {
      const a = [0, 1, 2, 3, 4, 5, 6]
      delete a[3]
      delete a[4]

      return promise
        .filter(a, function (value) {
          return value > 1 && value < 6
        })
        .then(function (val) {
          assert.deepStrictEqual([2, 5], val)
        })
    })

    it('preservesInputs', function () {
      const a = [0, 1, 2, 3]

      return promise
        .filter(a, function (_value, i, a2) {
          assert.strictEqual(a, a2)
          // Even if a function modifies the input array, the original value
          // should be inserted into the new array.
          a2[i] = a2[i] - 1
          return a2[i] >= 1
        })
        .then(function (val) {
          assert.deepStrictEqual([2, 3], val)
        })
    })

    it('inputIsPromise', function () {
      const input = defer()
      let result = promise.filter(input.promise, function (value) {
        return value > 1 && value < 3
      })

      const pair = callbackPair(function (value) {
        assert.deepStrictEqual([2], value)
      })
      result = result.then(pair.callback, pair.errback)
      return NativePromise.resolve()
        .then(function () {
          pair.assertNeither()
          input.resolve([1, 2, 3])
          return result
        })
        .then(pair.assertCallback)
    })

    it('waitsForFunctionResultToResolve', function () {
      const innerResults = [defer(), defer()]

      let result = promise.filter([1, 2], function (_value, index) {
        return innerResults[index].promise
      })

      const pair = callbackPair(function (value) {
        assert.deepStrictEqual([2], value)
      })
      result = result.then(pair.callback, pair.errback)
      return NativePromise.resolve()
        .then(function () {
          pair.assertNeither()
          innerResults[0].resolve(false)
        })
        .then(function () {
          pair.assertNeither()
          innerResults[1].resolve(true)
          return result
        })
        .then(pair.assertCallback)
    })

    it('rejectsPromiseIfFunctionReturnsRejectedPromise', function () {
      return promise
        .filter([1], function () {
          return createRejectedPromise(new StubError())
        })
        .then(assert.fail, assertIsStubError)
    })

    it('stopsCallingFunctionIfPreviousIterationFailed', function () {
      var count = 0
      return promise
        .filter([1, 2, 3, 4], function () {
          count++
          if (count === 3) {
            throw new StubError()
          }
        })
        .then(assert.fail, function (e) {
          assertIsStubError(e)
          assert.strictEqual(3, count)
        })
    })

    it('rejectsWithFirstRejectedPromise', function () {
      var innerResult = [
        Promise.resolve(),
        createRejectedPromise(new StubError()),
        createRejectedPromise(Error('should be ignored')),
      ]

      return promise
        .filter([1, 2, 3, 4], function (_value, index) {
          assert.ok(index < innerResult.length)
          return innerResult[index]
        })
        .then(assert.fail, assertIsStubError)
    })

    it('preservesOrderWhenFilterReturnsPromise', function () {
      const deferreds = [defer(), defer(), defer(), defer()]
      let result = promise.filter([0, 1, 2, 3], function (_value, index) {
        return deferreds[index].promise
      })

      const pair = callbackPair(function (value) {
        assert.deepStrictEqual([1, 2], value)
      })
      result = result.then(pair.callback, pair.errback)

      return NativePromise.resolve()
        .then(function () {
          pair.assertNeither()
          for (let i = deferreds.length - 1; i >= 0; i -= 1) {
            deferreds[i].resolve(i > 0 && i < 3)
          }
          return result
        })
        .then(pair.assertCallback)
    })
  })
})
