/**
 * @flow
 * @format
 * @lint-ignore-every LINEWRAP1
 */

import {dirname, normalize, relative, resolve} from 'path';
import {format} from 'util';

import {exists, glob, readFile} from '../utils/async';
import Builder from './builder';
import {getTestsDir} from '../constants';
import Suite from './Suite';

import type {Tests} from './Tester';
import type {SuiteResult} from './runTestSuite';

const testSuiteRegex = /(.*)[\/\\]test.js/;

async function findTestSuites(): Promise<Array<string>> {
  const testSuites = await glob(format('%s/!(lsp)/**/test.js', getTestsDir()), {
    cwd: __dirname,
  });
  // On Windows, glob still uses unix dir seperators, so we need to normalize
  return testSuites.map(normalize);
}

export async function findTestsByName(
  suitesOrig: ?Set<string>,
): Promise<Set<string>> {
  let suites = null;
  if (suitesOrig != null) {
    suites = new Set();
    for (let suite of suitesOrig) {
      suite = normalize(suite.trim());
      suites.add(suite);
      suites.add(resolve(suite));
    }
  }
  const testsDir = getTestsDir();
  const testSuites = await findTestSuites();

  const result = new Set();

  process.stderr.write(format('Found %d suites\n', testSuites.length));
  for (const suiteFile of testSuites) {
    const suiteName = relative(testsDir, suiteFile).replace(
      testSuiteRegex,
      '$1',
    );
    const pathToDir = dirname(suiteFile);
    if (
      !suites ||
      suites.has(suiteName) ||
      suites.has(suiteFile) ||
      suites.has(pathToDir)
    ) {
      result.add(suiteName);
    }
  }

  return result;
}

function loadSuiteByFilename(filename: string): Suite {
  delete require.cache[require.resolve(filename)];
  const {default: suite} = (require: any)(filename);
  // I don't know why, but suite instanceof Suite doesn't seem to work when
  // using symlinks. So this is a fuzzy approximation
  if (!(suite && suite.constructor && suite.constructor.name === Suite.name)) {
    throw new Error(
      format('Test suite `%s` forgot to export default suite(...)', filename),
    );
  }
  return suite;
}

export function loadSuite(suiteName: string): Suite {
  return loadSuiteByFilename(resolve(getTestsDir(), suiteName, 'test.js'));
}

export async function findTestsByRun(
  runID: string,
  failedOnly: boolean,
): Promise<Set<string>> {
  const runDir = Builder.getDirForRun(runID);
  const runDirExists = await exists(runDir);

  if (!runID || !runDirExists) {
    process.stderr.write(
      format("Cannot find the tmp directory for run '%s'.\n", runID),
    );
    return new Set();
  }

  const results = await glob(format('%s/*/results.json', runDir), {
    cwd: __dirname,
  });

  let resultContents = await Promise.all(
    results.map(async filename => {
      const rawContents = await readFile(filename);
      try {
        return JSON.parse(rawContents);
      } catch (e) {
        throw new Error(
          format('Error parsing run %s result %s:\n%s', runID, filename, e),
        );
      }
    }),
  );

  if (failedOnly) {
    resultContents = resultContents.filter(contents => {
      const suiteResult: SuiteResult = contents.results;
      if (suiteResult.type === 'exceptional') {
        return true;
      } else if (suiteResult.type === 'normal') {
        return suiteResult.testResults.some(testResult =>
          testResult.stepResults.some(stepResult => !stepResult.passed),
        );
      }
    });
  }

  const suites = new Set(resultContents.map(contents => contents.suiteName));

  process.stderr.write(
    format(
      'Looks like run %s ran %d suites%s\n',
      runID,
      suites.size,
      failedOnly ? ' that failed' : '',
    ),
  );

  return findTestsByName(suites);
}
