/*
 * Copyright (C) 2019 StApps
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
import {
  SCNotFoundErrorResponse,
  SCPluginAdd,
  SCPluginAlreadyRegisteredErrorResponse,
  SCPluginRegisterResponse,
  SCPluginRegisterRoute,
  SCPluginRemove,
  SCValidationErrorResponse,
} from '@openstapps/core';
import nock from 'nock';
import {plugins} from '../../src/common.js';
import {pluginRegisterHandler} from '../../src/routes/plugin-register-route.js';
import {expect, use} from 'chai';
import chaiAsPromised from 'chai-as-promised';
import {DEFAULT_TEST_TIMEOUT} from '../common.js';
import {testApp} from '../tests-setup.js';
import {backendConfig} from '../../src/config.js';
import registerRequest from '@openstapps/core/test/resources/PluginRegisterRequest.1.json' assert {type: 'json'};

// for using promises in expectations (to.eventually.be...)
use(chaiAsPromised);

// cast it because of "TS2322: Type 'string' is not assignable to type '"add"'"
export const registerAddRequest: SCPluginAdd = registerRequest.instance as SCPluginAdd;

export const registerRemoveRequest: SCPluginRemove = {
  action: 'remove',
  route: registerAddRequest.plugin.route,
};

describe('Plugin registration', async function () {
  const bodySuccess: SCPluginRegisterResponse = {success: true};
  describe('Middleware', async function () {
    after(function () {
      // remove plugins
      plugins.clear();
      backendConfig.app.features = {};
    });

    it('should register a plugin', async function () {
      // register one plugin
      const response = await pluginRegisterHandler(registerAddRequest, {});

      expect(response).to.deep.equal(bodySuccess) &&
        expect(plugins.size).to.equal(1) &&
        expect(backendConfig.app.features.plugins!['Foo Plugin']).to.not.be.empty;
    });

    it('should allow re-registering the same plugin', async function () {
      // register one plugin
      await pluginRegisterHandler(registerAddRequest, {});

      // register the same plugin again
      const response = await pluginRegisterHandler(registerAddRequest, {});

      return (
        expect(response).to.deep.equal(bodySuccess) &&
        expect(plugins.size).to.equal(1) &&
        expect(backendConfig.app.features.plugins!['Foo Plugin']).to.not.be.empty
      );
    });

    it('should show an error if a plugin has already been registered', async function () {
      // register one plugin
      await pluginRegisterHandler(registerAddRequest, {});

      // create new request for adding a plugin with only name that changed
      const registerAddRequestAltered: SCPluginAdd = {
        ...registerAddRequest,
        plugin: {...registerAddRequest.plugin, name: registerAddRequest.plugin.name + 'foo'},
      };

      // register the same plugin again
      expect(pluginRegisterHandler(registerAddRequestAltered, {}))
        .to.eventually.be.rejectedWith('Plugin already registered')
        .and.be.an.instanceOf(SCPluginAlreadyRegisteredErrorResponse)
        // check that the right plugin information (of the previously registered plugin) is provided with the error
        .and.have.property('additionalData', registerAddRequest.plugin);
    });

    it('should remove plugin if it exists', async function () {
      // register one plugin
      await pluginRegisterHandler(registerAddRequest, {});

      expect(plugins.size).to.equal(1);

      const response = await pluginRegisterHandler(registerRemoveRequest, {});

      expect(response).to.deep.equal(bodySuccess) &&
        expect(plugins.size).to.equal(0) &&
        expect(backendConfig.app.features.plugins).to.be.empty;
    });

    it('should throw a "not found" error when removing a plugin whose registered route does not exist', async function () {
      // register one plugin
      await pluginRegisterHandler(registerAddRequest, {});

      expect(pluginRegisterHandler({...registerRemoveRequest, route: '/not-foo'}, {}))
        .to.eventually.be.rejectedWith('Resource not found')
        .and.be.an.instanceOf(SCNotFoundErrorResponse);
    });
  });

  describe('Routes', async function () {
    // increase timeout for the suite
    this.timeout(DEFAULT_TEST_TIMEOUT);
    const pluginRegisterRoute = new SCPluginRegisterRoute();
    const validationError = new SCValidationErrorResponse([]);
    const notFoundError = new SCNotFoundErrorResponse();
    // @ts-expect-error not assignable
    const alreadyRegisteredError = new SCPluginAlreadyRegisteredErrorResponse('Foo Message', {});

    afterEach(async function () {
      // remove routes
      plugins.clear();
      // clean up request mocks (fixes issue with receiving response from mock from previous test case)
      nock.cleanAll();
    });

    it('should provide "not found" (404) if plugin/route is not registered', async function () {
      // lets simulate that the plugin is already registered
      plugins.set(registerAddRequest.plugin.route, registerAddRequest.plugin);
      // mock response of the plugin address
      nock('http://foo.com:1234')
        .post('/foo')
        .reply(200, {result: [{foo: 'bar'}, {bar: 'foo'}]});

      const {status} = await testApp
        .post('/not-foo')
        .set('Content-Type', 'application/json')
        .set('Accept', 'application/json')
        .send({query: 'foo'});

      expect(status).to.be.equal(notFoundError.statusCode);
    });

    it('should respond with bad request if when providing invalid request', async function () {
      const {status} = await testApp
        .post(pluginRegisterRoute.urlPath)
        .set('Content-Type', 'application/json')
        .set('Accept', 'application/json')
        .send({foo: 'bar'});

      expect(status).to.be.equal(validationError.statusCode);
    });

    it('should respond with an error if something went wrong with a valid request', async function () {
      // lets simulate that a plugin is already registered
      plugins.set(registerAddRequest.plugin.route, registerAddRequest.plugin);
      // registering a different plugin for the same route causes the expected error
      const registerAddRequestAltered = {
        ...registerAddRequest,
        plugin: {...registerAddRequest.plugin, name: 'FooBar Plugin'},
      };

      const {status, body} = await testApp
        .post(pluginRegisterRoute.urlPath)
        .set('Content-Type', 'application/json')
        .set('Accept', 'application/json')
        .send(registerAddRequestAltered);

      expect(status).to.be.equal(alreadyRegisteredError.statusCode);
      expect(body).to.haveOwnProperty('name', 'SCPluginAlreadyRegisteredError');
    });

    it('should respond with success when de-registering already registered plugin', async function () {
      // lets simulate that a plugin is already registered
      plugins.set(registerAddRequest.plugin.route, registerAddRequest.plugin);

      const {status, body} = await testApp
        .post('/plugin/register')
        .set('Content-Type', 'application/json')
        .set('Accept', 'application/json')
        .send(registerRemoveRequest);

      expect(status).to.be.equal(new SCPluginRegisterRoute().statusCodeSuccess);
      expect(body).to.be.deep.equal(bodySuccess);
    });

    it('should response with 404 when deleting a plugin which was not registered', async function () {
      // lets simulate that the plugin is already registered
      plugins.set(registerAddRequest.plugin.route, registerAddRequest.plugin);

      const {status} = await testApp
        .post('/plugin/register')
        .set('Content-Type', 'application/json')
        .set('Accept', 'application/json')
        // using a different route for the remove request
        .send({action: 'remove', route: '/not-foo'} as SCPluginRemove);

      expect(status).to.be.equal(notFoundError.statusCode);
    });
  });
});
