import WDIOReporter from '@wdio/reporter'
import allureReporter from '@wdio/allure-reporter'
import Watcher from '../watcher.js'
import path from 'path'
import fs from 'fs'
import fse from 'fs-extra'
import sanitize from 'sanitize-filename'
import publisherUtils from './publishers/publisher-utils.js'
import tools from '@eui/tools/scripts/utils/tools.js'
const {suite : suiteArg, root: configFile} = tools.getArgs();

export default class EuiReporter extends WDIOReporter {
  constructor(options) {
    options = Object.assign(options);
    super(options);
  }

  async onRunnerStart(runner) {
    browser.addCommand('addTestMeta', this['addTestMeta'].bind(this));
    if (browser.config.versionCheck) {
      this.testedVersion = browser.config.versionCheck;
    } else {
      this.testedVersion = 'UNKNOWN';
    }
    if (runner.sessionId) {
      this.sessionId = runner.sessionId;
    } else {
      this.sessionId = false;
    }
    if (browser.config.jiraissuekey) {
      this.jiraissuekey = browser.config.jiraissuekey;
    }
    browser.addCommand('setTestedVersion', this['setTestedVersion'].bind(this));
    browser.addCommand('performanceNav', this['performanceNav'].bind(this));
    browser.addCommand('deductedWait', this['deductedWait'].bind(this));
    browser.addCommand('deductTime', this['deductTime'].bind(this));
    browser.addCommand('startWatching', this['startWatching'].bind(this));
    browser.addCommand('stopWatching', this['stopWatching'].bind(this));
    browser.addCommand('fastFail', this['fastFail'].bind(this));
    browser.addCommand('logPuppeteerNetwork', this['logPuppeteerNetwork'].bind(this));
    browser.addCommand('retrieveSauceNetworkHar', this['retrieveSauceNetworkHar'].bind(this));
  }
  
  addTestMeta(meta) {
    browser.config.currentstepMeta = meta;
    return meta;
  }

  async fastFail(failThreshold = 3) {
    if (this.describeFailcount > failThreshold) {
      throw new Error('FASTFAIL, Failure count during test case was above the fail threshold of ' + failThreshold);
    }
  }
  async logPuppeteerNetwork() {
    browser.config.puppeteerNetworkCaptureAfterIt = true;
  }
  
  async retrieveSauceNetworkHar() {
    let conf = browser.config;
    let allureFilteredReporter = conf.reporters.filter(
      item => Array.isArray(item) && item[0] == 'allure'
    );
    console.log("Scheduling Saucelabs Network Har retrieval after test completion, if you encounter issues please make sure saucelabs extendedDebugging is enabled");
    if (!Array.isArray(conf.services) || !conf.services.includes('sauce')) {
      return new Error("Error in EuiReporter.retrieveSauceNetworkHar: retrieveSauceNetworkHar will only work if sauce service is enabled.");
    } else if (!allureFilteredReporter[0]) {
      return new Error("Error in EuiReporter.retrieveSauceNetworkHar: Allure reporter not set in config, it is required to save the sauce network HAR");
    } else if (!allureFilteredReporter[0][1]) {
      return new Error("Error in EuiReporter.retrieveSauceNetworkHar: Allure reporter settings are missing in config, it is required to save the sauce network HAR");
    } else if (!allureFilteredReporter[0][1].outputDir) {
      return new Error("Error in EuiReporter.retrieveSauceNetworkHar: Allure reporter outputDir setting is missing in config, it is required to save the sauce network HAR");
    } else if (!fs.existsSync(path.join('.', allureFilteredReporter[0][1].outputDir))) {
      return new Error("Error in EuiReporter.retrieveSauceNetworkHar: Allure reporter outputDir doesn't exist, it is required to save the sauce network HAR");
    } else if (!allureReporter) {
      return new Error("Error in EuiReporter.retrieveSauceNetworkHar: Default Allure reporter instance doesn't exist, it is required to save the sauce network HAR");
    } else {
      let outputDir = path.join('.', allureFilteredReporter[0][1].outputDir);
      let filesBefore = fs.readdirSync(outputDir);
      allureReporter.addAttachment('Saucelabs network HAR', '', 'application/zip');
      let filesAfter = fs.readdirSync(outputDir);
      let newFiles = filesAfter.filter(x => !filesBefore.includes(x));
      if (newFiles.length === 0 || newFiles.length > 1) {
        return new Error("Error in EuiReporter.retrieveSauceNetworkHar: Could not identify the file used by allure reporter for the Saucelabs network HAR attachment, expected 1 new file in outputDir but found " + newFiles.length);
      } else {
        return { file: path.join(outputDir, newFiles[0]) };
      }
    }
  }

  async startWatching(watchScenarios, watcherInterval = 500) {
      this.watcher = new Watcher();
      this.watcher.startWatching(watchScenarios, watcherInterval);
  }

  async stopWatching(unfullfilledTimeout) {
    if (this.watcher) {
      return await this.watcher.stopWatching(unfullfilledTimeout);
    }
  }

  async performanceNav(url, performanceElementChecks) {
    let promises = [];
    let navStart = new Date().getTime();
    let error = false;
    if (url){
      await browser.url(url)
    }
      performanceElementChecks.forEach(check => {
        check.navStart = navStart;
        promises.push(check.fullfill());
    })

    await Promise.all(promises);

    let performanceElementChecksWithError = performanceElementChecks.filter(check => check.error)
    if (performanceElementChecksWithError.length > 0) {
      error = 'Errors occrured during performanceNav: \n';
    }
    performanceElementChecksWithError.forEach(check => {
      error += '\n';
      error += 'Error message: ' + check.error.message + '\n';
      error += 'Error element: ' + check.error.element + '\n';
      error += 'Error stack: ' + check.error.stack + '\n';
    });
    if (error) {
      throw new Error(error);
    }
  }

  async deductedWait(timeout) {
    await browser.pause(timeout);
    return { deductedWait: timeout };
  }

  async deductTime(time) {
    time = Math.abs(time);
    return { deductTime: time };
  }

  setTestedVersion(version) {
    this.testedVersion = version;
    return { testedVersion: version };
  }

  async onTestStart(test) {
    while (this.testRunning) {
      // wait 50 ms
      await new Promise(resolve => setTimeout(resolve, 50));
    }
    this.testRunning = true;
  }

  onSuiteStart() {
    this.describeFailcount = 0;
  }

  async onTestEnd(test) {
    let screenshots = [];
    let testMeta = {invalidateResult: false};
    let browserlog = [];
    let deductedWaits = 0;
    let deductTime = 0;
    if (test.output && test.output.length > 0) {

      // clone the log
      var clonedOutput = JSON.parse(JSON.stringify(test.output));
      // certain test commands (like script execution) post the whole config including passwords to output so we need to clean those.
      test.output = publisherUtils.cleanLog(clonedOutput, ["config","password","token"]);

      if (browser.config.saveScreenshotToJson) {
        let outputFilteredforScreenshots = test.output.filter(
          item => item.endpoint && item.endpoint == '/session/:sessionId/screenshot' &&
            item.result && item.result.value
        )
        outputFilteredforScreenshots.forEach(screenshotOutput => {
          screenshots.push(screenshotOutput.result.value);
        });
      } else {
        test.output = test.output.filter(
          item => !(item.endpoint && item.endpoint == '/session/:sessionId/screenshot' &&
            item.result && item.result.value)
        )
      }

      let outputFilteredforTestMeta = test.output.filter(
        item => item.name && item.name == 'addTestMeta' &&
          item.result
      )
      outputFilteredforTestMeta.forEach(testMetaOutput => {
        testMeta = Object.assign(testMeta, testMetaOutput.result);
      });

      let outputFilteredforBrowserlogs = test.output.filter(
        item => item.endpoint && item.endpoint == '/session/:sessionId/se/log' &&
          item.result && item.result.value && Array.isArray(item.result.value)
      )
      outputFilteredforBrowserlogs.forEach(browserlogOutput => {
        Array.prototype.push.apply(browserlog, browserlogOutput.result.value);
      });

      let outputFilteredforDeductedWait = test.output.filter(
        item => item.name && item.name == 'deductedWait' &&
          item.result && item.result.deductedWait && Number.isInteger(item.result.deductedWait)
      )
      outputFilteredforDeductedWait.forEach(deductedWaitOutput => {
        deductedWaits += deductedWaitOutput.result.deductedWait;
      });

      let outputFilteredforDeductedTime = test.output.filter(
        item => item.name && item.name == 'deductTime' &&
          item.result && item.result.deductTime && Number.isInteger(item.result.deductTime)
      )
      outputFilteredforDeductedTime.forEach(deductTimeOutput => {
        deductTime += deductTimeOutput.result.deductTime;
      });

    }
    if (test.state == 'failed') {
      this.describeFailcount += 1;
      if (this.options.loggerOptions.invalidateErrors && Array.isArray(this.options.loggerOptions.invalidateErrors) && this.options.loggerOptions.invalidateErrors.length > 0){
        test.errors.forEach(err => {
          this.options.loggerOptions.invalidateErrors.forEach(invalidateError => {
            if (invalidateError.method === "includes"){
              if (err[invalidateError.check].includes(invalidateError.string)){
                testMeta.invalidateResult = true;
              }
            } else if (invalidateError.method === "endsWith"){
              if (err[invalidateError.check].endsWith(invalidateError.string)){
                testMeta.invalidateResult = true;
              }
            } else if (invalidateError.method === "startsWith"){
              if (err[invalidateError.check].startsWith(invalidateError.string)){
                testMeta.invalidateResult = true;
              }
            } else if (invalidateError.method === "equals"){
              if (err[invalidateError.check] === invalidateError.string){
                testMeta.invalidateResult = true;
              }
            } else {
              console.error("Invalid invalidateErrors set in loggerOptions, " + invalidateError.method + " is not a valid method");
            }
          });
        });
      }
    }
    Object.assign(test,
      {
        meta: testMeta,
        browserlog: browserlog,
        screenshots: screenshots,
        deductedWaits: deductedWaits,
        deductTime: deductTime
      });
    this.testRunning = false;
  }

  async onSuiteEnd(suite) {
    while (this.testRunning) {
      // wait 50 ms
      await new Promise(resolve => setTimeout(resolve, 50));
    }
    if (suite.hooks) {
      delete suite['hooks'];
    }
    if (suite.suites) {
      delete suite['suites'];
    }
    if (suite.hooksAndTests) {
      delete suite['hooksAndTests'];
    }
    let suiteMeta = {
      config_file: configFile,
      app: browser.config.app,
      suite: suiteArg,
      baseUrl: browser.config.baseUrl,
      env: browser.config.env,
      userenv: browser.config.userenv,
      window_width: browser.config.window_width,
      window_height: browser.config.window_height,
      testedVersion: this.testedVersion,
      testedSystems: [],
      sessionId: this.sessionId,
      invalidateResult: false
    };
    if (this.jiraissuekey) {
      suiteMeta["jiraissuekey"] = this.jiraissuekey;
    }
    let uuid = false;
    let reportGroups = false;
    let stepReportGroups = [];
    let suiteFailurecount = 0;
    let deductedSuiteFailurecount = 0;
    let testIndex = 0;
    let deductedWaits = 0;
    let deductTime = 0;
    suite.tests.forEach(test => {
      testIndex += 1;
      deductedWaits += test.deductedWaits;
      deductTime += test.deductTime;
      if (test.meta) {
        if (test.meta.uuid) {
          uuid = test.meta.uuid;
        }
        if (test.meta.reportGroups) {
          reportGroups = test.meta.reportGroups;
        }
        if (test.meta.stepReportGroups) {
          let formatedList = []
          test.meta.stepReportGroups.forEach(stepReport =>{
            formatedList.push(stepReport.toUpperCase());
          })
          test.meta.stepReportGroups = formatedList;
          stepReportGroups = stepReportGroups.concat(test.meta.stepReportGroups);
        }
        if (test.meta.testedSystems) {
          test.meta.testedSystems.forEach(testedSystem => {
            if (!suiteMeta.testedSystems.includes(testedSystem)) {
              suiteMeta.testedSystems.push(testedSystem);
            }
          });
        }
        if (test.meta.invalidateResult) {
          suiteMeta.invalidateResult = true;
        }
      }
      if (test.screenshots.length > 0) {
        let screenshotFiles = [];
        let screenshotIndex = 0;
        test.screenshots.forEach(screenshot => {
          screenshotIndex += 1;
          let filename = suite.title + '-' + ('0' + (testIndex)).slice(-2) + '-' + test.title + '-' + screenshotIndex + '.png';
          if (uuid) {
            filename = uuid + '-' + filename;
          }
          filename = browser.config.suite + '-' + suite.cid + '-' + filename;
          filename = sanitize(filename);
          let filepath = path.join(this.options.loggerOptions.reportDir, 'screenshots', filename);
          try {
            fse.outputFileSync(filepath, screenshot, { encoding: 'base64' });
            screenshotFiles.push(filepath);
          } catch (error) {
            console.error('Could not save screenshot: ' + filepath + ' error: ' + error);
          }
        });
        test.screenshots = screenshotFiles;
      }
      if (test.state === 'failed') {
        if (browser.config.maintenances) {
          if (browser.config.maintenances.allfeaturesimpacted) {
            deductedSuiteFailurecount += 1
          } else if (browser.config.maintenances.impactedsystems.
            some(r => suiteMeta.testedSystems.indexOf(r) >= 0)) {
            // if an impacted system appears in tested systems
            deductedSuiteFailurecount += 1
          } else {
            suiteFailurecount += 1;
          }
        } else {
          suiteFailurecount += 1;
        }
      }
    });

    let reportfilename = suite.title + '.json';
    if (uuid) {
      reportfilename = uuid + '-' + reportfilename;
      suiteMeta.uuid = uuid;
    }

    if (reportGroups) {
      suiteMeta.reportGroups = reportGroups;
    }

    if (stepReportGroups!=[]) {
      suiteMeta.stepReportGroups = stepReportGroups;
    }

    Object.assign(suite,
      {
        meta: suiteMeta,
        failures: suiteFailurecount,
        deductedfailures: deductedSuiteFailurecount,
        deductedWaits: deductedWaits,
        deductTime: deductTime
      });
    reportfilename = browser.config.env + '-' + suite.cid + '-' + reportfilename;
    reportfilename = sanitize(reportfilename);
    let reportFilePath = path.join(this.options.loggerOptions.reportDir, reportfilename);
    fs.writeFileSync(reportFilePath,
      JSON.stringify(suite, null, 2), function (err) {
        if (err) {
          return console.error(err);
        }
      }
    );
  }
}
