import * as Messages from '../helpers/console-messages';
import * as Models from '../models/models';
import * as jsonFuture from 'json-future';
import type * as messages from '@cucumber/messages';
import * as path from 'path';
import { format, intervalToDuration } from 'date-fns';
import fs, { promises as fsp } from 'fs';
import type { Readable } from 'stream';
import type { Stats } from 'fs';
import os from 'os';

export const streamToArray = async ( readableStream: Readable ): Promise<messages.Envelope[]> => {
  return new Promise<messages.Envelope[]>(
    (
      resolve: ( wrappers: messages.Envelope[] ) => void,
      reject: ( err: Error ) => void
    ) => {
      const items: messages.Envelope[] = [];
      readableStream.on( 'data', ( item: messages.Envelope ) => {
        items.push( item );
      } );
      /* istanbul ignore next */
      readableStream.on( 'error', ( err: Error ) => {
        reject( err );
      } );
      readableStream.on( 'end', () => {
        resolve( items );
      } );
    }
  );
};

export const updatePercentages = ( results: Models.FeatureModuleResults | Models.ModuleResults ): Models.FeatureModuleResults | Models.ModuleResults => {
  results.ambiguousPercentage = ( results.ambiguous ? results.ambiguous / results.total * 100 : 0 ).toFixed( 2 );
  results.failedPercentage = ( results.failed ? results.failed / results.total * 100 : 0 ).toFixed( 2 );
  results.undefinedPercentage = ( results.undefined ? results.undefined / results.total * 100 : 0 ).toFixed( 2 );
  results.passedPercentage = ( results.passed ? results.passed / results.total * 100 : 0 ).toFixed( 2 );
  results.pendingPercentage = ( results.pending ? results.pending / results.total * 100 : 0 ).toFixed( 2 );
  results.skippedPercentage = ( results.skipped ? results.skipped / results.total * 100 : 0 ).toFixed( 2 );
  if ( 'variousPercentage' in results ) {
    results.variousPercentage = ( results.various ? results.various / results.total * 100 : 0 ).toFixed( 2 );
    return results;
  }
  return results;
};

export const updateResultsAndPercentages = ( sourceResults: Models.ModuleResults, targetResults: Models.ModuleResults ): Models.ModuleResults => {
  sourceResults.passed += targetResults.passed;
  sourceResults.failed += targetResults.failed;
  sourceResults.skipped += targetResults.skipped;
  sourceResults.pending += targetResults.pending;
  sourceResults.undefined += targetResults.undefined;
  sourceResults.ambiguous += targetResults.ambiguous;
  sourceResults.total += targetResults.total;

  sourceResults.ambiguousPercentage = ( sourceResults.ambiguous ? sourceResults.ambiguous / sourceResults.total * 100 : 0 ).toFixed( 2 );
  sourceResults.failedPercentage = ( sourceResults.failed ? sourceResults.failed / sourceResults.total * 100 : 0 ).toFixed( 2 );
  sourceResults.undefinedPercentage = ( sourceResults.undefined ? sourceResults.undefined / sourceResults.total * 100 : 0 ).toFixed( 2 );
  sourceResults.passedPercentage = ( sourceResults.passed ? sourceResults.passed / sourceResults.total * 100 : 0 ).toFixed( 2 );
  sourceResults.pendingPercentage = ( sourceResults.pending ? sourceResults.pending / sourceResults.total * 100 : 0 ).toFixed( 2 );
  sourceResults.skippedPercentage = ( sourceResults.skipped ? sourceResults.skipped / sourceResults.total * 100 : 0 ).toFixed( 2 );
  return sourceResults;
};

export const updateStatus = ( featureResults: Models.FeatureResults ): Models.Result[] => {
  const result = <Models.Result[]>[];

  if ( featureResults.scenarios.ambiguous ) {
    result.push( { icon: 'fas fa-question-circle ambiguous-color', status: Models.Status.ambiguous } );
  }

  if ( featureResults.scenarios.pending ) {
    result.push( { icon: 'fas fa-question-circle pending-color', status: Models.Status.pending } );
  }

  if ( featureResults.scenarios.undefined ) {
    result.push( { icon: 'fa fa-question-circle undefined-color', status: Models.Status.undefined } );
  }

  if ( featureResults.scenarios.skipped ) {
    result.push( { icon: 'arrow-circle-right skipped-color', status: Models.Status.skipped } );
  }

  if ( featureResults.scenarios.failed ) {
    result.push( { icon: 'exclamation-circle failed-color', status: Models.Status.failed } );
  }

  if ( featureResults.scenarios.passed ) {
    result.push( { icon: 'check-circle passed-color', status: Models.Status.passed } );
  }

  return result;
};

export const isMetadata = ( metadata: Models.Metadata[] | undefined ): metadata is Models.Metadata[] =>
  Array.isArray( metadata )
    ? !metadata.filter( metadataElement => ( !( 'name' in metadataElement ) || !( 'value' in metadataElement ) ) ).length
    : false;

export const escapeHTML = ( value: string ): string => value
  .replace( /&/gu, '&amp;' )
  .replace( /</gu, '&lt;' )
  .replace( />/gu, '&gt;' );

export const isBase64 = ( value: string ): boolean => {
  const base64regex = /^(?<firstGroup>[0-9a-zA-Z+/]{4})*(?<secondGroup>(?<thirdGroup>[0-9a-zA-Z+/]{2}==)|(?<fourthGroup>[0-9a-zA-Z+/]{3}=))?$/u;

  return base64regex.test( value );
};

export const getFilesAsync = async ( dir: string ): Promise<string[]> => {
  let files: string[] = [];
  const getFiles = async ( folder: string ): Promise<string[]> => {
    files = await fsp.readdir( folder );
    const result: Promise<string[]>[] = files.map( async ( file ) => {
      const fileNameAndPath = path.join( folder, file );
      return fsp.stat( fileNameAndPath ).then( ( stat ) => ( stat.isDirectory() ? getFiles( fileNameAndPath ) : [ fileNameAndPath ] ) );
    } );
    return <string[]>Array.prototype.concat( ...( await Promise.all( result ) ) );
  };

  return getFiles( dir );
};

export const checkFolder = ( file: string | undefined ): boolean => {
  if ( typeof file === 'undefined' ) {
    return false;
  }
  let stats = <Stats>{};
  try {
    stats = fs.statSync( file );
  } catch ( error: unknown ) {
    throw new Error( Messages.invalidPathProvided( file ) );
  }

  return stats.isDirectory();
};

export const readJsonFile = async <T> ( file: string, encoding: BufferEncoding = 'utf8' ): Promise<T | null> => {
  const report = await fsp.readFile( file, { encoding } );

  try {
    return <T>jsonFuture.parse( report );
  } catch ( error: unknown ) {
    console.log( ( Messages.invalidJsonProvided( file, ( <Error>error ).message ) ) );
  }
  return null;
};

export const saveJsonFile = async<T> ( filePath: string | undefined, fileName: string, json: T ): Promise<boolean> => {
  const fileNameAndPath = path.join( filePath!, fileName );
  const jsonString = await jsonFuture.stringifyAsync( { data: json, replacer: null, space: 4 } );
  await fsp.writeFile( fileNameAndPath, jsonString, { encoding: 'utf8' } );
  return true;
};

export const getDateFormatted = ( date: Date ): string => format( date, 'yyyy-MM-dd HH:mm:ss' );

export const convertTimeFromNanoSecondsToHHMMSS = ( time: number ): string => {
  const duration = intervalToDuration( { start: 0, end: time / 1000000 } );
  const hours = ( `${String( duration.hours )}` ).padStart( 2, '0' );
  const minutes = ( `${String( duration.minutes )}` ).padStart( 2, '0' );
  const seconds = ( `${String( duration.seconds )}` ).padStart( 2, '0' );

  return `${hours}:${minutes}:${seconds}`;
};

export const moveArray = <T> ( arr: T[], fromIndex: number, toIndex: number ): T[] => {
  const element = arr[ fromIndex ];
  const deleteCountOne = 1;
  arr.splice( fromIndex, deleteCountOne );
  const deleteCountZero = 0;
  arr.splice( toIndex, deleteCountZero, element );
  return arr;
};

export const exists = ( filePath: string ): boolean => {
  return fs.existsSync( filePath );
};

export const initializePath = ( reportPath?: string | undefined ): string => {
  let reportPathFixed = reportPath;
  const resultCheckReportCreationPath = checkFolder( reportPath );

  if ( !resultCheckReportCreationPath && typeof reportPath === 'undefined' ) {
    const date = format( new Date(), 'yyyy-MM-dd__HH-mm-ss' );
    reportPathFixed = path.join( os.tmpdir(), 'cucumber-html-report-generator', date );
    fs.mkdirSync( reportPathFixed, { recursive: true } );
  }

  return reportPathFixed!;
};

export const pathExists = async ( folderPath: string ): Promise<boolean> => {
  return new Promise( ( resolve ) => {
    fs.access( folderPath, fs.constants.F_OK, error => {
      resolve( !error );
    } );
  } );
};

export const emptyFolder = async ( folderPath: string ): Promise<void> => {
  /* istanbul ignore next */
  if ( await pathExists( folderPath ) ) {
    await fsp.rm( folderPath, { recursive: true } );
  }
  await fsp.mkdir( folderPath, { recursive: true } );
};
