var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
import { createServer } from 'http';
import { sync } from 'resolve';
import { findOpenPort } from '../browser/findOpenPort';
import { getLauncher, openBrowser } from '../browser/openBrowser';
import { setupServer } from '../browser/server';
import { basename, join, isAbsolute } from 'path';
import * as tempy from 'tempy';
import { writeFileSync } from 'fs';
import { generateTestBundle } from '../browser/generateTestBundle';
import { createIndexHtml } from '../browser/createIndexHtml';
import { typecheckInAnotherProcess } from '../typescript/typeCheckInAnotherProcess';
import { watchTestMetadata } from '../tests/watchTestMetadata';
import { watchFile } from '../browser/webpack/watchFile';
import { collectByKey } from '../common/collectByKey';
import { Results } from './Results';
import { makeAbsolute } from '../common/makeAbsolute';
import { getTestStats, getTestResults } from '../results';
const isEqual = require('lodash.isequal');
const uniq = (list) => Array.from(new Set(list));
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const updateQueue = (metadata, queue) => {
    const queuedMetadataByFilePath = collectByKey(x => x.filePath, queue);
    const queuedFilePaths = Object.keys(queuedMetadataByFilePath).sort();
    const metadataByFilePath = collectByKey(x => x.filePath, metadata);
    const filePaths = Object.keys(metadataByFilePath).sort();
    const allFilePaths = uniq([...filePaths, ...queuedFilePaths]);
    return allFilePaths.reduce((xs, filePath) => metadataByFilePath[filePath] // replace updated files
        ? xs.concat(metadataByFilePath[filePath])
        : xs.concat(queuedMetadataByFilePath[filePath]), []);
};
export function watchBrowserTests(fileGlobs, compilerOptions, options, cwd, logger, cb, errCb, typeCheckCb) {
    return __awaiter(this, void 0, void 0, function* () {
        const { keepAlive, timeout, typeCheck, mode } = options;
        const outputDirectory = tempy.directory();
        const temporaryPath = join(outputDirectory, basename(tempy.file({ extension: 'ts' })));
        const bundlePath = join(outputDirectory, basename(tempy.file({ extension: 'js' })));
        const indexHtmlPath = join(outputDirectory, 'index.html');
        const port = yield findOpenPort();
        const url = `http://localhost:${port}`;
        const logErrors = (errors) => Promise.all(errors.map(logger.error));
        const makeAbsolutePath = (path) => (isAbsolute(path) ? path : join(cwd, path));
        const { updateResults, removeFilePath } = new Results();
        // shared mutable state
        let queuedMetadata = [];
        let previousMetadata = [];
        let testsAreRunning = false;
        let writingToDisk = false;
        let scheduleNextRunHandle;
        let newFileOnDisk = false;
        let killBrowser = () => void 0;
        let previousWebpackHash = '';
        let firstRun = true;
        let timeToRunTests = 0;
        let browserOpenTime = 0;
        let testsCompletedTime = 0;
        const killTestBrowser = () => (!keepAlive && killBrowser ? killBrowser() : void 0);
        const setupTestBrowser = () => __awaiter(this, void 0, void 0, function* () {
            const { browser, keepAlive } = options;
            if (browser !== 'chrome-headless') {
                const launcher = yield getLauncher();
                const run = () => __awaiter(this, void 0, void 0, function* () {
                    killTestBrowser();
                    logger.log('Opening browser...');
                    browserOpenTime = Date.now();
                    const instance = yield openBrowser(browser, url, keepAlive, launcher);
                    if (!firstRun) {
                        const lastCompletion = testsCompletedTime;
                        // Re-run tests if they seem to have stalled - this can happen when browsers are opened in succession quite quickly
                        // I've only been able to make it do this when I am very rapidly changing and saving a test file to purposely try and break things.
                        delay(timeToRunTests * 3).then(() => (testsCompletedTime === lastCompletion ? run() : void 0));
                    }
                    killBrowser = () => instance.stop();
                });
                return run;
            }
            const { launch } = require(sync('chrome-launcher', { basedir: cwd }));
            const run = () => __awaiter(this, void 0, void 0, function* () {
                killTestBrowser();
                logger.log('Opening browser...');
                browserOpenTime = Date.now();
                const chrome = yield launch({ startingUrl: url });
                if (!firstRun) {
                    const lastCompletion = testsCompletedTime;
                    delay(timeToRunTests * 3).then(() => (testsCompletedTime === lastCompletion ? run() : void 0));
                }
                killBrowser = () => chrome.kill();
            });
            return run;
        });
        const runTestsInBrowser = yield setupTestBrowser();
        const newStats = (stats) => __awaiter(this, void 0, void 0, function* () {
            const shouldReturnEarly = stats.hash === previousWebpackHash || !newFileOnDisk || testsAreRunning;
            if (shouldReturnEarly) {
                previousWebpackHash = stats.hash;
                return;
            }
            if (stats.hasErrors()) {
                const { errors } = stats.toJson();
                return logErrors(errors);
            }
            testsAreRunning = true;
            runTestsInBrowser();
        });
        const writeToDisk = (metadata) => {
            writeFileSync(temporaryPath, generateTestBundle(cwd, outputDirectory, port, timeout, metadata));
            newFileOnDisk = true;
            writingToDisk = false;
            if (firstRun) {
                watchFile(cwd, temporaryPath, bundlePath, options.webpackConfiguration, newStats, errCb);
            }
        };
        const typeCheckMetadata = (metadata) => __awaiter(this, void 0, void 0, function* () {
            logger.log('Typechecking...');
            const processResults = yield typecheckInAnotherProcess(cwd, metadata.map(x => x.filePath));
            logger.log('Type checking complete');
            typeCheckCb(processResults);
        });
        const removeFileFromQueue = (filePath) => {
            const path = makeAbsolute(cwd, filePath);
            removeFilePath(path);
            queuedMetadata = queuedMetadata.filter(x => makeAbsolutePath(x.filePath) !== path);
        };
        const scheduleNextBundleWrite = () => {
            if (testsAreRunning || writingToDisk) {
                clearTimeout(scheduleNextRunHandle);
                scheduleNextRunHandle = setTimeout(scheduleNextBundleWrite, 600);
                return;
            }
            writingToDisk = true;
            const currentQueue = queuedMetadata.slice();
            writeToDisk(currentQueue);
            if (typeCheck) {
                typeCheckMetadata(currentQueue);
            }
            queuedMetadata = [];
        };
        const updateMetadataAndWriteBundle = (metadata) => __awaiter(this, void 0, void 0, function* () {
            queuedMetadata = updateQueue(metadata, queuedMetadata);
            if (isEqual(metadata, previousMetadata) || queuedMetadata.length === 0) {
                return;
            }
            previousMetadata = metadata;
            scheduleNextBundleWrite();
        });
        const server = createServer(setupServer(logger, outputDirectory, newResults => {
            firstRun = false;
            if (!keepAlive && killBrowser) {
                killBrowser();
            }
            const results = updateResults(newResults);
            const stats = getTestStats(getTestResults(results));
            cb({ results, stats });
            testsCompletedTime = Date.now();
            timeToRunTests = testsCompletedTime - browserOpenTime;
            newFileOnDisk = testsAreRunning = false;
        }));
        writeFileSync(indexHtmlPath, createIndexHtml(basename(bundlePath)));
        server.listen(port, '0.0.0.0');
        const { dispose: stopWatchingMetadata } = yield watchTestMetadata(cwd, fileGlobs, compilerOptions, mode, logger, removeFileFromQueue, updateMetadataAndWriteBundle);
        const dispose = () => {
            stopWatchingMetadata();
            server.close();
        };
        return { dispose };
    });
}
//# sourceMappingURL=watchBrowserTests.js.map