import { NestContainer } from '@nestjs/core';
import { ApplicationConfig } from '@nestjs/core/application-config';
import { expect } from 'chai';
import { fromEvent, lastValueFrom, Observable, of } from 'rxjs';
import * as sinon from 'sinon';
import { GraphInspector } from '../../core/inspector/graph-inspector';
import { MetadataScanner } from '../../core/metadata-scanner';
import { AbstractWsAdapter } from '../adapters/ws-adapter';
import { PORT_METADATA } from '../constants';
import { WsContextCreator } from '../context/ws-context-creator';
import { WebSocketGateway } from '../decorators/socket-gateway.decorator';
import { InvalidSocketPortException } from '../errors/invalid-socket-port.exception';
import {
  GatewayMetadataExplorer,
  MessageMappingProperties,
} from '../gateway-metadata-explorer';
import { SocketServerProvider } from '../socket-server-provider';
import { WebSocketsController } from '../web-sockets-controller';

class NoopAdapter extends AbstractWsAdapter {
  public create(port: number, options?: any) {}
  public bindMessageHandlers(
    client: any,
    handlers,
    transform: (data: any) => Observable<any>,
  ) {
    handlers.forEach(({ message, callback }) => {
      const source$ = fromEvent(client, message);
      source$.subscribe(data => null);
    });
  }
}

describe('WebSocketsController', () => {
  let instance: WebSocketsController;
  let untypedInstance: any;
  let provider: SocketServerProvider,
    graphInspector: GraphInspector,
    config: ApplicationConfig,
    mockProvider: sinon.SinonMock;

  const messageHandlerCallback = () => Promise.resolve();
  const port = 90,
    namespace = '/';
  @WebSocketGateway(port, { namespace })
  class Test {}

  beforeEach(() => {
    config = new ApplicationConfig(new NoopAdapter());
    provider = new SocketServerProvider(null!, config);
    graphInspector = new GraphInspector(new NestContainer());
    mockProvider = sinon.mock(provider);

    const contextCreator = sinon.createStubInstance(WsContextCreator);
    contextCreator.create.returns(messageHandlerCallback);
    instance = new WebSocketsController(
      provider,
      config,
      contextCreator as any,
      graphInspector,
    );
    untypedInstance = instance as any;
  });
  describe('connectGatewayToServer', () => {
    let subscribeToServerEvents: sinon.SinonSpy;

    @WebSocketGateway('test' as any)
    class InvalidGateway {}

    @WebSocketGateway()
    class DefaultGateway {}

    beforeEach(() => {
      subscribeToServerEvents = sinon.spy();
      untypedInstance.subscribeToServerEvents = subscribeToServerEvents;
    });
    it('should throw "InvalidSocketPortException" when port is not a number', () => {
      Reflect.defineMetadata(PORT_METADATA, 'test', InvalidGateway);
      expect(() =>
        instance.connectGatewayToServer(
          new InvalidGateway(),
          InvalidGateway,
          'moduleKey',
          'instanceWrapperId',
        ),
      ).throws(InvalidSocketPortException);
    });
    it('should call "subscribeToServerEvents" with default values when metadata is empty', () => {
      const gateway = new DefaultGateway();
      instance.connectGatewayToServer(
        gateway,
        DefaultGateway,
        'moduleKey',
        'instanceWrapperId',
      );
      expect(subscribeToServerEvents.calledWith(gateway, {}, 0, 'moduleKey')).to
        .be.true;
    });
    it('should call "subscribeToServerEvents" when metadata is valid', () => {
      const gateway = new Test();
      instance.connectGatewayToServer(
        gateway,
        Test,
        'moduleKey',
        'instanceWrapperId',
      );
      expect(
        subscribeToServerEvents.calledWith(
          gateway,
          { namespace },
          port,
          'moduleKey',
        ),
      ).to.be.true;
    });
  });
  describe('subscribeToServerEvents', () => {
    let explorer: GatewayMetadataExplorer,
      mockExplorer: sinon.SinonMock,
      gateway,
      handlers,
      server,
      assignServerToProperties: sinon.SinonSpy,
      subscribeEvents: sinon.SinonSpy;
    const handlerCallback = () => {};

    beforeEach(() => {
      gateway = new Test();
      explorer = new GatewayMetadataExplorer(new MetadataScanner());
      mockExplorer = sinon.mock(explorer);
      untypedInstance.metadataExplorer = explorer;

      handlers = [
        {
          message: 'message',
          methodName: 'methodName',
          callback: handlerCallback,
          isAckHandledManually: false,
        },
      ];
      server = { server: 'test' };

      mockExplorer.expects('explore').returns(handlers);
      mockProvider.expects('scanForSocketServer').returns(server);

      assignServerToProperties = sinon.spy();
      subscribeEvents = sinon.spy();
      instance['assignServerToProperties'] = assignServerToProperties;
      instance['subscribeEvents'] = subscribeEvents;
    });
    it('should call "assignServerToProperties" with expected arguments', () => {
      instance.subscribeToServerEvents(
        gateway,
        { namespace },
        port,
        'moduleKey',
        'instanceWrapperId',
      );
      expect(assignServerToProperties.calledWith(gateway, server.server)).to.be
        .true;
    });
    it('should call "subscribeEvents" with expected arguments', () => {
      instance.subscribeToServerEvents(
        gateway,
        { namespace },
        port,
        'moduleKey',
        'instanceWrapperId',
      );
      expect(subscribeEvents.firstCall.args[0]).to.be.equal(gateway);
      expect(subscribeEvents.firstCall.args[2]).to.be.equal(server);
      expect(subscribeEvents.firstCall.args[1]).to.be.eql([
        {
          message: 'message',
          methodName: 'methodName',
          callback: messageHandlerCallback,
          isAckHandledManually: false,
        },
      ]);
    });
  });
  describe('inspectEntrypointDefinitions', () => {
    it('should inspect & insert corresponding entrypoint definitions', () => {
      class GatewayHostCls {}

      const port = 80;
      const instanceWrapperId = '1234';
      const messageHandlers: MessageMappingProperties[] = [
        {
          methodName: 'findOne',
          message: 'find',
          callback: null!,
          isAckHandledManually: false,
        },
        {
          methodName: 'create',
          message: 'insert',
          callback: null!,
          isAckHandledManually: false,
        },
      ];
      const insertEntrypointDefinitionSpy = sinon.spy(
        graphInspector,
        'insertEntrypointDefinition',
      );
      instance.inspectEntrypointDefinitions(
        new GatewayHostCls(),
        port,
        messageHandlers,
        instanceWrapperId,
      );

      expect(insertEntrypointDefinitionSpy.calledTwice).to.be.true;
      expect(
        insertEntrypointDefinitionSpy.calledWith({
          type: 'websocket',
          methodName: messageHandlers[0].methodName,
          className: GatewayHostCls.name,
          classNodeId: instanceWrapperId,
          metadata: {
            port,
            key: messageHandlers[0].message,
            message: messageHandlers[0].message,
          } as any,
        }),
      ).to.be.true;
      expect(
        insertEntrypointDefinitionSpy.calledWith({
          type: 'websocket',
          methodName: messageHandlers[1].methodName,
          className: GatewayHostCls.name,
          classNodeId: instanceWrapperId,
          metadata: {
            port,
            key: messageHandlers[1].message,
            message: messageHandlers[1].message,
          } as any,
        }),
      ).to.be.true;
    });
  });
  describe('subscribeEvents', () => {
    const gateway = new Test();

    let handlers: any;
    let server: any,
      subscribeConnectionEvent: sinon.SinonSpy,
      subscribeDisconnectEvent: sinon.SinonSpy,
      nextSpy: sinon.SinonSpy,
      onSpy: sinon.SinonSpy,
      subscribeInitEvent: sinon.SinonSpy,
      getConnectionHandler: sinon.SinonSpy;

    beforeEach(() => {
      nextSpy = sinon.spy();
      onSpy = sinon.spy();
      subscribeInitEvent = sinon.spy();
      getConnectionHandler = sinon.spy();
      subscribeConnectionEvent = sinon.spy();
      subscribeDisconnectEvent = sinon.spy();

      handlers = ['test'];
      server = {
        server: {
          on: onSpy,
        },
        init: {
          next: nextSpy,
        },
        disconnect: {},
        connection: {},
      };
      untypedInstance.subscribeInitEvent = subscribeInitEvent;
      untypedInstance.getConnectionHandler = getConnectionHandler;
      untypedInstance.subscribeConnectionEvent = subscribeConnectionEvent;
      untypedInstance.subscribeDisconnectEvent = subscribeDisconnectEvent;
    });

    it('should call "subscribeConnectionEvent" with expected arguments', () => {
      instance.subscribeEvents(gateway, handlers, server);
      expect(subscribeConnectionEvent.calledWith(gateway, server.connection)).to
        .be.true;
    });
    it('should call "subscribeDisconnectEvent" with expected arguments', () => {
      instance.subscribeEvents(gateway, handlers, server);
      expect(subscribeDisconnectEvent.calledWith(gateway, server.disconnect)).to
        .be.true;
    });
    it('should call "subscribeInitEvent" with expected arguments', () => {
      instance.subscribeEvents(gateway, handlers, server);
      expect(subscribeInitEvent.calledWith(gateway, server.init)).to.be.true;
    });
    it('should bind connection handler to server', () => {
      instance.subscribeEvents(gateway, handlers, server);
      expect(onSpy.calledWith('connection', getConnectionHandler())).to.be.true;
    });
    it('should call "getConnectionHandler" with expected arguments', () => {
      instance.subscribeEvents(gateway, handlers, server);
      expect(
        getConnectionHandler.calledWith(
          instance,
          gateway,
          handlers,
          server.disconnect,
          server.connection,
        ),
      ).to.be.true;
    });
  });
  describe('getConnectionHandler', () => {
    const gateway = new Test();

    let handlers, fn;
    let connection,
      client,
      nextSpy: sinon.SinonSpy,
      onSpy: sinon.SinonSpy,
      subscribeMessages: sinon.SinonSpy,
      subscribeDisconnectEvent: sinon.SinonSpy,
      subscribeConnectionEvent: sinon.SinonSpy;

    beforeEach(() => {
      nextSpy = sinon.spy();
      onSpy = sinon.spy();
      subscribeMessages = sinon.spy();
      subscribeDisconnectEvent = sinon.spy();
      subscribeConnectionEvent = sinon.spy();

      handlers = ['test'];
      connection = {
        next: nextSpy,
      };
      client = {
        on: onSpy,
      };
      untypedInstance.subscribeDisconnectEvent = subscribeDisconnectEvent;
      untypedInstance.subscribeConnectionEvent = subscribeConnectionEvent;
      untypedInstance.subscribeMessages = subscribeMessages;

      fn = instance.getConnectionHandler(
        instance,
        gateway,
        handlers,
        null!,
        connection,
      );
      fn(client);
    });

    it('should return function', () => {
      expect(
        instance.getConnectionHandler(null!, null!, null!, null!, null!),
      ).to.be.a('function');
    });
    it('should call "next" method of connection object with expected argument', () => {
      expect(nextSpy.calledWith([client])).to.be.true;
    });
    it('should call "subscribeMessages" with expected arguments', () => {
      expect(subscribeMessages.calledWith(handlers, client, gateway)).to.be
        .true;
    });
    it('should call "on" method of client object with expected arguments', () => {
      expect(onSpy.called).to.be.true;
    });
  });
  describe('subscribeInitEvent', () => {
    const gateway = new Test();
    let event: any, subscribe: sinon.SinonSpy;

    beforeEach(() => {
      subscribe = sinon.spy();
      event = { subscribe, pipe: sinon.stub().returnsThis() };
    });
    it('should not call subscribe method when "afterInit" method not exists', () => {
      instance.subscribeInitEvent(gateway, event);
      expect(subscribe.called).to.be.false;
    });
    it('should call subscribe method of event object with expected arguments when "afterInit" exists', () => {
      (gateway as any).afterInit = () => {};
      instance.subscribeInitEvent(gateway, event);
      expect(subscribe.called).to.be.true;
    });
  });
  describe('subscribeConnectionEvent', () => {
    const gateway = new Test();
    let event, subscribe: sinon.SinonSpy;

    beforeEach(() => {
      subscribe = sinon.spy();
      event = { subscribe, pipe: sinon.stub().returnsThis() };
    });
    it('should not call subscribe method when "handleConnection" method not exists', () => {
      instance.subscribeConnectionEvent(gateway, event);
      expect(subscribe.called).to.be.false;
    });
    it('should call subscribe method of event object with expected arguments when "handleConnection" exists', () => {
      (gateway as any).handleConnection = () => {};
      instance.subscribeConnectionEvent(gateway, event);
      expect(subscribe.called).to.be.true;
    });
  });
  describe('subscribeDisconnectEvent', () => {
    const gateway = new Test();
    let event, subscribe: sinon.SinonSpy;

    beforeEach(() => {
      subscribe = sinon.spy();
      event = { subscribe, pipe: sinon.stub().returnsThis() };
    });
    it('should not call subscribe method when "handleDisconnect" method not exists', () => {
      instance.subscribeDisconnectEvent(gateway, event);
      expect(subscribe.called).to.be.false;
    });
    it('should call subscribe method of event object with expected arguments when "handleDisconnect" exists', () => {
      (gateway as any).handleDisconnect = () => {};
      instance.subscribeDisconnectEvent(gateway, event);
      expect(subscribe.called).to.be.true;
    });
  });
  describe('subscribeMessages', () => {
    const gateway = new Test();

    let client, handlers, onSpy: sinon.SinonSpy;

    beforeEach(() => {
      onSpy = sinon.spy();
      client = { on: onSpy, off: onSpy };

      handlers = [
        {
          message: 'test',
          callback: { bind: () => 'testCallback' },
          isAckHandledManually: true,
        },
        {
          message: 'test2',
          callback: { bind: () => 'testCallback2' },
          isAckHandledManually: false,
        },
      ];
    });
    it('should bind each handler to client', () => {
      instance.subscribeMessages(handlers, client, gateway);
      expect(onSpy.calledTwice).to.be.true;
    });
    it('should pass "isAckHandledManually" flag to the adapter', () => {
      const adapter = config.getIoAdapter();
      const bindMessageHandlersSpy = sinon.spy(adapter, 'bindMessageHandlers');

      instance.subscribeMessages(handlers, client, gateway);

      const handlersPassedToAdapter = bindMessageHandlersSpy.firstCall.args[1];

      expect(handlersPassedToAdapter[0].message).to.equal(handlers[0].message);
      expect(handlersPassedToAdapter[0].isAckHandledManually).to.equal(
        handlers[0].isAckHandledManually,
      );

      expect(handlersPassedToAdapter[1].message).to.equal(handlers[1].message);
      expect(handlersPassedToAdapter[1].isAckHandledManually).to.equal(
        handlers[1].isAckHandledManually,
      );
    });
  });
  describe('pickResult', () => {
    describe('when deferredResult contains value which', () => {
      describe('is a Promise', () => {
        it('should return Promise<Observable>', async () => {
          const value = 100;
          expect(
            await lastValueFrom(
              await instance.pickResult(
                Promise.resolve(Promise.resolve(value)),
              ),
            ),
          ).to.be.eq(value);
        });
      });

      describe('is an Observable', () => {
        it('should return Promise<Observable>', async () => {
          const value = 100;
          expect(
            await lastValueFrom(
              await instance.pickResult(Promise.resolve(of(value))),
            ),
          ).to.be.eq(value);
        });
      });

      describe('is an object that has the method `subscribe`', () => {
        it('should return Promise<Observable>', async () => {
          const value = { subscribe() {} };
          expect(
            await lastValueFrom(
              await instance.pickResult(Promise.resolve(value)),
            ),
          ).to.equal(value);
        });
      });

      describe('is an ordinary value', () => {
        it('should return Promise<Observable>', async () => {
          const value = 100;
          expect(
            await lastValueFrom(
              await instance.pickResult(Promise.resolve(value)),
            ),
          ).to.be.eq(value);
        });
      });
    });
  });
});
