/* eslint-env mocha */
const expect = require('chai').expect;
const sinon = require('sinon');
const { Observable } = require('rxjs/Observable');
const { isFSA } = require('flux-standard-action');
require('../source/rxjs.js');
const {
  interserviceDispatch,
  interserviceDispatchWithDispatchMap
} = require('../source/interserviceDispatch.js');

describe('interserviceDispatch', () => {
  const testDispatchMap = {
    actionType: {
      service: 'ServiceName',
      endpoint: '//service.endpoint'
    }
  };

  it('should throw when passed a non-function getDispatchMap', () => {
    expect(() => interserviceDispatch('not a function', () => {})).to.throw(
      /Invalid argument. Expected "getDispatchMap" to be a function./
    );
  });

  it('should resolve "dispatchMap"', done => {
    const dispatchFunctionCreatorSpy = sinon.spy();
    interserviceDispatch(
      () => testDispatchMap,
      dispatchFunctionCreatorSpy
    )({}).subscribe(() => {
      expect(dispatchFunctionCreatorSpy.calledOnce).to.be.true;
      expect(dispatchFunctionCreatorSpy.calledWith({}, testDispatchMap)).to.be.true;
      done();
    });
  });

  it('should resolve "dispatchMap" from promise', done => {
    const dispatchFunctionCreatorSpy = sinon.spy();
    interserviceDispatch(
      () => Promise.resolve(testDispatchMap),
      dispatchFunctionCreatorSpy
    )({}).subscribe(() => {
      expect(dispatchFunctionCreatorSpy.calledOnce).to.be.true;
      expect(dispatchFunctionCreatorSpy.calledWith({}, testDispatchMap)).to.be.true;
      done();
    });
  });

  it('should resolve "dispatchMap" from observable', done => {
    const dispatchFunctionCreatorSpy = sinon.spy();
    interserviceDispatch(
      () => Observable.of(testDispatchMap),
      dispatchFunctionCreatorSpy
    )({}).subscribe(() => {
      expect(dispatchFunctionCreatorSpy.calledOnce).to.be.true;
      expect(dispatchFunctionCreatorSpy.calledWith({}, testDispatchMap)).to.be.true;
      done();
    });
  });

  it('should default to interserviceDispatchWithDispatchMap', done => {
    interserviceDispatch(() => Observable.of(testDispatchMap))({
      name: 'TestService',
      procedures: {},
      models: {}
    }).subscribe(dispatch => {
      expect(dispatch.name).to.be.equal('interserviceDispatchWithScopedState');
      done();
    });
  });
});

describe('interserviceDispatchWithDispatchMap', () => {
  const testDispatchMap = {
    test: {
      service: 'TestService',
      endpoint: '//service.endpoint'
    },
    testPromise: {
      service: 'TestService',
      endpoint: '//service.endpoint'
    },
    testObservable: {
      service: 'TestService',
      endpoint: '//service.endpoint'
    }
  };

  it('should throw when passed invalid service instance', () => {
    expect(() => interserviceDispatchWithDispatchMap('invalid state')()).to.throw(
      /Invalid service state. Expected "state" parameter to be a valid XenService instance./
    );
  });

  it('should throw when passed non-Flux Standard Action', () => {
    expect(
      () => interserviceDispatchWithDispatchMap({ procedures: {}, models: {}})('test')
    ).to.throw(/Dispatched actions must comply with Flux Standard Action syntax/);
  });

  it('should throw when passed local procedure is not implemented', () => {
    expect(() =>
      interserviceDispatchWithDispatchMap({ procedures: {}, models: {}})({ type: 'test' }, true))
        .to.throw(/Local procedure \"test\" not implemented/);
  });

  it('should throw when passed remote procedure is not implemented', () => {
    expect(
      () => interserviceDispatchWithDispatchMap({ procedures: {}, models: {}})({ type: 'test' })
    ).to.throw(/Procedure \"test\" not implemented/);
  });

  it('should invoke local procedure', done => {
    const procedureSpy = sinon.spy();
    const dispatch = interserviceDispatchWithDispatchMap({
      name: 'TestService',
      procedures: {
        test: procedureSpy
      },
      models: {}
    }, testDispatchMap);

    dispatch({ type: 'test' }, true).subscribe(() => {
      expect(procedureSpy.calledOnce).to.be.true;
      done();
    });
  });

  it('should wrap procedure result in Flux standard action', done => {
    const dispatch = interserviceDispatchWithDispatchMap({
      name: 'TestService',
      procedures: {
        test: () => 'G-man'
      },
      models: {}
    }, testDispatchMap);
    dispatch({ type: 'test' }, true).subscribe(responseAction => {
      expect(isFSA(responseAction)).to.be.true;
      expect(responseAction.payload).to.equal('G-man');
      expect(responseAction.meta.protocol).to.equal('response');
      done();
    });
  });

  it('should respond to error with an error action', done => {
    const dispatch = interserviceDispatchWithDispatchMap({
      name: 'TestService',
      procedures: {
        test: () => { throw new Error('Zombine'); }
      },
      models: {}
    }, testDispatchMap);
    dispatch({ type: 'test' }, true).subscribe(responseAction => {
      expect(isFSA(responseAction)).to.be.true;
      expect(responseAction.error).to.be.true;
      expect(responseAction.payload).to.eql({ message: 'Zombine' });
      expect(responseAction.meta.protocol).to.equal('response');
      done();
    });
  });

  it('should wrap procedure result in Observable', () => {
    const dispatch = interserviceDispatchWithDispatchMap({
      name: 'TestService',
      procedures: {
        test: () => 'G-man',
        testPromise: () => Promise.resolve('G-man'),
        testObservable: () => Observable.of('G-man')
      },
      models: {}
    }, testDispatchMap);
    expect(dispatch({ type: 'test' }, true)).to.be.an.instanceof(Observable);
    expect(dispatch({ type: 'testPromise' }, true)).to.be.an.instanceof(Observable);
    expect(dispatch({ type: 'testObservable' }, true)).to.be.an.instanceof(Observable);
  });

  it('should invoke remote procedure', () => {
    const fetchActionSpy = sinon.spy();
    const dispatch = interserviceDispatchWithDispatchMap({
      procedures: {},
      models: {}
    }, testDispatchMap, fetchActionSpy);
    dispatch({ type: 'test' });

    expect(fetchActionSpy.calledOnce).to.be.true;
    expect(fetchActionSpy.calledWith(testDispatchMap.test.endpoint, { type: 'test' })).to.be.true;
  });

  xit('should maintain trace UUID through nested procedure calls', done => { done(); });
});
