/*
 * Copyright (C) 2019-2022 Open 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/>.
 */
/* eslint-disable unicorn/prevent-abbreviations */

import {SCSearchRequest, SCThings, SCThingType} from '@openstapps/core';
import {Logger} from '@openstapps/logger';
import {deepStrictEqual} from 'assert';
import {readdir, readFile} from 'fs';
import path from 'path';
import {promisify} from 'util';
import {ConnectorClient, HttpClientInterface} from '@openstapps/api';
import junit from 'junit-report-builder';

const localItemMap: Map<string, SCThings> = new Map();
const remoteItemMap: Map<string, SCThings> = new Map();

async function runTest(name: string, scope: () => Promise<void>, suite: junit.TestSuite, errors: string[]) {
  const testCase = suite.testCase().name(name);
  process.stdout.addListener('data', testCase.standardOutput);
  process.stderr.addListener('data', testCase.standardError);

  const start = performance.now();
  await scope().catch(async error => {
    await Logger.error(error);
    testCase.failure(error.message);
    errors.push(error.message);
  });
  process.stdout.removeListener('data', testCase.standardOutput);
  process.stderr.removeListener('data', testCase.standardError);

  const end = performance.now();
  testCase.time((end - start) / 1000);
}

/**
 * Options to set up indexing core test files to backend
 */
export interface E2EOptions {
  /**
   * File path of the directory containing core test files
   */
  samplesLocation: string;

  /**
   * URL of the backend to index to
   */
  to: string;

  /**
   * Location of the report
   */
  reportLocation?: string;
}

/**
 * Function that can be used for integration tests.
 * Adds all the SCThings that getItemsFromSamples() returns to the backend.
 * Afterward, retrieves the items from backend and checks for differences with original ones.
 */
export async function e2eRun(client: HttpClientInterface, options: E2EOptions): Promise<string[]> {
  localItemMap.clear();
  remoteItemMap.clear();
  const builder = junit.newBuilder();
  const errors: string[] = [];

  const api = new ConnectorClient(client, options.to);
  try {
    const indexSuite = builder.testSuite().name('e2e index');
    await indexSamples(api, options, indexSuite, errors);
    Logger.info(`All samples have been indexed via the backend`);

    const retrieveSuite = builder.testSuite().name('e2e retrieve');
    await retrieveItems(api, retrieveSuite, errors);
    Logger.info(`All samples have been retrieved from the backend`);

    const compareSuite = builder.testSuite().name('e2e compare');
    await compareItems(compareSuite, errors);
  } catch (error) {
    throw error;
  }

  if (options.reportLocation) {
    builder.writeTo(options.reportLocation);
  }
  await (errors.length > 0
    ? Logger.error(`\n${errors.length} failed test cases`)
    : Logger.ok('All tests passed.'));

  return errors;
}

/**
 * Retrieves all samples previously index using the api
 */
async function retrieveItems(api: ConnectorClient, suite: junit.TestSuite, errors: string[]): Promise<void> {
  const singleItemSearchRequest: SCSearchRequest = {
    filter: {
      arguments: {
        field: 'uid',
        value: 'replace-me',
      },
      type: 'value',
    },
  };
  for (const {uid, type} of localItemMap.values()) {
    await runTest(
      `Should find ${type} (${uid})`,
      async () => {
        singleItemSearchRequest.filter!.arguments.value = uid;
        const searchResponse = await api.search(singleItemSearchRequest);
        if (searchResponse.data.length !== 1) {
          throw new Error(
            `Search for single SCThing with uid: ${uid} returned ${searchResponse.data.length} results`,
          );
        }
        remoteItemMap.set(uid, searchResponse.data[0]);
      },
      suite,
      errors,
    );
  }
}

/**
 * Compares all samples (local and remote) with the same uid and throws if they're not deep equal
 */
async function compareItems(suite: junit.TestSuite, errors: string[]) {
  for (const localThing of localItemMap.values()) {
    await runTest(
      `Should be the same for ${localThing.type} (${localThing.uid})`,
      async () => {
        /* istanbul ignore next retrieveItems will throw before*/
        if (!remoteItemMap.has(localThing.uid)) {
          throw new Error(`Did not retrieve expected SCThing with uid: ${localThing.uid}`);
        }
        const remoteThing = remoteItemMap.get(localThing.uid);
        deepStrictEqual(
          remoteThing,
          localThing,
          `Unexpected difference between original and retrieved sample`,
        );
      },
      suite,
      errors,
    );
  }
  Logger.info(`All samples retrieved from the backend have been compared`);
}
/**
 * Function to add all the SCThings that getItemsFromSamples() returns to the backend
 */
async function indexSamples(
  api: ConnectorClient,
  options: E2EOptions,
  suite: junit.TestSuite,
  errors: string[],
): Promise<void> {
  const items = await getItemsFromSamples(options.samplesLocation);

  if (items.length === 0) {
    throw new Error('Could not index samples. None were retrieved from the file system.');
  }

  // sort items by type
  const itemMap: Map<SCThingType, SCThings[]> = new Map();
  for (const item of items) {
    if (!itemMap.has(item.type)) {
      itemMap.set(item.type, []);
    }
    const itemsOfSameType = itemMap.get(item.type) as SCThings[];
    itemsOfSameType.push(item);
    itemMap.set(item.type, itemsOfSameType);
    localItemMap.set(item.uid, item);
  }
  // add items depending on their type property with one type per bulk
  for (const type of itemMap.keys()) {
    await runTest(
      `Should index ${type}`,
      async () => api.index(itemMap.get(type) as SCThings[], 'stapps-core-sample-data'),
      suite,
      errors,
    );
  }
}

/**
 * Get all SCThings from the predefined core test json files
 * @param samplesDirectory Filepath to the directory containing to the core test json files
 * @returns an Array of all the SCThings specified for test usage
 */
export async function getItemsFromSamples<T extends SCThings>(samplesDirectory: string): Promise<T[]> {
  const readDirPromised = promisify(readdir);
  const readFilePromised = promisify(readFile);

  const things: T[] = [];
  try {
    const fileNames = await readDirPromised(samplesDirectory);
    for (const fileName of fileNames) {
      const filePath = path.join(samplesDirectory, fileName);
      if (filePath.endsWith('.json')) {
        const fileContent = await readFilePromised(filePath, {encoding: 'utf8'});
        const schemaObject = JSON.parse(fileContent);
        if (schemaObject.errorNames.length === 0 && typeof schemaObject.instance.type === 'string') {
          things.push(schemaObject.instance);
        }
      }
    }
  } catch (error) {
    throw error;
  }

  return things;
}
