import { promisify } from 'util';
import fs from 'fs';
import path from 'path';
import chai from 'chai';
import chalk from 'chalk';
import Mocha from 'mocha';
import { Key, until } from 'selenium-webdriver';
import { isImageError } from '../../types';
import { subscribeOn, emitTestMessage, emitWorkerMessage } from '../messages';
import chaiImage from './chai-image';
import { closeBrowser, getBrowser, switchStory } from '../selenium';
import { CreeveyReporter, TeamcityReporter } from './reporter';
import { addTestsFromStories } from './helpers';
import { logger } from '../logger';
const statAsync = promisify(fs.stat);
const readdirAsync = promisify(fs.readdir);
const readFileAsync = promisify(fs.readFile);
const writeFileAsync = promisify(fs.writeFile);
const mkdirAsync = promisify(fs.mkdir);
async function getStat(filePath) {
  try {
    return await statAsync(filePath);
  } catch (error) {
    if (typeof error == 'object' && error && error.code === 'ENOENT') {
      return null;
    }
    throw error;
  }
}
async function getLastImageNumber(imageDir, imageName) {
  const actualImagesRegexp = new RegExp(`${imageName}-actual-(\\d+)\\.png`);
  try {
    return (await readdirAsync(imageDir)).map(filename => filename.replace(actualImagesRegexp, '$1')).map(Number).filter(x => !isNaN(x)).sort((a, b) => b - a)[0] ?? 0;
  } catch (_error) {
    return 0;
  }
}

// FIXME browser options hotfix
export default async function worker(config, options) {
  var _await$browser$getSes;
  let retries = 0;
  let images = {};
  let error = undefined;
  const screenshots = [];
  const testScope = [];
  function runHandler(failures) {
    if (failures > 0 && (error || Object.values(images).some(image => (image === null || image === void 0 ? void 0 : image.error) != null))) {
      const isTimeout = hasTimeout(error) || Object.values(images).some(image => hasTimeout(image === null || image === void 0 ? void 0 : image.error));
      const payload = {
        status: 'failed',
        images,
        error
      };
      isTimeout ? emitWorkerMessage({
        type: 'error',
        payload: {
          error: error ?? 'Unknown error'
        }
      }) : emitTestMessage({
        type: 'end',
        payload
      });
    } else {
      emitTestMessage({
        type: 'end',
        payload: {
          status: 'success',
          images
        }
      });
    }
  }
  async function saveImages(imageDir, images) {
    await mkdirAsync(imageDir, {
      recursive: true
    });
    for (const {
      name,
      data
    } of images) {
      await writeFileAsync(path.join(imageDir, name), data);
    }
  }
  async function getExpected(assertImageName) {
    // context => [kind, story, test, browser]
    // rootSuite -> kindSuite -> storyTest -> [browsers.png]
    // rootSuite -> kindSuite -> storySuite -> test -> [browsers.png]
    const testPath = [...testScope];
    const imageName = assertImageName ?? testPath.pop();
    const imagesMeta = [];
    const reportImageDir = path.join(config.reportDir, ...testPath);
    const imageNumber = (await getLastImageNumber(reportImageDir, imageName)) + 1;
    const actualImageName = `${imageName}-actual-${imageNumber}.png`;
    const image = images[imageName] = images[imageName] ?? {
      actual: actualImageName
    };
    const onCompare = async (actual, expect, diff) => {
      imagesMeta.push({
        name: image.actual,
        data: actual
      });
      if (diff && expect) {
        image.expect = `${imageName}-expect-${imageNumber}.png`;
        image.diff = `${imageName}-diff-${imageNumber}.png`;
        imagesMeta.push({
          name: image.expect,
          data: expect
        });
        imagesMeta.push({
          name: image.diff,
          data: diff
        });
      }
      if (options.saveReport) {
        await saveImages(reportImageDir, imagesMeta);
      }
    };
    const expectImageDir = path.join(config.screenDir, ...testPath);
    const expectImageStat = await getStat(path.join(expectImageDir, `${imageName}.png`));
    if (!expectImageStat) return {
      expected: null,
      onCompare
    };
    const expected = await readFileAsync(path.join(expectImageDir, `${imageName}.png`));
    return {
      expected,
      onCompare
    };
  }
  const mochaOptions = {
    timeout: 30000,
    reporter: process.env.TEAMCITY_VERSION ? TeamcityReporter : options.reporter || CreeveyReporter,
    reporterOptions: {
      reportDir: config.reportDir,
      topLevelSuite: options.browser,
      get willRetry() {
        return retries < config.maxRetries;
      },
      get images() {
        return images;
      },
      get sessionId() {
        return sessionId;
      }
    }
  };
  const mocha = new Mocha(mochaOptions);
  mocha.cleanReferencesAfterRun(false);
  chai.use(chaiImage(getExpected, config.diffOptions));
  if ((await getBrowser(config, options)) == null) return;
  await addTestsFromStories(mocha.suite, config, {
    browser: options.browser,
    watch: options.ui,
    debug: options.debug,
    port: options.port
  });
  try {
    var _await$getBrowser;
    await ((_await$getBrowser = await getBrowser(config, options)) === null || _await$getBrowser === void 0 ? void 0 : _await$getBrowser.getCurrentUrl());
  } catch (_) {
    await closeBrowser();
  }
  const browser = await getBrowser(config, options);
  const sessionId = (_await$browser$getSes = await (browser === null || browser === void 0 ? void 0 : browser.getSession())) === null || _await$browser$getSes === void 0 ? void 0 : _await$browser$getSes.getId();
  if (browser == null) return;
  const interval = setInterval(() => void browser.getCurrentUrl().then(url => {
    if (options.debug) logger.debug(`${options.browser}:${chalk.gray(sessionId)}`, 'current url', chalk.magenta(url));
  }), 10 * 1000);
  subscribeOn('shutdown', () => clearInterval(interval));
  mocha.suite.beforeAll(function () {
    this.config = config;
    this.browser = browser;
    this.until = until;
    this.keys = Key;
    this.expect = chai.expect;
    this.browserName = options.browser;
    this.testScope = testScope;
    this.screenshots = screenshots;
  });
  mocha.suite.beforeEach(switchStory);
  subscribeOn('test', message => {
    if (message.type != 'start') return;
    const test = message.payload;
    const testPath = test.path.join(' ').replace(/[|\\{}()[\]^$+*?.-]/g, '\\$&');
    images = {};
    error = undefined;
    retries = test.retries;
    mocha.grep(new RegExp(`^${testPath}$`));
    const runner = mocha.run(runHandler);

    // TODO How handle browser corruption?
    runner.on('fail', (_test, reason) => {
      if (!(reason instanceof Error)) {
        error = reason;
      } else if (!isImageError(reason)) {
        error = reason.stack ?? reason.message;
      } else if (typeof reason.images == 'string') {
        const image = images[testScope.slice(-1)[0]];
        if (image) image.error = reason.images;
      } else {
        const imageErrors = reason.images;
        Object.keys(imageErrors).forEach(imageName => {
          const image = images[imageName];
          if (image) image.error = imageErrors[imageName];
        });
      }
    });
  });
  logger.info(`${options.browser}:${chalk.gray(sessionId)} is ready`);
  emitWorkerMessage({
    type: 'ready'
  });
}
function hasTimeout(str) {
  return str != null && str.toLowerCase().includes('timeout');
}