'use strict';

const should = require('should');
const sinon = require('sinon');

const {
  Request,
  BadRequestError,
  SizeLimitError
} = require('../../../../index');
const KuzzleMock = require('../../../mocks/kuzzle.mock');

const SecurityController = require('../../../../lib/api/controller/security');
const Role = require('../../../../lib/model/security/role');

describe('Test: security controller - roles', () => {
  let kuzzle;
  let request;
  let securityController;

  beforeEach(() => {
    kuzzle = new KuzzleMock();
    securityController = new SecurityController();

    request = new Request(
      {controller: 'security'},
      {user: { _id: '4' } });
  });

  describe('#updateRoleMapping', () => {
    it('should throw a BadRequestError if the body is missing', async () => {
      await should(securityController.updateRoleMapping(request))
        .rejectedWith(BadRequestError, { id: 'api.assert.body_required' });

      should(kuzzle.ask.withArgs('core:storage:private:mappings:update'))
        .not.called();
    });

    it('should update the role mapping', async () => {
      request.input.body = { foo: 'bar' };
      kuzzle.ask
        .withArgs('core:storage:private:mappings:update')
        .resolves(request.input.body);

      const response = await securityController.updateRoleMapping(request);

      should(kuzzle.ask).calledWith(
        'core:storage:private:mappings:update',
        kuzzle.internalIndex.index,
        'roles',
        request.input.body);

      should(response).match(request.input.body);
    });
  });

  describe('#getRoleMapping', () => {
    it('should fulfill with a response object', async () => {
      kuzzle.ask.withArgs('core:storage:private:mappings:get').resolves({
        properties: { foo: 'bar' },
      });

      const response = await securityController.getRoleMapping(request);

      should(kuzzle.ask).be.calledWith(
        'core:storage:private:mappings:get',
        kuzzle.internalIndex.index,
        'roles');

      should(response).match({ mapping: { foo: 'bar' } });
    });
  });

  describe('#createOrReplaceRole', () => {
    const createOrReplaceEvent = 'core:security:role:createOrReplace';
    let createOrReplaceStub;
    let createdRole;

    beforeEach(() => {
      request.input.resource._id = 'test';
      request.input.body = { foo: 'bar' };

      createdRole = new Role();
      createdRole._id = request.input.resource._id;
      Object.assign(createdRole, {controllers: {ctrl: true}, foo: 'bar'});

      createOrReplaceStub = kuzzle.ask
        .withArgs(createOrReplaceEvent, request.input.resource._id)
        .resolves(createdRole);
    });

    it('should create a role using default options', async () => {
      const response = await securityController.createOrReplaceRole(request);

      should(createOrReplaceStub).calledWithMatch(
        createOrReplaceEvent,
        request.input.resource._id,
        request.input.body,
        {
          force: false,
          refresh: 'wait_for',
          userId: request.context.user._id,
        });

      should(response).be.an.Object().and.not.instanceof(Role);
      should(response).match({
        _id: createdRole._id,
        _source: {
          controllers: createdRole.controllers,
          foo: 'bar',
        },
      });
    });

    it('should forward a security module exception', () => {
      const error = new Error('foo');

      createOrReplaceStub.rejects(error);

      return should(securityController.createOrReplaceRole(request))
        .be.rejectedWith(error);
    });

    it('should forward request options', async () => {
      request.input.args.force = true;
      request.input.args.refresh = false;

      await securityController.createOrReplaceRole(request);

      should(createOrReplaceStub).calledWithMatch(
        createOrReplaceEvent,
        request.input.resource._id,
        request.input.body,
        {
          force: true,
          refresh: 'false',
          userId: request.context.user._id,
        });
    });

    it('should reject if no id is provided', async () => {
      request.input.resource._id = null;

      await should(securityController.createOrReplaceRole(request))
        .rejectedWith(BadRequestError, { id: 'api.assert.missing_argument' });

      should(createOrReplaceStub).not.called();
    });

    it('should reject if no body is provided', async () => {
      request.input.body = null;

      await should(securityController.createOrReplaceRole(request))
        .rejectedWith(BadRequestError, { id: 'api.assert.body_required' });

      should(createOrReplaceStub).not.called();
    });
  });

  describe('#createRole', () => {
    const createEvent = 'core:security:role:create';
    let createStub;
    let createdRole;

    beforeEach(() => {
      request.input.resource._id = 'test';
      request.input.body = { foo: 'bar' };

      createdRole = new Role();
      createdRole._id = request.input.resource._id;
      Object.assign(createdRole, {controllers: {ctrl: true}, foo: 'bar'});

      createStub = kuzzle.ask
        .withArgs(createEvent, request.input.resource._id)
        .resolves(createdRole);
    });

    it('should create a role using default options', async () => {
      const response = await securityController.createRole(request);

      should(createStub).calledWithMatch(
        createEvent,
        request.input.resource._id,
        request.input.body,
        {
          force: false,
          refresh: 'wait_for',
          userId: request.context.user._id,
        });

      should(response).be.an.Object().and.not.instanceof(Role);
      should(response).match({
        _id: createdRole._id,
        _source: {
          controllers: createdRole.controllers,
          foo: 'bar',
        },
      });
    });

    it('should forward a security module exception', () => {
      const error = new Error('foo');

      createStub.rejects(error);

      return should(securityController.createRole(request))
        .be.rejectedWith(error);
    });

    it('should forward request options', async () => {
      request.input.args.force = true;
      request.input.args.refresh = false;

      await securityController.createRole(request);

      should(createStub).calledWithMatch(
        createEvent,
        request.input.resource._id,
        request.input.body,
        {
          force: true,
          refresh: 'false',
          userId: request.context.user._id,
        });
    });

    it('should reject if no id is provided', async () => {
      request.input.resource._id = null;

      await should(securityController.createRole(request))
        .rejectedWith(BadRequestError, { id: 'api.assert.missing_argument' });

      should(createStub).not.called();
    });

    it('should reject if no body is provided', async () => {
      request.input.body = null;

      await should(securityController.createRole(request))
        .rejectedWith(BadRequestError, { id: 'api.assert.body_required' });

      should(createStub).not.called();
    });
  });

  describe('#getRole', () => {
    const getEvent = 'core:security:role:get';
    let getStub;

    beforeEach(() => {
      request.input.resource._id = 'test';
      getStub = kuzzle.ask.withArgs(getEvent, request.input.resource._id);
    });

    it('should resolve to an object on a getRole call', async () => {
      let returnedRole = new Role();
      returnedRole._id = 'foo';
      Object.assign(returnedRole, {controllers: {ctrl: true}, foo: 'bar'});

      getStub.resolves(returnedRole);

      const response = await securityController.getRole(request);

      should(getStub).calledOnce();

      should(response).be.Object().and.not.instanceof(Role);
      should(response).match({
        _id: returnedRole._id,
        _source: {
          controllers: returnedRole.controllers,
          foo: 'bar',
        },
      });
    });

    it('should forward a security module exception', () => {
      const error = new Error('foo');

      getStub.rejects(error);

      return should(securityController.getRole(request)).be.rejectedWith(error);
    });

    it('should reject if no id is provided', async () => {
      request.input.resource._id = null;

      await should(securityController.getRole(request))
        .rejectedWith(BadRequestError, { id: 'api.assert.missing_argument' });

      should(getStub).not.called();
    });
  });

  describe('#mGetRoles', () => {
    const mGetEvent = 'core:security:role:mGet';
    let mGetStub;

    beforeEach(() => {
      request.input.body = { ids: 'foobar'.split('') };

      mGetStub = kuzzle.ask.withArgs(mGetEvent);
    });

    it('should reject if no body is provided', async () => {
      request.input.body = null;

      await should(securityController.mGetRoles(request))
        .rejectedWith(BadRequestError, { id: 'api.assert.body_required' });

      should(mGetStub).not.called();
    });

    it('should reject if no ids is provided', async () => {
      delete request.input.body.ids;

      await should(securityController.mGetRoles(request))
        .rejectedWith(BadRequestError, { id: 'api.assert.missing_argument' });

      should(mGetStub).not.called();
    });

    it('should reject if ids is not an array', async () => {
      request.input.body.ids = 'foobar';

      await should(securityController.mGetRoles(request))
        .rejectedWith(BadRequestError, { id: 'api.assert.invalid_type' });

      should(mGetStub).not.called();
    });

    it('should forward a security module exception', () => {
      const error = new Error('foo');

      mGetStub.rejects(error);

      return should(securityController.mGetRoles(request)).rejectedWith(error);
    });

    it('should resolve to an object', async () => {
      const role1 = new Role();
      const role2 = new Role();
      const role3 = new Role();

      role1._id = 'role1';
      role2._id = 'role2';
      role3._id = 'role3';

      mGetStub.resolves([role1, role2, role3]);

      const response = await securityController.mGetRoles(request);

      should(mGetStub).calledWithMatch(mGetEvent, request.input.body.ids);

      should(response).be.an.Object();
      should(response.hits).be.an.Array().and.have.length(3);

      for (let i = 0; i < response.hits.length; i++) {
        should(response.hits[i]).be.an.Object().and.not.instanceof(Role);
        should(response.hits[i]._id).eql(`role${i+1}`);
      }
    });
  });

  describe('#searchRoles', () => {
    const searchEvent = 'core:security:role:search';
    let searchStub;
    let searchedRole;

    beforeEach(() => {
      request.input.body = { controllers: 'foobar'.split('') };

      searchedRole = new Role();
      searchedRole._id = 'foo';
      Object.assign(searchedRole, {controllers: {ctrl: true}, foo: 'bar'});

      searchStub = kuzzle.ask
        .withArgs(searchEvent)
        .resolves({ hits: [searchedRole], total: 1 });
    });

    it('should return response with an array of roles on searchRole call', async () => {
      const response = await securityController.searchRoles(request);

      should(response).be.an.Object();
      should(response.hits).be.an.Array().and.have.length(1);

      should(response.hits[0]).not.instanceof(Role).and.match({
        _id: searchedRole._id,
        _source: {
          controllers: searchedRole.controllers,
          foo: 'bar',
        }
      });

      should(searchStub).calledWithMatch(
        searchEvent,
        request.input.body.controllers,
        { from: 0, size: kuzzle.config.limits.documentsFetchCount });
    });

    it('should reject if the "size" option exceeds server limits', async () => {
      kuzzle.config.limits.documentsFetchCount = 1;
      request.input.args.size = 10;

      await should(securityController.searchRoles(request))
        .rejectedWith(SizeLimitError, {
          id: 'services.storage.get_limit_exceeded',
        });

      should(searchStub).not.called();
    });

    it('should reject if "controllers" is not an array', async () => {
      request.input.body.controllers = 'foo';

      await should(securityController.searchRoles(request))
        .rejectedWith(BadRequestError, {
          id: 'api.assert.invalid_type',
        });

      should(searchStub).not.called();
    });

    it('should reject if "from" is invalid', async () => {
      for (const from of [true, false, {}, [], 'foo', 12.34]) {
        request.input.args.from = from;

        await should(securityController.searchRoles(request))
          .rejectedWith(BadRequestError, {
            id: 'api.assert.invalid_type',
          });

        should(searchStub).not.called();
      }
    });

    it('should reject if "size" is invalid', async () => {
      for (const size of [true, false, {}, [], 'foo', 12.34]) {
        request.input.args.size = size;

        await should(securityController.searchRoles(request))
          .rejectedWith(BadRequestError, {
            id: 'api.assert.invalid_type',
          });

        should(searchStub).not.called();
      }
    });

    it('should search for all controllers if none are provided', async () => {
      delete request.input.body.controllers;
      await securityController.searchRoles(request);
      should(searchStub).calledWithMatch(
        searchEvent,
        [],
        { from: 0, size: kuzzle.config.limits.documentsFetchCount });

      request.input.body = null;
      await securityController.searchRoles(request);
      should(searchStub).calledWithMatch(
        searchEvent,
        [],
        { from: 0, size: kuzzle.config.limits.documentsFetchCount });
    });

    it('should forward security module exceptions', () => {
      const error = new Error('foo');

      searchStub.rejects(error);

      return should(securityController.searchRoles(request)).rejectedWith(error);
    });
  });

  describe('#updateRole', () => {
    const updateEvent = 'core:security:role:update';
    let updateStub;
    let updatedRole;

    beforeEach(() => {
      request.input.resource._id = 'test';
      request.input.body = { foo: 'bar' };

      updatedRole = new Role();
      updatedRole._id = 'test';
      Object.assign(updatedRole, {controllers: { ctrl: true }, foo: 'bar'});

      updateStub = kuzzle.ask.withArgs(updateEvent).resolves(updatedRole);
    });

    it('should return a valid response and use default options', async () => {
      const response = await securityController.updateRole(request);

      should(updateStub).calledWithMatch(
        updateEvent,
        request.input.resource._id,
        request.input.body,
        {
          force: false,
          refresh: 'wait_for',
          retryOnConflict: 10,
          userId: request.context.user._id,
        });

      should(response).be.an.Object().and.not.instanceof(Role);
      should(response).match({
        _id: updatedRole._id,
        _source: {
          controllers: updatedRole.controllers,
          foo: 'bar',
        }
      });
    });

    it('should reject if no id is given', async () => {
      request.input.resource._id = null;

      await should(securityController.updateRole(request))
        .rejectedWith(BadRequestError, { id: 'api.assert.missing_argument' });

      should(updateStub).not.called();
    });

    it('should reject if no body is provided', async () => {
      request.input.body = null;

      await should(securityController.updateRole(request))
        .rejectedWith(BadRequestError, { id: 'api.assert.body_required' });

      should(updateStub).not.called();
    });

    it('should forward an exception thrown by the security module', () => {
      const error = new Error('foo');

      updateStub.rejects(error);

      return should(securityController.updateRole(request)).rejectedWith(error);
    });

    it('should forward request options', async () => {
      request.input.args.force = true;
      request.input.args.refresh = false;
      request.input.args.retryOnConflict = 123;

      await securityController.updateRole(request);

      should(updateStub).calledWithMatch(
        updateEvent,
        request.input.resource._id,
        request.input.body,
        {
          force: true,
          refresh: 'false',
          retryOnConflict: 123,
          userId: request.context.user._id,
        });
    });
  });

  describe('#deleteRole', () => {
    const deleteEvent = 'core:security:role:delete';
    let deleteStub;

    beforeEach(() => {
      request.input.resource._id = 'test';

      deleteStub = kuzzle.ask.withArgs(deleteEvent);
    });

    it('should return a valid response and handle default options', async () => {
      const response = await securityController.deleteRole(request);

      should(deleteStub).calledWithMatch(
        deleteEvent,
        request.input.resource._id,
        { refresh: 'wait_for' });

      should(response).match({ _id: request.input.resource._id });
    });

    it('should reject if no id is provided', async () => {
      request.input.resource._id = null;

      await should(securityController.deleteRole(request))
        .rejectedWith(BadRequestError, { id: 'api.assert.missing_argument' });

      should(deleteStub).not.called();
    });

    it('should forward request options', async () => {
      request.input.args.refresh = false;

      await securityController.deleteRole(request);

      should(deleteStub).calledWithMatch(
        deleteEvent,
        request.input.resource._id,
        { refresh: 'false' });
    });
  });

  describe('#mDeleteRoles', () => {
    it('should forward to _mDelete and return its response', async () => {
      sinon.stub(securityController, '_mDelete');
      securityController._mDelete.resolves('foobar');

      const response = await securityController.mDeleteRoles(request);

      should(securityController._mDelete).calledWith('role', request);
      should(response).eql('foobar');
    });
  });
});
