import { expect } from 'chai';
import * as sinon from 'sinon';
import { NO_MESSAGE_HANDLER } from '../../constants';
import { RedisContext } from '../../ctx-host';
import { BaseRpcContext } from '../../ctx-host/base-rpc.context';
import { ServerRedis } from '../../server/server-redis';
import { objectToMap } from './utils/object-to-map';

describe('ServerRedis', () => {
  let server: ServerRedis;
  let untypedServer: any;

  beforeEach(() => {
    server = new ServerRedis({});
    untypedServer = server as any;
  });
  describe('listen', () => {
    let onSpy: sinon.SinonSpy;
    let connectSpy: sinon.SinonSpy;
    let client: any;
    let callbackSpy: sinon.SinonSpy;

    beforeEach(() => {
      onSpy = sinon.spy();
      connectSpy = sinon.spy();

      client = {
        on: onSpy,
        connect: connectSpy,
      };
      sinon.stub(server, 'createRedisClient').callsFake(() => client);

      callbackSpy = sinon.spy();
    });
    it('should bind "error" event to handler', () => {
      server.listen(callbackSpy);
      expect(onSpy.getCall(0).args[0]).to.be.equal('error');
    });
    it('should call "RedisClient#connect()"', () => {
      server.listen(callbackSpy);
      expect(connectSpy.called).to.be.true;
    });
    describe('when "start" throws an exception', () => {
      it('should call callback with a thrown error as an argument', () => {
        const error = new Error('random error');

        const callbackSpy = sinon.spy();
        sinon.stub(server, 'start').callsFake(() => {
          throw error;
        });
        server.listen(callbackSpy);
        expect(callbackSpy.calledWith(error)).to.be.true;
      });
    });
  });
  describe('close', () => {
    const pub = { quit: sinon.spy() };
    const sub = { quit: sinon.spy() };
    beforeEach(() => {
      untypedServer.pubClient = pub;
      untypedServer.subClient = sub;
    });
    it('should close pub & sub server', async () => {
      await server.close();

      expect(pub.quit.calledOnce).to.be.true;
      expect(sub.quit.calledOnce).to.be.true;
    });
  });
  describe('handleConnection', () => {
    let onSpy: sinon.SinonSpy, subscribeSpy: sinon.SinonSpy, sub, psub;

    beforeEach(() => {
      onSpy = sinon.spy();
      subscribeSpy = sinon.spy();
      sub = {
        on: onSpy,
        subscribe: subscribeSpy,
      };
      psub = {
        on: onSpy,
        psubscribe: subscribeSpy,
      };
    });
    it('should bind "message" event to handler if wildcards are disabled', () => {
      server.bindEvents(sub, null);
      expect(onSpy.getCall(0).args[0]).to.be.equal('message');
    });
    it('should bind "pmessage" event to handler if wildcards are enabled', () => {
      untypedServer.options = {};
      untypedServer.options.wildcards = true;

      server.bindEvents(psub, null);
      expect(onSpy.getCall(0).args[0]).to.be.equal('pmessage');
    });

    it('should "subscribe" to each pattern if wildcards are disabled', () => {
      const pattern = 'test';
      const handler = sinon.spy();
      untypedServer.messageHandlers = objectToMap({
        [pattern]: handler,
      });
      server.bindEvents(sub, null);
      expect(subscribeSpy.calledWith(pattern)).to.be.true;
    });

    it('should "psubscribe" to each pattern if wildcards are enabled', () => {
      untypedServer.options = {};
      untypedServer.options.wildcards = true;

      const pattern = 'test';
      const handler = sinon.spy();
      untypedServer.messageHandlers = objectToMap({
        [pattern]: handler,
      });
      server.bindEvents(psub, null);
      expect(subscribeSpy.calledWith(pattern)).to.be.true;
    });
  });
  describe('getMessageHandler', () => {
    it(`should return function`, () => {
      expect(typeof server.getMessageHandler(null)).to.be.eql('function');
    });
  });
  describe('handleMessage', () => {
    let getPublisherSpy: sinon.SinonSpy;

    const channel = 'test';
    const data = 'test';
    const id = '3';

    beforeEach(() => {
      getPublisherSpy = sinon.spy();
      sinon.stub(server, 'getPublisher').callsFake(() => getPublisherSpy);
    });
    it('should call "handleEvent" if identifier is not present', async () => {
      const handleEventSpy = sinon.spy(server, 'handleEvent');
      sinon.stub(server, 'parseMessage').callsFake(() => ({ data }) as any);

      await server.handleMessage(channel, JSON.stringify({}), null, channel);
      expect(handleEventSpy.called).to.be.true;
    });
    it(`should publish NO_MESSAGE_HANDLER if pattern not exists in messageHandlers object`, async () => {
      sinon.stub(server, 'parseMessage').callsFake(() => ({ id, data }) as any);
      await server.handleMessage(
        channel,
        JSON.stringify({ id }),
        null,
        channel,
      );
      expect(
        getPublisherSpy.calledWith({
          id,
          status: 'error',
          err: NO_MESSAGE_HANDLER,
        }),
      ).to.be.true;
    });
    it(`should call handler with expected arguments`, async () => {
      const handler = sinon.spy();
      untypedServer.messageHandlers = objectToMap({
        [channel]: handler,
      });
      sinon.stub(server, 'parseMessage').callsFake(() => ({ id, data }) as any);

      await server.handleMessage(channel, '', null, channel);
      expect(handler.calledWith(data)).to.be.true;
    });
  });
  describe('getPublisher', () => {
    let publisherSpy: sinon.SinonSpy;
    let pub, publisher;

    const id = '1';
    const pattern = 'test';
    const context = new RedisContext([] as any);

    beforeEach(() => {
      publisherSpy = sinon.spy();
      pub = {
        publish: publisherSpy,
      };
      publisher = server.getPublisher(pub, pattern, id, context);
    });
    it(`should return function`, () => {
      expect(typeof server.getPublisher(null, null, id, context)).to.be.eql(
        'function',
      );
    });
    it(`should call "publish" with expected arguments`, () => {
      const respond = 'test';
      publisher({ respond, id });
      expect(
        publisherSpy.calledWith(
          `${pattern}.reply`,
          JSON.stringify({ respond, id }),
        ),
      ).to.be.true;
    });
  });
  describe('parseMessage', () => {
    it(`should return parsed json`, () => {
      const obj = { test: 'test' };
      expect(server.parseMessage(obj)).to.deep.equal(
        JSON.parse(JSON.stringify(obj)),
      );
    });
    it(`should not parse argument if it is not an object`, () => {
      const content = 'test';
      expect(server.parseMessage(content)).to.equal(content);
    });
  });
  describe('getRequestPattern', () => {
    const test = 'test';
    it(`should leave pattern as it is`, () => {
      const expectedResult = test;
      expect(server.getRequestPattern(test)).to.equal(expectedResult);
    });
  });
  describe('getReplyPattern', () => {
    const test = 'test';
    it(`should append ".reply" to string`, () => {
      const expectedResult = test + '.reply';
      expect(server.getReplyPattern(test)).to.equal(expectedResult);
    });
  });
  describe('getClientOptions', () => {
    it('should return options object with "retryStrategy" and call "createRetryStrategy"', () => {
      const createSpy = sinon.spy(server, 'createRetryStrategy');
      const { retryStrategy } = server.getClientOptions()!;
      try {
        retryStrategy!(0);
      } catch {
        // Ignore
      }
      expect(createSpy.called).to.be.true;
    });
  });
  describe('createRetryStrategy', () => {
    describe('when is terminated', () => {
      it('should return undefined', () => {
        untypedServer.isManuallyClosed = true;
        const result = server.createRetryStrategy(0);
        expect(result).to.be.undefined;
      });
    });
    describe('when "retryAttempts" does not exist', () => {
      it('should return undefined', () => {
        untypedServer.options.options = {};
        untypedServer.options.options.retryAttempts = undefined;

        expect(server.createRetryStrategy(4)).to.be.undefined;
      });
    });
    describe('when "attempts" count is max', () => {
      it('should return undefined', () => {
        untypedServer.options.options = {};
        untypedServer.options.options.retryAttempts = 3;

        expect(server.createRetryStrategy(4)).to.be.undefined;
      });
    });
    describe('otherwise', () => {
      it('should return delay (ms)', () => {
        untypedServer.options = {};
        untypedServer.isManuallyClosed = false;
        untypedServer.options.retryAttempts = 3;
        untypedServer.options.retryDelay = 3;
        const result = server.createRetryStrategy(2);
        expect(result).to.be.eql(untypedServer.options.retryDelay);
      });
    });
  });
  describe('handleEvent', () => {
    const channel = 'test';
    const data = 'test';

    it('should call handler with expected arguments', async () => {
      const handler = sinon.spy();
      untypedServer.messageHandlers = objectToMap({
        [channel]: handler,
      });

      await server.handleEvent(
        channel,
        { pattern: '', data },
        new BaseRpcContext([]),
      );
      expect(handler.calledWith(data)).to.be.true;
    });
  });
});
