import {
  Document,
  model,
  PopulateOptions,
  Schema,
  Types,
} from 'mongoose';
import {
  createRequest,
  createResponse,
  MockRequest,
  MockResponse,
} from 'node-mocks-http';

import {
  IApiMeta,
} from '../dist/types/IApiMeta';
import BaseController from './base.controller';
import {
  ApiModel,
  IApiError,
  IApiModel,
  IApiQuery,
} from './types';

interface IMockModel extends IApiModel {
  property?: string;
}
type MockModelDocument = Document & IApiModel;

export type MockModel = ApiModel<MockModelDocument>;


const mockFilters = [ 'propertyOne' ];
class MockController extends BaseController<MockModelDocument> {
  constructor(_model: MockModel) {
    super(_model);
    this.filters.push(...mockFilters);
  }
}

const mockSchema = new Schema<IMockModel, MockModel>({
  property: String,
  timestamps: {
    created: {
      at: {
        type: Date,
        default: Date.now,
      },
      by: String,
    },
    updated: {
      at: {
        type: Date,
        default: Date.now,
      },
      by: String,
    },
  },
  mark: {
    deleted: Boolean,
  },
});

describe('base.controller spec', () => {
  let mockModel: MockModel;
  let controller: MockController;
  const mockNext = jest.fn();

  beforeEach(() => {
    mockModel = model<IMockModel, MockModel>('mockModel', mockSchema);
    controller = new MockController(mockModel);
  });

  describe('parseSort()', () => {
    it('should parse a sort string', () => {
      const parsedSort = controller.parseSort('type');

      expect(parsedSort).toEqual({ type: 1 });
    });
    it('should parse a sort object', () => {
      const parsedSort = controller.parseSort('{"property": -1}');

      expect(parsedSort).toEqual({ property: -1 });
    });
    it('should parse a sort object with string numbers', () => {
      const parsedSort = controller.parseSort('{"property": "-1"}');

      expect(parsedSort).toEqual({ property: -1 });
    });
    it('should parse a sort object with invalid numbers', () => {
      const parsedSort = controller.parseSort('{"property": "sort"}');

      expect(parsedSort).toEqual({ property: 1 });
    });
    it('should parse a sort object with multiple params', () => {
      const mockSort = {
        propertyOne: -1,
        propertyTwo: 1,
      };
      const parsedSort = controller.parseSort(JSON.stringify(mockSort));

      expect(parsedSort).toEqual(mockSort);
    });
    it('should not fail on invalid string', () => {
      const mockSort = '\{"foo: 1';
      const parsedSort = controller.parseSort(mockSort);

      // eslint-disable-next-line @typescript-eslint/naming-convention
      expect(parsedSort).toEqual({ '1': 1 });
    });
  });
  describe('parseFilter()', () => {
    it('should return empty object when no filter specified', () => {
      const mockQuery: IApiQuery = {
        limit: 0,
        offset: 0,
      };
      const filter = controller.parseFilter(mockQuery.filter);

      expect(filter).toEqual({});
    });
    it('should parse filter for allowed properties', () => {
      const mockQuery: IApiQuery = {
        limit: 0,
        offset: 0,
        filter: `{"${ mockFilters[0] }": 'foo'}`,
      };
      const filter = controller.parseFilter(mockQuery.filter);

      const expectedQuery = {
        ...mockQuery,
        filter: {
          [mockFilters[0]]: 'foo',
        },
      };

      expect(filter).toEqual(expectedQuery.filter);
    });

    it('should not parse filter for not allowed properties', () => {
      const mockQuery: IApiQuery = {
        limit: 0,
        offset: 0,
        filter: '{"bar": \'foo\'}',
      };
      const parsedFilter = controller.parseFilter(mockQuery.filter);

      expect(parsedFilter).toEqual({});
    });
    it('should not fail for invalid string', () => {
      const mockQuery: IApiQuery = {
        limit: 0,
        offset: 0,
        filter: '{"bar": \'foo',
      };
      const parsedFilter = controller.parseFilter(mockQuery.filter);

      expect(parsedFilter).toEqual({});
    });
  });
  describe('"parseDateRange() fills requests dataRange and stats"', () => {
    it('should be possible to parse date ranges from given year and month param', () => {
      const year = 2018;
      const month = 12;
      const mockRequest: any = createRequest({
        params: {
          year: year.toString(),
          month: month.toString(),
        },
      });
      const mockResponse: any = createResponse();
      const from = new Date(year, month - 1, 1, 0, 0, 0);
      const to = new Date(year, month, 1, 0, 0, 0);
      const expectedDataRange = {
        $and: [
          {
            date: {
              $gte: from,
            },
          }, {
            date: {
              $lt: to,
            },
          },
        ],
      };

      controller.parseDateRange(mockRequest, mockResponse, mockNext, '', '');
      expect(mockRequest.dateRange).toEqual(expectedDataRange);
      expect(mockRequest.stats.range).toEqual({ from, to });
    });
    it('should be possible to parse date range from a year param', () => {
      const year = 2018;
      const mockRequest: any = createRequest({
        params: {
          year: year.toString(),
        },
      });
      const mockResponse: any = createResponse();
      const from = new Date(year, 0, 1, 0, 0, 0);
      const to = new Date(year, 12, 1, 0, 0, 0);
      const expectedDataRange = {
        $and: [
          {
            date: {
              $gte: from,
            },
          }, {
            date: {
              $lt: to,
            },
          },
        ],
      };

      controller.parseDateRange(mockRequest, mockResponse, mockNext, '', '');
      expect(mockRequest.dateRange).toEqual(expectedDataRange);
      expect(mockRequest.stats.range).toEqual({ from, to });
    });
    it('should not fail to parse date range from an invalid year param', () => {
      const year = 'foo';
      const mockRequest: any = createRequest({
        params: {
          year: year.toString(),
        },
      });
      const mockResponse: any = createResponse();

      controller.parseDateRange(mockRequest, mockResponse, mockNext, '', '');
      expect(mockRequest.dateRange).toBeFalsy();
      expect(mockRequest.stats).toBeFalsy();
    });
    it(
      'should not fail to parse date range from a valid year but invalid month param',
      () => {
        const year = 2019;
        const month = 'foo';
        const mockRequest: any = createRequest({
          params: {
            year: year.toString(),
            month: month,
          },
          stats: {},
        });
        const mockResponse: any = createResponse();

        const from = new Date(year, 0, 1, 0, 0, 0);
        const to = new Date(year, 12, 1, 0, 0, 0);
        const expectedDataRange = {
          $and: [
            {
              date: {
                $gte: from,
              },
            }, {
              date: {
                $lt: to,
              },
            },
          ],
        };

        controller.parseDateRange(mockRequest, mockResponse, mockNext, '', '');
        expect(mockRequest.dateRange).toEqual(expectedDataRange);
        expect(mockRequest.stats.range).toEqual({ from, to });
      });
  });
  describe('"statistics()"', () => {
    let mockRequest: MockRequest<any>;
    let mockResponse: MockResponse<any>;
    let error: any;
    let statsResults: any;
    let nrOfDocuments: number;
    beforeEach(() => {
      mockRequest = createRequest({});
      mockResponse = createResponse();
      error = null;
      statsResults = {
        statsOne: 0,
        statsTwo: 300,
        statsThree: -12,
      };
      nrOfDocuments = 111;
      mockModel.statistics = jest.fn(() => {
        if (error) {
          return Promise.reject(error);
        } else {
          return Promise.resolve(statsResults);
        }
      });

      jest.fn((_query, callback) => callback(error, statsResults));

      mockModel.countDocuments = jest.fn().mockImplementation(() => {
        // something is odd here, countDocuments is not a promise
        if (error) {
          return Promise.reject(error);
        } else {
          return Promise.resolve(nrOfDocuments);
        }
      });
    });

    it('should append statistics to request if model has statistics function', async() => {
      await controller.statistics(mockRequest, mockResponse, mockNext);

      expect(mockRequest.stats[mockModel.collection.name]).toEqual(statsResults);
    });
    it('should not recreate stats object if already exists', async () => {
      mockRequest.stats = {
        otherStats: 'something',
      };

      await controller.statistics(mockRequest, mockResponse, mockNext);

      expect(mockRequest.stats[mockModel.collection.name]).toEqual(statsResults);
    });
    it('should respond with server error if error occurs', async() => {
      error = {
        id: 'error',
        message: 'message',
      };
      statsResults = null;

      await controller.statistics(mockRequest, mockResponse, mockNext);

      expect(mockResponse.statusCode).toBe(500);
      expect(JSON.parse(mockResponse._getData())).toEqual({ error });
    });
    it('should append count of objects to request if model has no statistics function',
      async() => {
        delete mockModel.statistics;

        await controller.statistics(mockRequest, mockResponse, mockNext);

        expect(mockRequest.stats[mockModel.collection.name]).toEqual(nrOfDocuments);
      });
    it(
      'should add minimal stats and not create stats if stats already exists on req',
      async() => {
        nrOfDocuments = 1000;
        statsResults = null;
        delete mockModel.statistics;
        mockRequest.stats = {
          otherStats: 'something',
        };
        await controller.statistics(mockRequest, mockResponse, mockNext);

        expect(mockRequest.stats[mockModel.collection.name]).toEqual(nrOfDocuments);
      });
    it('should respond with server error if error occurs', async() => {
      error = {
        id: 'error',
        message: 'message',
      };
      nrOfDocuments = null;
      statsResults = null;
      delete mockModel.statistics;

      await controller.statistics(mockRequest, mockResponse, mockNext);

      expect(mockResponse.statusCode).toBe(500);
      expect(JSON.parse(mockResponse._getData())).toEqual({ error });
    });

    afterEach(() => {
      delete mockModel.statistics;
      delete mockModel.countDocuments;
    });
  });
  describe('"statsResponse()"', () => {
    let mockRequest: MockRequest<any>;
    let mockResponse: MockResponse<any>;
    beforeEach(() => {
      mockRequest = createRequest({});
      mockResponse = createResponse();
    });
    it('should respond with stats', () => {
      controller.statsResponse(mockRequest, mockResponse, mockNext);

      expect(JSON.parse(mockResponse._getData())).toEqual({});
      expect(mockResponse.statusCode).toBe(200);
    });
    it('should not recreate stats if exists', () => {
      const stats = {
        this: 'that',
      };
      mockRequest.stats = stats;
      controller.statsResponse(mockRequest, mockResponse, mockNext);

      expect(JSON.parse(mockResponse._getData())).toEqual(stats);
    });
  });
  describe('"findById()"', () => {
    let mockRequest: MockRequest<any>;
    let mockResponse: MockResponse<any>;
    let _model: MockModelDocument;
    let error: any;
    let populate: PopulateOptions;
    beforeEach(() => {
      mockRequest = createRequest({});
      mockResponse = createResponse();
      error = null;
      _model = new mockModel();
      mockModel.findById = jest.fn().mockImplementation((_id) => ({
        populate: function (foreignProperties: PopulateOptions[]): IMockModel {
          foreignProperties
            .forEach((foreignProperty) => {
              const key = foreignProperty.path as keyof IMockModel;
              (_model as any)[key] = foreignProperty.select;
            });
          return this;
        },
        exec: jest.fn(() => {
          if (error) {
            return Promise.reject(error);
          } else {
            return Promise.resolve(_model);
          }
        }),
      }));
    });

    it('should append model to request if it has a valid id', async() => {
      const id = new Types.ObjectId();
      _model = new mockModel({ id: id });

      await controller.findById(mockRequest, mockResponse, mockNext, id);

      expect(mockRequest.model).toEqual(_model);
    });
    it('should respond 500 if error occurs', async() => {
      const id = new Types.ObjectId();
      _model = null;
      error = {
        id: 'serverError',
        message: 'message',
      };

      await controller.findById(mockRequest, mockResponse, mockNext, id);

      expect(mockResponse.statusCode).toBe(500);
      expect(JSON.parse(mockResponse._getData())).toEqual({ error });
    });
    it('should return 404 if model not found', async() => {
      const id = new Types.ObjectId();
      const err: IApiError = {
        id: 'notFound',
        message: `${ mockModel.modelName } ${ id } does not exist`,
        errors: [],
        fields: {},
      };
      _model = null;

      await controller.findById(mockRequest, mockResponse, mockNext, id);

      expect(mockResponse.statusCode).toBe(404);
      expect(JSON.parse(mockResponse._getData())).toEqual({ error: err });
    });
    it('should return 400 if invalid model id', async() => {
      error = {
        id: 'invalidId',
        message: 'Invalid id',
        errors: [],
        fields: {},
      };
      _model = null;

      await controller.findById(mockRequest, mockResponse, mockNext, 'id');

      expect(mockResponse.statusCode).toBe(400);
      expect(JSON.parse(mockResponse._getData())).toEqual({ error });
    });
    it('should populate model with requested data', async() => {
      const id = new Types.ObjectId();
      populate = {
        path: 'foo',
        select: 'bar baz',
      };
      _model = new mockModel({ id: id });

      await controller.findById(mockRequest, mockResponse, mockNext, id, '', [ populate ]);

      expect(mockRequest.model).toHaveProperty(populate.path, populate.select);
    });

    afterEach(() => {
      delete mockModel.findById;
    });
  });
  describe('"index()"', () => {
    let mockRequest: MockRequest<any>;
    let mockResponse: MockResponse<any>;
    let error: any;
    let data: MockModelDocument[] = [];
    let query: IApiQuery;
    beforeEach(() => {
      mockResponse = createResponse();
      mockResponse.query = {}; // a mock model to test against;
      error = null;
      data = [ new mockModel(), new mockModel() ];
      query = {
        q: '',
        offset: 0,
        limit: 100,
        populate: [],
        select: null,
        sort: null,
      };
      (mockModel as any).find = jest.fn((q: any): any => { // TODO: fix any
        mockResponse.query.q = q;
        const mockFind = {
          limit: (limit: any): any => {
            mockResponse.query.limit = limit;
            return mockFind;
          },
          skip: (skip: any): any => {
            mockResponse.query.offset = skip;
            return mockFind;
          },
          sort: (sort: any): any => {
            mockResponse.query.sort = sort;
            return mockFind;
          },
          select: (select: any): any => {
            mockResponse.query.select = select;
            return mockFind;
          },
          populate: (populate: any): any => {
            mockResponse.query.populate = populate;
            return mockFind;
          },
          exec: jest.fn().mockImplementation(() => {
            if (error) {
              return Promise.reject(error);
            } else {
              return Promise.resolve(data);
            }
          }),
        };

        return mockFind;
      });
    });
    it('should return a list of models', async () => {
      mockRequest = createRequest({});

      await controller.index(mockRequest, mockResponse, mockNext);

      expect(mockRequest.data).toEqual(data);
      expect(mockResponse.query).toEqual({
        ...query,
        q: {},
        select: {},
      });
    });
    it('should allow to change query', async() => {
      query.offset = '10';
      query.limit = '10';
      mockRequest = createRequest({
        query,
      });

      await controller.index(mockRequest, mockResponse, mockNext);

      expect(mockResponse.query.offset).toEqual(parseInt(query.offset, 10));
      expect(mockResponse.query.limit).toEqual(parseInt(query.limit, 10));
    });
    it('should add sort and filters', async() => {
      query.sort = 'type';
      query.filter = 'type';
      mockRequest = createRequest({
        query,
      });

      await controller.index(mockRequest, mockResponse, mockNext);

      expect(mockResponse.query.sort).toEqual({ type: 1 });
      /* don't check for filter, they are checked above  */
    });
    it('should call model parseQuery, if exists', async() => {
      query.sort = 'type';
      query.filter = 'type';
      mockRequest = createRequest({
        query,
      });

      mockModel.parseQuery = jest.fn().mockImplementation((_query: IApiQuery) => _query);

      await controller.index(mockRequest, mockResponse, mockNext);

      expect(mockResponse.query.sort).toEqual({ type: 1 });
      /* don't check for filter, they are checked above */
    });
    it('should return error, if error occurs', async() => {
      query.sort = 'type';
      query.filter = 'type';
      mockRequest = createRequest({
        query,
      });
      error = {
        id: 'mockError',
        message: 'mockMessage',
      };

      await controller.index(mockRequest, mockResponse, (err: any) => {
        expect(err).toEqual(error);
      });

      expect(mockResponse.query.sort).toEqual({ type: 1 });
      /* don't check for filter, they are checked above */
    });
  });
  describe('"read()"', () => {
    let mockRequest: MockRequest<any>;
    let mockResponse: MockResponse<any>;
    let _model: MockModelDocument;
    beforeEach(() => {
      _model = new mockModel({ property: 'bar' });
      mockResponse = createResponse();
    });
    it('should return model as json', () => {
      mockRequest = createRequest({
        model: _model,
      });

      controller.read(mockRequest, mockResponse, mockNext);

      const clone = JSON.parse(JSON.stringify(_model.toObject())); // needs parsing of id
      expect(mockResponse.statusCode).toBe(200);
      expect(JSON.parse(mockResponse._getData())).toEqual(clone);
    });
    it('should return model not found if no model', () => {
      const err: IApiError = {
        id: 'modelMissing',
        message: 'the model is missing in the request',
        errors: [],
        fields: {},
      };
      _model = null;
      mockRequest = createRequest({});

      controller.read(mockRequest, mockResponse, mockNext);

      expect(mockResponse.statusCode).toBe(400);
      expect(JSON.parse(mockResponse._getData())).toEqual({ error: err });

    });
  });
  describe('"create()"', () => {
    let mockRequest: MockRequest<any>;
    let mockResponse: MockResponse<any>;
    let body: any;
    const user = 'test';
    let error: any;
    let _model: MockModelDocument;
    beforeEach(() => {
      error = null;
      body = {
        property: 'something',
      };
      mockResponse = createResponse();
      mockModel.prototype.save = jest.fn(function () {
        if (error) {
          return Promise.reject(error);
        } else {
          return Promise.resolve(this);
        }
      });
      _model = new mockModel(body);
      mockRequest = createRequest({
        user: {
          username: user,
        },
        body,
      });
    });
    it('should create a model', async() => {
      await controller.create(mockRequest, mockResponse, mockNext);

      expect(mockResponse.statusCode).toBe(201);
      const response = JSON.parse(mockResponse._getData());
      expect(response).toHaveProperty('property', body.property);
      expect(response).toHaveProperty('timestamps.created.by', user);
    });
    it('should return validation error if error occurs', async() => {
      error = {
        name: 'ValidationError',
        id: 'validationError',
        message: 'the model has invalid properties',
        errors: 'fields',
      };

      await controller.create(mockRequest, mockResponse, mockNext);

      expect(mockResponse.statusCode).toBe(400);
      expect(JSON.parse(mockResponse._getData())).toEqual({
        error: {
          id: error.id,
          message: error.message,
          fields: error.errors,
          errors: [],
        },
      });
    });
    it('should return duplicate error if is duplication', async() => {
      error = {
        code: 11000,
      };

      await controller.create(mockRequest, mockResponse, mockNext);

      expect(mockResponse.statusCode).toBe(400);
      expect(JSON.parse(mockResponse._getData())).toEqual({
        error: {
          id: 'duplicate',
          message: `${ mockModel.modelName } already exists`,
        },
      });
    });
    it('should return next error, if not duplicate nor validation error', async() => {
      error = {
        message: 'something',
      };
      await controller.create(mockRequest, mockResponse, (err: any) => {
        expect(err).toEqual(error);
      });

    });
    it('should not allow to set timestamps or id', async() => {
      body = {
        property: 'something else',
        _id: 5,
        id: 17,
        timestamps: {
          crated: {
            at: new Date(),
            by: 'someone',
          },
        },
      };
      _model = new mockModel(body);
      mockRequest = createRequest({
        user: {
          username: user,
        },
        body,
      });

      await controller.create(mockRequest, mockResponse, mockNext);

      expect(mockResponse.statusCode).toBe(201);
      const response = JSON.parse(mockResponse._getData());
      expect(response).toHaveProperty('property', body.property);
      expect(response).toHaveProperty('timestamps.created.by', user);
      expect(response).not.toHaveProperty('_id', body._id);
      expect(response).not.toHaveProperty('id', body.id);

    });
    afterEach(() => {
      delete _model.save;
    });
  });
  describe('"update()"', () => {
    let mockRequest: MockRequest<any>;
    let mockResponse: MockResponse<any>;
    let body: any;
    const user = 'test';
    let error: any;
    let _model: MockModelDocument;
    beforeEach(() => {
      error = null;
      body = {
        property: 'something',
      };
      mockResponse = createResponse();
      mockModel.prototype.save = jest.fn(function () {
        if (error) {
          return Promise.reject(error);
        } else {
          return Promise.resolve(this);
        }
      });
      _model = new mockModel({ property: 'initial' });
      mockRequest = createRequest({
        user: {
          username: user,
        },
        model: _model,
        body,
      });
    });
    it('should update a model', async () => {
      await controller.update(mockRequest, mockResponse, mockNext);

      expect(mockResponse.statusCode).toBe(200);
      const response = JSON.parse(mockResponse._getData());
      expect(response).toHaveProperty('property', body.property);
      expect(response).toHaveProperty('timestamps.updated.by', user);
    });
    it('should return validation error if error occurs', async() => {
      error = {
        name: 'ValidationError',
        id: 'validationError',
        message: 'the model has invalid properties',
        errors: 'fields',
      };

      await controller.update(mockRequest, mockResponse, mockNext);

      expect(mockResponse.statusCode).toBe(400);
      expect(JSON.parse(mockResponse._getData())).toEqual({
        error: {
          id: error.id,
          message: error.message,
          fields: error.errors,
          errors: [],
        },
      });
    });
    it('should return next error, if its not a validation error', async() => {
      error = {
        message: 'something',
      };
      await controller.update(mockRequest, mockResponse, (err: any) => {
        expect(err).toEqual(error);
      });

    });
    it('should return model missing, if model is missing', async() => {
      error = {
        id: 'modelMissing',
        message: 'the model is missing in the request',
        errors: [],
        fields: {},
      };
      mockRequest = createRequest({
        user: {
          username: user,
        },
        body,
      });

      await controller.update(mockRequest, mockResponse, mockNext);

      expect(mockResponse.statusCode).toBe(400);
      expect(JSON.parse(mockResponse._getData())).toEqual({ error });

    });
    it('should not allow to set timestamps or id', async() => {
      body = {
        property: 'something else',
        _id: null,
        id: 17,
        timestamps: {
          crated: {
            at: new Date(),
            by: 'someone',
          },
        },
      };
      _model = new mockModel({ property: 'initial' });
      mockRequest = createRequest({
        user: {
          username: user,
        },
        model: _model,
        body,
      });

      await controller.update(mockRequest, mockResponse, mockNext);

      expect(mockResponse.statusCode).toBe(200);
      const response = JSON.parse(mockResponse._getData());
      expect(response).toHaveProperty('property', body.property);
      expect(response).toHaveProperty('timestamps.updated.by', user);
      expect(response).not.toHaveProperty('_id', body._id);
      expect(response).not.toHaveProperty('id', body.id);

    });
    afterEach(() => {
      delete _model.save;
    });
  });
  describe('"softDelete()"', () => {
    let mockRequest: MockRequest<any>;
    let mockResponse: MockResponse<any>;
    let error: any;
    let _model: MockModelDocument;
    const user = 'test';
    beforeEach(() => {
      error = null;
      mockResponse = createResponse();
      mockModel.prototype.save = jest.fn(function () {
        if (error) {
          return Promise.reject(error);
        } else {
          return Promise.resolve(this);
        }
      });
      _model = new mockModel({ property: 'something' });
      mockRequest = createRequest({
        model: _model,
        user: {
          username: user,
        },
      });
    });
    it('should mark a model as deleted', async () => {
      await controller.softDelete(mockRequest, mockResponse, mockNext);

      expect(mockResponse.statusCode).toBe(200);
      const response = JSON.parse(mockResponse._getData());
      expect(response).toHaveProperty('mark.deleted', true);
    });
    it('should return deletion error if error occurs', async () => {
      error = {
        id: 'delete',
        message: 'the model has invalid properties',
        errors: [],
        fields: {},
      };

      await controller.softDelete(mockRequest, mockResponse, mockNext);

      expect(mockResponse.statusCode).toBe(400);
      expect(JSON.parse(mockResponse._getData())).toEqual({ error });
    });
    it('should return model missing, if model is missing', async () => {
      error = {
        id: 'modelMissing',
        message: 'the model is missing in the request',
        errors: [],
        fields: {},
      };
      mockRequest = createRequest({});

      await controller.softDelete(mockRequest, mockResponse, mockNext);

      expect(mockResponse.statusCode).toBe(400);
      expect(JSON.parse(mockResponse._getData())).toEqual({ error });

    });
    afterEach(() => {
      delete _model.save;
    });
  });
  describe('"delete()"', () => {
    let mockRequest: MockRequest<any>;
    let mockResponse: MockResponse<any>;
    let error: any;
    let _model: MockModelDocument;
    beforeEach(() => {
      error = null;
      mockResponse = createResponse();
      mockModel.deleteOne = jest.fn().mockReturnValue(Promise.resolve(_model));
      _model = new mockModel({ property: 'something' });
      mockRequest = createRequest({
        model: _model,
      });
    });
    it('should remove a model', async () => {
      await controller.delete(mockRequest, mockResponse, mockNext);

      expect(mockResponse.statusCode).toBe(200);
      const response = JSON.parse(mockResponse._getData());
      expect(response._id.toString()).toEqual(_model._id.toString());
    });
    it('should return model missing, if model is missing', () => {
      error = {
        id: 'modelMissing',
        message: 'the model is missing in the request',
        errors: [],
        fields: {},
      };
      mockRequest = createRequest({});

      controller.delete(mockRequest, mockResponse, mockNext);

      expect(mockResponse.statusCode).toBe(400);
      expect(JSON.parse(mockResponse._getData())).toEqual({ error });

    });
    it('should return error if error occurs', () => {
      _model = new mockModel({ property: 'something' });
      mockRequest = createRequest({
        model: _model,
      });
      error = {
        id: 'mockError',
        message: 'mock message',
      };

      controller.delete(mockRequest, mockResponse, (err: any) => {
        expect(err).toEqual(error);
      });
    });
    afterEach(() => {
      delete _model.save;
    });
  });
  describe('"apiResponse()"', () => {
    let mockRequest: MockRequest<any>;
    let mockResponse: MockResponse<any>;
    let data: MockModelDocument[] = [];
    let meta: IApiMeta;
    beforeEach(() => {
      mockResponse = createResponse();
      meta = {
        total: 100,
        count: 10,
        offset: 0,
        limit: 100,
      };
      data = [ new mockModel(), new mockModel() ];
    });
    it('should generate an api response', () => {
      mockRequest = createRequest({
        data: data,
        meta: meta,
      });

      controller.apiResponse(mockRequest, mockResponse, mockNext);

      expect(mockResponse.statusCode).toBe(200);
      const response = JSON.parse(mockResponse._getData());
      expect(response.data.length).toEqual(data.length);
      expect(response.meta).toEqual(meta);
    });
    it('should return a 500 error on meta error', () => {
      meta.error = {
        id: 'foo',
        message: 'bar',
        errors: [],
        fields: {},
      };
      mockRequest = createRequest({
        data: data,
        meta: meta,
      });

      controller.apiResponse(mockRequest, mockResponse, mockNext);

      expect(mockResponse.statusCode).toBe(500);
      const response = JSON.parse(mockResponse._getData());
      expect(response.data.length).toEqual(data.length);
      expect(response.meta).toEqual(meta);
    });
  });
  describe('"populateMeta()"', () => {
    let mockRequest: MockRequest<any>;
    let mockResponse: MockResponse<any>;
    let data: MockModelDocument[] = [];
    let error: any;
    let nrOfDocuments: number;
    beforeEach(() => {
      mockResponse = createResponse();
      error = null;
      data = [ new mockModel(), new mockModel() ];
      nrOfDocuments = 111;
      mockModel.countDocuments =
        jest.fn().mockImplementation((_query) => Promise.resolve(nrOfDocuments));
    });
    it('should append meta data to request with strings', async() => {
      const offset = '10';
      const limit = '10';
      mockRequest = createRequest({
        data: data,
        modelQuery: {
          total: 'query',
          offset: offset,
          limit: limit,
        },
      });

      await controller.populateMeta(mockRequest, mockResponse, mockNext);

      expect(mockRequest.meta).toBeTruthy();
      expect(mockRequest.meta.total).toEqual(nrOfDocuments);
      expect(mockRequest.meta.count).toEqual(data.length);
      expect(mockRequest.meta.offset).toEqual(parseInt(offset, 10));
      expect(mockRequest.meta.limit).toEqual(parseInt(limit, 10));
    });
    it('should append meta data to request with numbers', async() => {
      const offset = 10;
      const limit = 10;
      mockRequest = createRequest({
        data: data,
        modelQuery: {
          offset: offset,
          limit: limit,
        },
      });

      await controller.populateMeta(mockRequest, mockResponse, mockNext);

      expect(mockRequest.meta).toBeTruthy();
      expect(mockRequest.meta.total).toEqual(nrOfDocuments);
      expect(mockRequest.meta.count).toEqual(data.length);
      expect(mockRequest.meta.offset).toEqual(offset);
      expect(mockRequest.meta.limit).toEqual(limit);
    });
    it('should append meta data even if no data found', async() => {
      const offset = 10;
      const limit = 10;
      mockRequest = createRequest({
        data: null,
        modelQuery: {
          offset: offset,
          limit: limit,
        },
      });

      await controller.populateMeta(mockRequest, mockResponse, mockNext);

      expect(mockRequest.meta).toBeTruthy();
      expect(mockRequest.meta.total).toEqual(nrOfDocuments);
      expect(mockRequest.meta.count).toEqual(0);
      expect(mockRequest.meta.offset).toEqual(offset);
      expect(mockRequest.meta.limit).toEqual(limit);
    });
    it('should return next, if error occurs', async () => {
      mockRequest = createRequest({
        modelQuery: {},
      });
      error = {
        id: 'mockError',
        message: 'mockMessage',
        errors: [],
        fields: {},
      };

      mockModel.countDocuments =
      jest.fn().mockImplementation((_query) => Promise.reject(error));

      await controller.populateMeta(mockRequest, mockResponse, mockNext);

      expect(mockNext).toHaveBeenCalledWith(error);
    });
  });
  describe('"parsePagination()"', () => {
    it('should also parse pagination on a string value', () => {
      const mockValue = controller.parsePagination('10', '100');

      expect(mockValue).toBe(10);
    });
  });

});
