/**
 * @license
 * Copyright 2017 Google LLC
 *
 * Licensed 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.
 */

import { assert } from 'chai';
import * as sinon from 'sinon';
import { async, createSubscribe, Observer, Subscribe } from '../src/subscribe';

describe('createSubscribe', () => {
  let spy: any;
  beforeEach(() => {
    // Listen to console.error calls.
    spy = sinon.spy(console, 'error');
  });

  afterEach(() => {
    spy.restore();
  });

  it('Creation', done => {
    const subscribe = createSubscribe<number>((observer: Observer<number>) => {
      observer.next(123);
    });

    const unsub = subscribe((value: number) => {
      unsub();
      assert.equal(value, 123);
      done();
    });
  });

  it('Logging observer error to console', done => {
    const uncatchableError = new Error('uncatchable');
    const subscribe = createSubscribe<number>((observer: Observer<number>) => {
      observer.next(123);
      observer.complete();
    });

    subscribe({
      next(value) {
        assert.equal(value, 123);
        // Simulate an error is thrown in the next callback.
        // This should log to the console as an error.
        throw uncatchableError;
      },
      complete() {
        // By this point, the error should have been logged.
        assert.ok(spy.calledWith(uncatchableError));
        done();
      }
    });
  });

  it('Well-defined subscription order', done => {
    const subscribe = createSubscribe<number>(observer => {
      observer.next(123);
      // Subscription after value emitted should NOT be received.
      subscribe({
        next(_value) {
          assert.ok(false);
        }
      });
    });
    // Subscription before value emitted should be recieved.
    subscribe({
      next(_value) {
        done();
      }
    });
  });

  it('Subscribing to already complete Subscribe', done => {
    let seq = 0;
    const subscribe = createSubscribe<number>(observer => {
      observer.next(456);
      observer.complete();
    });
    subscribe({
      next(value: number) {
        assert.equal(seq++, 0);
        assert.equal(value, 456);
      },
      complete() {
        subscribe({
          complete() {
            assert.equal(seq++, 1);
            done();
          }
        });
      }
    });
  });

  it('Subscribing to errored Subscribe', done => {
    let seq = 0;
    const subscribe = createSubscribe<number>(observer => {
      observer.next(246);
      observer.error(new Error('failure'));
    });
    subscribe({
      next(value: number) {
        assert.equal(seq++, 0);
        assert.equal(value, 246);
      },
      error(e) {
        assert.equal(seq++, 1);
        subscribe({
          error(_e2) {
            assert.equal(seq++, 2);
            assert.equal(e.message, 'failure');
            done();
          }
        });
      },
      complete() {
        assert.ok(false);
      }
    });
  });

  it('Delayed value', done => {
    const subscribe = createSubscribe<number>((observer: Observer<number>) => {
      setTimeout(() => observer.next(123), 10);
    });

    subscribe((value: number) => {
      assert.equal(value, 123);
      done();
    });
  });

  it('Executor throws => Error', () => {
    // It's an application error to throw an exception in the executor -
    // but since it is called asynchronously, our only option is
    // to emit that Error and terminate the Subscribe.
    const subscribe = createSubscribe<number>((_observer: Observer<number>) => {
      throw new Error('Executor throws');
    });
    subscribe({
      error(e) {
        assert.equal(e.message, 'Executor throws');
      }
    });
  });

  it('Sequence', done => {
    const subscribe = makeCounter(10);

    let j = 1;
    subscribe({
      next(value: number) {
        assert.equal(value, j++);
      },
      complete() {
        assert.equal(j, 11);
        done();
      }
    });
  });

  it('unlisten', done => {
    const subscribe = makeCounter(10);

    subscribe({
      complete: () => {
        async(done)();
      }
    });

    let j = 1;
    const unsub = subscribe({
      next: (value: number) => {
        assert.ok(value <= 5);
        assert.equal(value, j++);
        if (value === 5) {
          unsub();
        }
      },
      complete: () => {
        assert.ok(false, 'Does not call completed if unsubscribed');
      }
    });
  });

  it('onNoObservers', done => {
    const subscribe = makeCounter(10);

    let j = 1;
    const unsub = subscribe({
      next: (value: number) => {
        assert.ok(value <= 5);
        assert.equal(value, j++);
        if (value === 5) {
          unsub();
          async(done)();
        }
      },
      complete: () => {
        assert.ok(false, 'Does not call completed if unsubscribed');
      }
    });
  });

  // TODO(koss): Add test for partial Observer (missing methods).
  it('Partial Observer', done => {
    const subscribe = makeCounter(10);

    const _unsub = subscribe({
      complete: () => {
        done();
      }
    });
  });
});

function makeCounter(maxCount: number, ms = 10): Subscribe<number> {
  let id: any;

  return createSubscribe<number>(
    (observer: Observer<number>) => {
      let i = 1;
      id = setInterval(() => {
        observer.next(i++);
        if (i > maxCount) {
          if (id) {
            clearInterval(id);
            id = undefined;
          }
          observer.complete();
        }
      }, ms);
    },
    (_observer: Observer<number>) => {
      clearInterval(id);
      id = undefined;
    }
  );
}
