"use strict";
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());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
const http_1 = require("http");
const resolve_1 = require("resolve");
const findOpenPort_1 = require("../browser/findOpenPort");
const openBrowser_1 = require("../browser/openBrowser");
const server_1 = require("../browser/server");
const path_1 = require("path");
const tempy = require("tempy");
const fs_1 = require("fs");
const generateTestBundle_1 = require("../browser/generateTestBundle");
const createIndexHtml_1 = require("../browser/createIndexHtml");
const typeCheckInAnotherProcess_1 = require("../typescript/typeCheckInAnotherProcess");
const watchTestMetadata_1 = require("../tests/watchTestMetadata");
const watchFile_1 = require("../browser/webpack/watchFile");
const collectByKey_1 = require("../common/collectByKey");
const Results_1 = require("./Results");
const makeAbsolute_1 = require("../common/makeAbsolute");
const results_1 = require("../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_1.collectByKey(x => x.filePath, queue);
    const queuedFilePaths = Object.keys(queuedMetadataByFilePath).sort();
    const metadataByFilePath = collectByKey_1.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]), []);
};
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 = path_1.join(outputDirectory, path_1.basename(tempy.file({ extension: 'ts' })));
        const bundlePath = path_1.join(outputDirectory, path_1.basename(tempy.file({ extension: 'js' })));
        const indexHtmlPath = path_1.join(outputDirectory, 'index.html');
        const port = yield findOpenPort_1.findOpenPort();
        const url = `http://localhost:${port}`;
        const logErrors = (errors) => Promise.all(errors.map(logger.error));
        const makeAbsolutePath = (path) => (path_1.isAbsolute(path) ? path : path_1.join(cwd, path));
        const { updateResults, removeFilePath } = new Results_1.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 openBrowser_1.getLauncher();
                const run = () => __awaiter(this, void 0, void 0, function* () {
                    killTestBrowser();
                    logger.log('Opening browser...');
                    browserOpenTime = Date.now();
                    const instance = yield openBrowser_1.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(resolve_1.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) => {
            fs_1.writeFileSync(temporaryPath, generateTestBundle_1.generateTestBundle(cwd, outputDirectory, port, timeout, metadata));
            newFileOnDisk = true;
            writingToDisk = false;
            if (firstRun) {
                watchFile_1.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_1.typecheckInAnotherProcess(cwd, metadata.map(x => x.filePath));
            logger.log('Type checking complete');
            typeCheckCb(processResults);
        });
        const removeFileFromQueue = (filePath) => {
            const path = makeAbsolute_1.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 = http_1.createServer(server_1.setupServer(logger, outputDirectory, newResults => {
            firstRun = false;
            if (!keepAlive && killBrowser) {
                killBrowser();
            }
            const results = updateResults(newResults);
            const stats = results_1.getTestStats(results_1.getTestResults(results));
            cb({ results, stats });
            testsCompletedTime = Date.now();
            timeToRunTests = testsCompletedTime - browserOpenTime;
            newFileOnDisk = testsAreRunning = false;
        }));
        fs_1.writeFileSync(indexHtmlPath, createIndexHtml_1.createIndexHtml(path_1.basename(bundlePath)));
        server.listen(port, '0.0.0.0');
        const { dispose: stopWatchingMetadata } = yield watchTestMetadata_1.watchTestMetadata(cwd, fileGlobs, compilerOptions, mode, logger, removeFileFromQueue, updateMetadataAndWriteBundle);
        const dispose = () => {
            stopWatchingMetadata();
            server.close();
        };
        return { dispose };
    });
}
exports.watchBrowserTests = watchBrowserTests;
//# sourceMappingURL=watchBrowserTests.js.map