/*! @license
 * Shaka Player
 * Copyright 2016 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

describe('AbortableOperation', () => {
  const Util = shaka.test.Util;

  describe('promise', () => {
    it('is resolved by the constructor argument', async () => {
      const promise = new shaka.util.PublicPromise();
      const abort = () => Promise.resolve();

      const operation = new shaka.util.AbortableOperation(promise, abort);
      promise.resolve(100);

      const value = await operation.promise;
      expect(value).toBe(100);
    });
  });

  describe('abort', () => {
    it('calls the abort argument from the constructor', () => {
      const promise = Promise.resolve();
      const abort =
          jasmine.createSpy('abort').and.returnValue(Promise.resolve());

      const operation = new shaka.util.AbortableOperation(
          promise, shaka.test.Util.spyFunc(abort));
      operation.abort();
      expect(abort).toHaveBeenCalled();
    });

    it('is resolved when the underlying abort() is resolved', async () => {
      /** @type {!shaka.util.PublicPromise} */
      const p = new shaka.util.PublicPromise();
      const abort = jasmine.createSpy('abort').and.returnValue(p);

      const operation = new shaka.util.AbortableOperation(
          new shaka.util.PublicPromise(), shaka.test.Util.spyFunc(abort));

      const abortComplete = jasmine.createSpy('abort complete');
      operation.abort().then(shaka.test.Util.spyFunc(abortComplete), fail);

      expect(abortComplete).not.toHaveBeenCalled();
      await shaka.test.Util.shortDelay();
      // Nothing has happened yet, so abort is not complete.
      expect(abortComplete).not.toHaveBeenCalled();
      // Resolve the underlying Promise.
      p.resolve();

      await shaka.test.Util.shortDelay();
      // The abort is now complete.
      expect(abortComplete).toHaveBeenCalled();
    });
  });

  describe('failed', () => {
    it('creates a failed operation with the given error', async () => {
      const error = new shaka.util.Error(
          shaka.util.Error.Severity.RECOVERABLE,
          shaka.util.Error.Category.NETWORK,
          shaka.util.Error.Code.MALFORMED_DATA_URI);

      const operation = shaka.util.AbortableOperation.failed(error);
      await expectAsync(operation.promise)
          .toBeRejectedWith(Util.jasmineError(error));
    });
  });

  describe('aborted', () => {
    it('creates a failed operation with OPERATION_ABORTED', async () => {
      const error = Util.jasmineError(new shaka.util.Error(
          shaka.util.Error.Severity.CRITICAL,
          shaka.util.Error.Category.PLAYER,
          shaka.util.Error.Code.OPERATION_ABORTED));

      const operation = shaka.util.AbortableOperation.aborted();
      await expectAsync(operation.promise).toBeRejectedWith(error);
    });
  });

  describe('completed', () => {
    it('creates a completed operation with the given value', async () => {
      const operation = shaka.util.AbortableOperation.completed(100);
      const value = await operation.promise;
      expect(value).toBe(100);
    });
  });

  describe('notAbortable', () => {
    it('creates an operation from the given promise', async () => {
      /** @type {!shaka.util.PublicPromise} */
      const promise = new shaka.util.PublicPromise();
      const operation = shaka.util.AbortableOperation.notAbortable(promise);

      let isAborted = false;
      operation.abort().then(() => {
        isAborted = true;
      });

      let isComplete = false;
      operation.promise.catch(fail).then((value) => {
        isComplete = true;
        expect(value).toBe(100);
      });

      await shaka.test.Util.shortDelay();
      // Even though we called abort(), the operation hasn't completed
      // because it isn't abortable.  The abort() Promise hasn't been
      // resolved yet, either.
      expect(isComplete).toBe(false);
      expect(isAborted).toBe(false);

      promise.resolve(100);
      await shaka.test.Util.shortDelay();

      // Now that we resolved the underlying promise, the operation is
      // complete, and so is the abort() Promise.
      expect(isComplete).toBe(true);
      expect(isAborted).toBe(true);
    });
  });  // describe('notAbortable')

  describe('all', () => {
    it('creates a successful operation when all succeed', async () => {
      /** @type {!shaka.util.PublicPromise} */
      const p1 = new shaka.util.PublicPromise();
      const op1 = shaka.util.AbortableOperation.notAbortable(p1);

      /** @type {!shaka.util.PublicPromise} */
      const p2 = new shaka.util.PublicPromise();
      const op2 = shaka.util.AbortableOperation.notAbortable(p2);

      /** @type {!shaka.util.PublicPromise} */
      const p3 = new shaka.util.PublicPromise();
      const op3 = shaka.util.AbortableOperation.notAbortable(p3);

      const all = shaka.util.AbortableOperation.all([op1, op2, op3]);

      const onSuccessSpy = jasmine.createSpy('onSuccess');
      const onSuccess = shaka.test.Util.spyFunc(onSuccessSpy);
      const onErrorSpy = jasmine.createSpy('onError');
      const onError = shaka.test.Util.spyFunc(onErrorSpy);

      all.promise.then(onSuccess, onError);

      await shaka.test.Util.shortDelay();
      expect(onSuccessSpy).not.toHaveBeenCalled();
      expect(onErrorSpy).not.toHaveBeenCalled();
      p1.resolve();
      await shaka.test.Util.shortDelay();

      expect(onSuccessSpy).not.toHaveBeenCalled();
      expect(onErrorSpy).not.toHaveBeenCalled();
      p2.resolve();
      await shaka.test.Util.shortDelay();

      expect(onSuccessSpy).not.toHaveBeenCalled();
      expect(onErrorSpy).not.toHaveBeenCalled();
      p3.resolve();
      await shaka.test.Util.shortDelay();

      expect(onSuccessSpy).toHaveBeenCalled();
      expect(onErrorSpy).not.toHaveBeenCalled();
    });

    it('creates a failed operation when any fail', async () => {
      /** @type {!shaka.util.PublicPromise} */
      const p1 = new shaka.util.PublicPromise();
      const op1 = shaka.util.AbortableOperation.notAbortable(p1);

      /** @type {!shaka.util.PublicPromise} */
      const p2 = new shaka.util.PublicPromise();
      const op2 = shaka.util.AbortableOperation.notAbortable(p2);

      const p3 = new shaka.util.PublicPromise();
      const op3 = shaka.util.AbortableOperation.notAbortable(p3);

      const all = shaka.util.AbortableOperation.all([op1, op2, op3]);

      const onSuccessSpy = jasmine.createSpy('onSuccess');
      const onSuccess = shaka.test.Util.spyFunc(onSuccessSpy);
      const onErrorSpy = jasmine.createSpy('onError');
      const onError = shaka.test.Util.spyFunc(onErrorSpy);

      all.promise.then(onSuccess, onError);

      await shaka.test.Util.shortDelay();
      expect(onSuccessSpy).not.toHaveBeenCalled();
      expect(onErrorSpy).not.toHaveBeenCalled();
      p1.resolve();
      await shaka.test.Util.shortDelay();

      expect(onSuccessSpy).not.toHaveBeenCalled();
      expect(onErrorSpy).not.toHaveBeenCalled();
      p2.reject('error');
      await shaka.test.Util.shortDelay();

      expect(onSuccessSpy).not.toHaveBeenCalled();
      expect(onErrorSpy).toHaveBeenCalledWith('error');
    });

    it('aborts all operations on abort', async () => {
      /** @type {!shaka.util.PublicPromise} */
      const p1 = new shaka.util.PublicPromise();
      const abort1Spy = jasmine.createSpy('abort1')
          .and.callFake(() => p1.reject());
      const abort1 = shaka.test.Util.spyFunc(abort1Spy);
      const op1 = new shaka.util.AbortableOperation(p1, abort1);

      /** @type {!shaka.util.PublicPromise} */
      const p2 = new shaka.util.PublicPromise();
      const abort2Spy = jasmine.createSpy('abort2')
          .and.callFake(() => p2.reject());
      const abort2 = shaka.test.Util.spyFunc(abort2Spy);
      const op2 = new shaka.util.AbortableOperation(p2, abort2);

      /** @type {!shaka.util.PublicPromise} */
      const p3 = new shaka.util.PublicPromise();
      const abort3Spy = jasmine.createSpy('abort3')
          .and.callFake(() => p3.reject());
      const abort3 = shaka.test.Util.spyFunc(abort3Spy);
      const op3 = new shaka.util.AbortableOperation(p3, abort3);

      /** @type {!shaka.util.AbortableOperation} */
      const all = shaka.util.AbortableOperation.all([op1, op2, op3]);

      const onSuccessSpy = jasmine.createSpy('onSuccess');
      const onSuccess = shaka.test.Util.spyFunc(onSuccessSpy);
      const onErrorSpy = jasmine.createSpy('onError');
      const onError = shaka.test.Util.spyFunc(onErrorSpy);

      all.promise.then(onSuccess, onError);

      await shaka.test.Util.shortDelay();
      expect(onSuccessSpy).not.toHaveBeenCalled();
      expect(onErrorSpy).not.toHaveBeenCalled();

      expect(abort1Spy).not.toHaveBeenCalled();
      expect(abort2Spy).not.toHaveBeenCalled();
      expect(abort3Spy).not.toHaveBeenCalled();

      all.abort();
      await shaka.test.Util.shortDelay();

      expect(onSuccessSpy).not.toHaveBeenCalled();
      expect(onErrorSpy).toHaveBeenCalled();

      expect(abort1Spy).toHaveBeenCalled();
      expect(abort2Spy).toHaveBeenCalled();
      expect(abort3Spy).toHaveBeenCalled();
    });
  });  // describe('all')

  describe('finally', () => {
    it('executes after the operation is successful', async () => {
      let isDone = false;
      /** @type {!shaka.util.PublicPromise} */
      const promise = new shaka.util.PublicPromise();

      shaka.util.AbortableOperation.notAbortable(promise).finally((ok) => {
        expect(ok).toBe(true);
        isDone = true;
      });

      await shaka.test.Util.shortDelay();
      expect(isDone).toBe(false);
      promise.resolve(100);

      await shaka.test.Util.shortDelay();
      expect(isDone).toBe(true);
    });

    it('executes after the operation fails', async () => {
      let isDone = false;
      /** @type {!shaka.util.PublicPromise} */
      const promise = new shaka.util.PublicPromise();

      shaka.util.AbortableOperation.notAbortable(promise).finally((ok) => {
        expect(ok).toBe(false);
        isDone = true;
      });

      await shaka.test.Util.shortDelay();
      expect(isDone).toBe(false);
      promise.reject(0);

      await shaka.test.Util.shortDelay();
      expect(isDone).toBe(true);
    });

    it('executes after the chain is successful', async () => {
      let isDone = false;
      /** @type {!shaka.util.PublicPromise} */
      const promise1 = new shaka.util.PublicPromise();
      /** @type {!shaka.util.PublicPromise} */
      const promise2 = new shaka.util.PublicPromise();

      shaka.util.AbortableOperation.notAbortable(promise1).chain(() => {
        return shaka.util.AbortableOperation.notAbortable(promise2);
      }).finally((ok) => {
        expect(ok).toBe(true);
        isDone = true;
      });

      await shaka.test.Util.shortDelay();
      expect(isDone).toBe(false);
      promise1.resolve();
      await shaka.test.Util.shortDelay();

      expect(isDone).toBe(false);
      promise2.resolve();
      await shaka.test.Util.shortDelay();

      expect(isDone).toBe(true);
    });

    it('executes after the chain fails', async () => {
      let isDone = false;
      /** @type {!shaka.util.PublicPromise} */
      const promise1 = new shaka.util.PublicPromise();
      const promise2 = new shaka.util.PublicPromise();

      shaka.util.AbortableOperation.notAbortable(promise1).chain(() => {
        return shaka.util.AbortableOperation.notAbortable(promise2);
      }).finally((ok) => {
        expect(ok).toBe(false);
        isDone = true;
      });

      await shaka.test.Util.shortDelay();
      expect(isDone).toBe(false);
      promise1.reject(0);
      await shaka.test.Util.shortDelay();

      expect(isDone).toBe(true);
    });

    it('executes after a complex chain', async () => {
      let isDone = false;

      shaka.util.AbortableOperation.completed(0).chain(() => {
        return shaka.util.AbortableOperation.aborted();
      }).chain(() => {
        fail('Should not be reachable');
      }, (e) => {
        return shaka.util.AbortableOperation.completed(100);
      }).finally((ok) => {
        expect(ok).toBe(true);
        isDone = true;
      });

      await shaka.test.Util.shortDelay();
      expect(isDone).toBe(true);
    });
  });  // describe('finally')

  describe('chain', () => {
    it('passes the value to the next operation on success', async () => {
      /** @type {!Array.<number>} */
      const values = [];

      const op = shaka.util.AbortableOperation.completed(100).chain((value) => {
        values.push(value);
        expect(value).toBe(100);
        // Plain value
        return 200;
      }).chain((value) => {
        values.push(value);
        expect(value).toBe(200);
        // Resolved Promise
        return Promise.resolve(300);
      }).chain((value) => {
        values.push(value);
        expect(value).toBe(300);
        // Delayed Promise
        return shaka.test.Util.shortDelay().then(() => 400);
      }).chain((value) => {
        values.push(value);
        expect(value).toBe(400);
        // Abortable operation
        return shaka.util.AbortableOperation.completed(500);
      }).chain((value) => {
        values.push(value);
        expect(value).toBe(500);
      }).finally((ok) => {
        expect(ok).toBe(true);
        // The bug https://github.com/google/shaka-player/issues/1260 makes this
        // expectation fail because some stages were skipped.  Without this
        // check, the test would pass, even though the bug shows up first in the
        // basic functionality of 'chain'.
        expect(values).toEqual([100, 200, 300, 400, 500]);
      });
      await op.promise;
    });

    it('skips the onSuccess callbacks on error', async () => {
      const error = new shaka.util.Error(
          shaka.util.Error.Severity.RECOVERABLE,
          shaka.util.Error.Category.NETWORK,
          shaka.util.Error.Code.MALFORMED_DATA_URI);

      const op = shaka.util.AbortableOperation.failed(error)
          .chain(fail, (e) => {
            shaka.test.Util.expectToEqualError(e, error);
            throw error;  // rethrow
          }).chain(fail, (e) => {
            shaka.test.Util.expectToEqualError(e, error);
            throw error;  // rethrow
          }).chain(fail, (e) => {
            shaka.test.Util.expectToEqualError(e, error);
          }).finally((ok) => {
            expect(ok).toBe(true);  // Last stage did not rethrow
          });
      await op.promise;
    });

    it('can fall back to other operations in onError callback', async () => {
      const error1 = new shaka.util.Error(
          shaka.util.Error.Severity.RECOVERABLE,
          shaka.util.Error.Category.NETWORK,
          shaka.util.Error.Code.MALFORMED_DATA_URI);
      const error2 = new shaka.util.Error(
          shaka.util.Error.Severity.RECOVERABLE,
          shaka.util.Error.Category.TEXT,
          shaka.util.Error.Code.INVALID_XML);
      const error3 = new shaka.util.Error(
          shaka.util.Error.Severity.RECOVERABLE,
          shaka.util.Error.Category.MEDIA,
          shaka.util.Error.Code.EBML_BAD_FLOATING_POINT_SIZE);

      const op = shaka.util.AbortableOperation.failed(error1)
          .chain(fail, (e) => {
            shaka.test.Util.expectToEqualError(e, error1);
            return shaka.util.AbortableOperation.failed(error2);
          }).chain(fail, (e) => {
            shaka.test.Util.expectToEqualError(e, error2);
            return shaka.util.AbortableOperation.failed(error3);
          }).chain(fail, (e) => {
            shaka.test.Util.expectToEqualError(e, error3);
            return shaka.util.AbortableOperation.completed(400);
          }).chain((value) => {
            expect(value).toBe(400);
          }).finally((ok) => {
            expect(ok).toBe(true);
          });
      await op.promise;
    });

    it('fails when an error is thrown', async () => {
      const error1 = new shaka.util.Error(
          shaka.util.Error.Severity.RECOVERABLE,
          shaka.util.Error.Category.NETWORK,
          shaka.util.Error.Code.MALFORMED_DATA_URI);
      const error2 = new shaka.util.Error(
          shaka.util.Error.Severity.RECOVERABLE,
          shaka.util.Error.Category.TEXT,
          shaka.util.Error.Code.INVALID_XML);

      const op = shaka.util.AbortableOperation.completed(100).chain((value) => {
        throw error1;
      }).chain(fail, (e) => {
        shaka.test.Util.expectToEqualError(e, error1);
        throw error2;
      }).chain(fail, (e) => {
        shaka.test.Util.expectToEqualError(e, error2);
      }).finally((ok) => {
        expect(ok).toBe(true);  // Last stage did not rethrow
      });
      await op.promise;
    });

    it('goes to success state when onError returns undefined', async () => {
      const error = new shaka.util.Error(
          shaka.util.Error.Severity.RECOVERABLE,
          shaka.util.Error.Category.NETWORK,
          shaka.util.Error.Code.MALFORMED_DATA_URI);

      const op = shaka.util.AbortableOperation.failed(error)
          .chain(fail, (e) => {
            shaka.test.Util.expectToEqualError(e, error);
            // no return value
          }).chain((value) => {
            expect(value).toBe(undefined);
          }, fail).finally((ok) => {
            expect(ok).toBe(true);
          });
      await op.promise;
    });

    it('does not need return when onSuccess omitted', async () => {
      const operation = shaka.util.AbortableOperation.completed(100)
          .chain(undefined, fail).chain(undefined, fail).chain((value) => {
            expect(value).toBe(100);
          }).finally((ok) => {
            expect(ok).toBe(true);
          });
      await operation.promise;
    });

    it('does not need rethrow when onError omitted', async () => {
      const error = new shaka.util.Error(
          shaka.util.Error.Severity.RECOVERABLE,
          shaka.util.Error.Category.NETWORK,
          shaka.util.Error.Code.MALFORMED_DATA_URI);

      const operation = shaka.util.AbortableOperation.failed(error)
          .chain(fail).chain(fail).chain(fail).chain(fail, (e) => {
            shaka.test.Util.expectToEqualError(e, error);
          }).finally((ok) => {
            expect(ok).toBe(true);  // Last stage did not rethrow
          });
      await operation.promise;
    });

    it('ensures abort is called with the correct "this"', async () => {
      // During testing and development, an early version of chain() would
      // sometimes unbind an abort method from an earlier stage of the chain.
      // Make sure this doesn't happen.
      let innerOperation;
      /** @type {!shaka.util.PublicPromise} */
      const p = new shaka.util.PublicPromise();
      let abortCalled = false;

      /**
       * NOTE: This is a subtle thing, but this must be an ES5 anonymous
       * function for the test to work.  ES6 arrow functions would always be
       * called with the "this" of the test itself, regardless of what the
       * library is doing.
       *
       * @this {shaka.util.AbortableOperation}
       * @return {!Promise}
       */
      function abort() {
        expect(this).toBe(innerOperation);
        abortCalled = true;
        return Promise.resolve();
      }

      // Since the issue was with the calling of operation.abort, rather than
      // the onAbort_ callback, we make an operation-like thing instead of using
      // the AbortableOperation constructor.
      innerOperation = {promise: p, abort: abort};

      // The second stage of the chain returns innerOperation.  A brief moment
      // later, the outer chain is aborted.
      const operation =
          shaka.util.AbortableOperation.completed(100)
              .chain(() => {
                shaka.test.Util.shortDelay().then(() => {
                  operation.abort();
                  p.resolve();
                });
                return innerOperation;
              })
              .finally((ok) => {
                // We resolved the non-abortable inner operation
                expect(ok).toBe(true);
                expect(abortCalled).toBe(true);
              });
      await operation.promise;
    });

    it('aborts nested AbortableOperation objects', async () => {
      /** @type {!shaka.util.PublicPromise} */
      const promise = new shaka.util.PublicPromise();
      const abort = jasmine.createSpy('abort');

      /** @type {!shaka.util.AbortableOperation} */
      const operation = shaka.util.AbortableOperation.completed(0).chain(() => {
        return shaka.util.AbortableOperation.completed(1)
            .chain(() => 2)
            .chain(() => {
              return new shaka.util.AbortableOperation(
                  promise, Util.spyFunc(abort));
            })
            .chain(() => 3);
      });
      await Util.shortDelay();  // Ensure we are waiting on the "promise".
      operation.abort();
      promise.resolve();
      await expectAsync(operation.promise).toBeRejected();
      expect(abort).toHaveBeenCalled();
    });

    it('aborts even with a failure callback', async () => {
      /** @type {!shaka.util.PublicPromise} */
      const promise = new shaka.util.PublicPromise();

      /** @type {!shaka.util.AbortableOperation} */
      const operation = shaka.util.AbortableOperation.completed(0)
          .chain(() => promise)
          .chain(
              () => fail('Promise should be rejected'),
              () => {});
      await Util.shortDelay();  // Ensure we are waiting on the "promise".
      operation.abort();
      promise.reject();
      await expectAsync(operation.promise).toBeRejected();
    });

    it('abort waits for plain Promise to be resolved', async () => {
      /** @type {!shaka.util.PublicPromise} */
      const promise = new shaka.util.PublicPromise();
      const resolved = jasmine.createSpy('resolve');

      /** @type {!shaka.util.AbortableOperation} */
      const operation = shaka.util.AbortableOperation.completed(0)
          .chain(() => promise);
      await Util.shortDelay();  // Ensure we are waiting on the "promise".
      const p = operation.abort().then(
          Util.spyFunc(resolved), Util.spyFunc(resolved));
      // Even after waiting for some Promises, the abort() promise should not
      // get resolved until the "promise" is finished.
      await Util.shortDelay();
      expect(resolved).not.toHaveBeenCalled();
      // Now the abort() can get resolved.
      promise.resolve();
      await p;
    });
  });  // describe('chain')
});  // describe('AbortableOperation')
