/* eslint-disable unicorn/no-null */
/*
 * Copyright (C) 2018 StApps
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation, version 3.
 *
 * 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 General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program. If not, see <https://www.gnu.org/licenses/>.
 */
import {
  isThing,
  SCBulkAddResponse,
  SCBulkAddRoute,
  SCBulkDoneResponse,
  SCBulkDoneRoute,
  SCBulkResponse,
  SCBulkRoute,
  SCMessage,
  SCThingOriginType,
  SCThingType,
  SCThingUpdateResponse,
  SCThingUpdateRoute,
  SCThingWithoutReferences,
} from '@openstapps/core';
import chai from 'chai';
import {expect} from 'chai';
import chaiAsPromised from 'chai-as-promised';
import chaiSpies from 'chai-spies';
import traverse from 'traverse';
import {
  ConnectorClient,
  EmptyBulkError,
  NamespaceNotDefinedError,
  HttpClient,
  HttpClientRequest,
  HttpClientResponse,
} from '../src/index.js';
import path from 'path';
import {fileURLToPath} from 'url';
import {readdir, readFile} from 'fs/promises';

chai.should();
chai.use(chaiSpies);
chai.use(chaiAsPromised);

const sandbox = chai.spy.sandbox();

const bulkAddRoute = new SCBulkAddRoute();
const bulkDoneRoute = new SCBulkDoneRoute();
const bulkRoute = new SCBulkRoute();
const thingUpdateRoute = new SCThingUpdateRoute();

const httpClient = new HttpClient();

/**
 * Check if something contains things
 *
 * @param thing Thing to check
 */
function doesContainThings<T extends SCThingWithoutReferences>(thing: T): boolean {
  /* tslint:disable-next-line:only-arrow-functions */
  return traverse(thing).reduce(function (sum, item) {
    if (this.isRoot) {
      return false;
    }

    return sum || item === null ? false : isThing(item);
  }, false);
}

describe('ConnectorClient', function () {
  afterEach(function () {
    sandbox.restore();
  });

  it('should bulk', async function () {
    let expiration = 'ERROR';
    sandbox.on(httpClient, 'request', async (response): Promise<HttpClientResponse<SCBulkResponse>> => {
      expiration = response?.body?.expiration ?? 'ERROR';
      return {
        body: {
          expiration,
          source: 'foo',
          state: 'in progress',
          type: SCThingType.Message,
          uid: 'foo',
        },
        headers: {},
        statusCode: bulkRoute.statusCodeSuccess,
      };
    });

    expect(httpClient.request).not.to.have.been.called();

    const connectorClient = new ConnectorClient(httpClient, 'http://localhost');
    await connectorClient.bulk(SCThingType.Message, 'foo', 1800);

    expect(httpClient.request).to.have.been.first.called.with({
      body: {
        expiration,
        source: 'foo',
        type: SCThingType.Message,
      },
      headers: {
        'Content-Type': 'application/json',
      },
      method: bulkRoute.method,
      url: new URL('http://localhost' + bulkRoute.getUrlPath()),
    });
  });

  it('should bulk without timeout', async function () {
    let expiration = 'ERROR';
    sandbox.on(httpClient, 'request', async (request): Promise<HttpClientResponse<SCBulkResponse>> => {
      expiration = request?.body?.expiration ?? 'ERROR';
      return {
        body: {
          expiration,
          source: 'foo',
          state: 'in progress',
          type: SCThingType.Message,
          uid: 'foo',
        },
        headers: {},
        statusCode: bulkRoute.statusCodeSuccess,
      };
    });

    expect(httpClient.request).not.to.have.been.called();

    const connectorClient = new ConnectorClient(httpClient, 'http://localhost');
    await connectorClient.bulk(SCThingType.Message, 'foo');

    expect(httpClient.request).to.have.been.first.called.with({
      body: {
        expiration,
        source: 'foo',
        type: SCThingType.Message,
      },
      headers: {
        'Content-Type': 'application/json',
      },
      method: bulkRoute.method,
      url: new URL('http://localhost' + bulkRoute.getUrlPath()),
    });
  });

  it('should index', async function () {
    const messages: SCMessage[] = [
      {
        audiences: ['employees'],
        categories: ['news'],
        messageBody: 'Lorem ipsum.',
        name: 'foo',
        origin: {
          indexed: 'foo',
          name: 'foo',
          type: SCThingOriginType.Remote,
        },
        type: SCThingType.Message,
        uid: 'foo',
      },
      {
        audiences: ['employees'],
        categories: ['news'],
        messageBody: 'Lorem ipsum.',
        name: 'foo',
        origin: {
          indexed: 'foo',
          name: 'foo',
          type: SCThingOriginType.Remote,
        },
        type: SCThingType.Message,
        uid: 'bar',
      },
    ];

    type responses = SCBulkResponse | SCBulkAddResponse | SCBulkDoneResponse;

    let expiration = 'ERROR';
    sandbox.on(
      httpClient,
      'request',
      async (request: HttpClientRequest): Promise<HttpClientResponse<responses>> => {
        if (request.url.toString() === new URL('http://localhost' + bulkRoute.getUrlPath()).toString()) {
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
          expiration = (request?.body as any)?.expiration ?? 'ERROR';
          return {
            body: {
              expiration,
              source: 'copy',
              state: 'in progress',
              type: SCThingType.Message,
              uid: 'foo',
            },
            headers: {},
            statusCode: bulkRoute.statusCodeSuccess,
          };
        } else if (
          request.url.toString() ===
          new URL(
            'http://localhost' +
              bulkAddRoute.getUrlPath({
                UID: 'foo',
              }),
          ).toString()
        ) {
          return {
            body: {},
            headers: {},
            statusCode: bulkAddRoute.statusCodeSuccess,
          };
        }

        return {
          body: {},
          headers: {},
          statusCode: bulkDoneRoute.statusCodeSuccess,
        };
      },
    );

    const connectorClient = new ConnectorClient(httpClient, 'http://localhost');
    await connectorClient.index(messages, 'copy');

    expect(httpClient.request).to.have.been.first.called.with({
      body: {
        expiration,
        source: 'copy',
        type: SCThingType.Message,
      },
      headers: {
        'Content-Type': 'application/json',
      },
      method: bulkRoute.method,
      url: new URL('http://localhost' + bulkRoute.getUrlPath()),
    });
  });

  it('should fail to index', async function () {
    const connectorClient = new ConnectorClient(httpClient, 'http://localhost');
    await connectorClient.index([]).should.be.rejectedWith(EmptyBulkError);
  });

  it('should index without source', async function () {
    const messages: SCMessage[] = [
      {
        audiences: ['employees'],
        categories: ['news'],
        messageBody: 'Lorem ipsum.',
        name: 'foo',
        origin: {
          indexed: 'foo',
          name: 'foo',
          type: SCThingOriginType.Remote,
        },
        type: SCThingType.Message,
        uid: 'foo',
      },
      {
        audiences: ['employees'],
        categories: ['news'],
        messageBody: 'Lorem ipsum.',
        name: 'foo',
        origin: {
          indexed: 'foo',
          name: 'foo',
          type: SCThingOriginType.Remote,
        },
        type: SCThingType.Message,
        uid: 'bar',
      },
    ];

    type responses = SCBulkResponse | SCBulkAddResponse | SCBulkDoneResponse;

    let expiration = 'ERROR';
    sandbox.on(
      httpClient,
      'request',
      async (request: HttpClientRequest): Promise<HttpClientResponse<responses>> => {
        if (request.url.toString() === new URL('http://localhost' + bulkRoute.getUrlPath()).toString()) {
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
          expiration = (request?.body as any)?.expiration ?? 'ERROR';
          return {
            body: {
              expiration,
              source: 'stapps-api',
              state: 'in progress',
              type: SCThingType.Message,
              uid: 'foo',
            },
            headers: {},
            statusCode: bulkRoute.statusCodeSuccess,
          };
        } else if (
          request.url.toString() ===
          new URL(
            'http://localhost' +
              bulkAddRoute.getUrlPath({
                UID: 'foo',
              }),
          ).toString()
        ) {
          return {
            body: {},
            headers: {},
            statusCode: bulkAddRoute.statusCodeSuccess,
          };
        }

        return {
          body: {},
          headers: {},
          statusCode: bulkDoneRoute.statusCodeSuccess,
        };
      },
    );

    const connectorClient = new ConnectorClient(httpClient, 'http://localhost');
    await connectorClient.index(messages);

    expect(httpClient.request).to.have.been.first.called.with({
      body: {
        expiration,
        source: 'stapps-api',
        type: SCThingType.Message,
      },
      headers: {
        'Content-Type': 'application/json',
      },
      method: bulkRoute.method,
      url: new URL('http://localhost' + bulkRoute.getUrlPath()),
    });
  });

  it('should make uuid', async function () {
    const uuid = ConnectorClient.makeUUID('foo', 'b-tu');

    expect(uuid).to.be.equal('abad271e-d9e9-5802-b7bc-96d8a647b451');
    expect(ConnectorClient.makeUUID('bar', 'b-tu')).not.to.be.equal(uuid);
    expect(ConnectorClient.makeUUID('foo', 'f-u')).not.to.be.equal(uuid);
  });

  it('should fail making a uuid', async function () {
    expect(() => {
      ConnectorClient.makeUUID('foo', 'b-u');
    }).to.throw(NamespaceNotDefinedError);
  });

  it('should remove references', async function () {
    const pathToTestFiles = path.resolve(
      path.dirname(fileURLToPath(import.meta.url)),
      '..',
      'node_modules',
      '@openstapps',
      'core',
      'test',
      'resources',
      'indexable',
    );

    const testFiles = await readdir(pathToTestFiles);

    const testInstances = await Promise.all(
      testFiles.map(async testFile => {
        const buffer = await readFile(path.join(pathToTestFiles, testFile));
        const content = JSON.parse(buffer.toString());

        return content.instance;
      }),
    );

    for (const testInstance of testInstances) {
      const checkInstance = JSON.parse(JSON.stringify(testInstance));
      const testInstanceWithoutReferences = ConnectorClient.removeReferences(testInstance);

      expect(doesContainThings(testInstanceWithoutReferences)).to.be.equal(
        false,
        JSON.stringify([testInstance, testInstanceWithoutReferences], null, 2),
      );
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      expect((testInstanceWithoutReferences as any).origin).to.be.equal(
        undefined,
        JSON.stringify([testInstance, testInstanceWithoutReferences], null, 2),
      );
      expect(testInstance).to.be.deep.equal(
        checkInstance,
        'Removing the references of a thing could have side effects because no deep copy is used',
      );
    }
  });

  it('should remove undefined properties', async function () {
    const objectWithUndefinedProperties = {
      value: 'foo',
      novalue: undefined,
      nested: {
        value: 'foo',
        novalue: undefined,
      },
    };
    const objectWithoutUndefinedProperties = {
      value: 'foo',
      nested: {
        value: 'foo',
      },
    };
    ConnectorClient.removeUndefinedProperties(objectWithUndefinedProperties);

    expect(objectWithUndefinedProperties).to.deep.equal(
      objectWithoutUndefinedProperties,
      JSON.stringify([objectWithUndefinedProperties, objectWithoutUndefinedProperties], null, 2),
    );
  });

  it('should update', async function () {
    const message: SCMessage = {
      audiences: ['employees'],
      categories: ['news'],
      messageBody: 'Lorem ipsum.',
      name: 'foo',
      origin: {
        indexed: 'foo',
        name: 'foo',
        type: SCThingOriginType.Remote,
      },
      type: SCThingType.Message,
      uid: 'foo',
    };

    sandbox.on(httpClient, 'request', async (): Promise<HttpClientResponse<SCThingUpdateResponse>> => {
      return {
        body: {},
        headers: {},
        statusCode: thingUpdateRoute.statusCodeSuccess,
      };
    });

    expect(httpClient.request).not.to.have.been.called();

    const connectorClient = new ConnectorClient(httpClient, 'http://localhost');
    await connectorClient.update(message);

    expect(httpClient.request).to.have.been.called.with({
      body: message,
      headers: {
        'Content-Type': 'application/json',
      },
      method: thingUpdateRoute.method,
      url: new URL(
        'http://localhost' +
          thingUpdateRoute.getUrlPath({
            TYPE: SCThingType.Message,
            UID: 'foo',
          }),
      ),
    });
  });
});
