/**
 * Copyright Microsoft Corporation. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import EventEmitter from 'events';
import fs from 'fs';
import path from 'path';
import { registry } from 'playwright-core/lib/coreBundle';
import { ManualPromise } from '@isomorphic/manualPromise';
import { setPlaywrightTestProcessEnv } from '@utils/env';
import { gracefullyProcessExitDoNotHang } from '@utils/processLauncher';
import { cc, configLoader } from '../common';
import { FSWatcher } from './fsWatcher';
import { baseFullConfig } from '../isomorphic/teleReceiver';
import { addGitCommitInfoPlugin } from '../plugins/gitCommitInfoPlugin';
import { webServerPluginsForConfig } from '../plugins/webServerPlugin';
import { internalScreen } from '../reporters/base';
import { InternalReporter } from '../reporters/internalReporter';
import { serializeError } from '../util';
import { createErrorCollectingReporter, createReporters } from './reporters';
import { TestRun, createApplyRebaselinesTask, createClearCacheTask, createGlobalSetupTasks, createListFilesTask, createLoadTask, createPluginSetupTasks, createReportBeginTask, createRunTestsTasks, runTasks, runTasksDeferCleanup } from './tasks';
import { LastRunReporter } from './lastRun';
import { filterProjects } from './projectUtils';
export const TestRunnerEvent = {
    TestFilesChanged: 'testFilesChanged',
    TestPaused: 'testPaused',
};
export class TestRunner extends EventEmitter {
    configLocation;
    _configCLIOverrides;
    _watcher;
    _watchedProjectDirs = new Set();
    _ignoredProjectOutputs = new Set();
    _watchedTestDependencies = new Set();
    _testRun;
    _queue = Promise.resolve();
    _globalSetup;
    _plugins;
    _watchTestDirs = false;
    _populateDependenciesOnList = false;
    _startingEnv = {};
    constructor(configLocation, configCLIOverrides) {
        super();
        this.configLocation = configLocation;
        this._configCLIOverrides = configCLIOverrides;
        this._watcher = new FSWatcher(events => {
            const collector = new Set();
            events.forEach(f => cc.collectAffectedTestFiles(f.file, collector));
            this.emit(TestRunnerEvent.TestFilesChanged, [...collector]);
        });
    }
    async initialize(params) {
        setPlaywrightTestProcessEnv();
        this._watchTestDirs = !!params.watchTestDirs;
        this._populateDependenciesOnList = !!params.populateDependenciesOnList;
        this._startingEnv = { ...process.env };
    }
    resizeTerminal(params) {
        /* eslint-disable no-restricted-properties */
        process.stdout.columns = params.cols;
        process.stdout.rows = params.rows;
        process.stderr.columns = params.cols;
        process.stderr.rows = params.rows;
        /* eslint-enable no-restricted-properties */
    }
    hasSomeBrowsers() {
        for (const browserName of ['chromium', 'webkit', 'firefox']) {
            try {
                registry.registry.findExecutable(browserName).executablePathOrDie('javascript');
                return true;
            }
            catch {
            }
        }
        return false;
    }
    async installBrowsers() {
        const executables = registry.registry.defaultExecutables();
        await registry.registry.install(executables);
    }
    async loadConfig() {
        const { config, error } = await this._loadConfig(this._configCLIOverrides);
        if (config)
            return config;
        throw new Error('Failed to load config: ' + (error ? error.message : 'Unknown error'));
    }
    async runGlobalSetup(userReporters) {
        await this.runGlobalTeardown();
        const reporter = new InternalReporter(userReporters);
        const config = await this._loadConfigOrReportError(reporter, this._configCLIOverrides);
        if (!config)
            return { status: 'failed', env: [] };
        const { status, cleanup } = await runTasksDeferCleanup(new TestRun(config, reporter), [
            ...createGlobalSetupTasks(config),
        ]);
        const env = [];
        for (const key of new Set([...Object.keys(process.env), ...Object.keys(this._startingEnv)])) {
            if (this._startingEnv[key] !== process.env[key])
                env.push([key, process.env[key] ?? null]);
        }
        if (status !== 'passed')
            await cleanup();
        else
            this._globalSetup = { cleanup };
        return { status, env };
    }
    async runGlobalTeardown() {
        const globalSetup = this._globalSetup;
        const status = await globalSetup?.cleanup();
        this._globalSetup = undefined;
        return { status };
    }
    async clearCache(userReporter) {
        const reporter = new InternalReporter(userReporter ? [userReporter] : []);
        const config = await this._loadConfigOrReportError(reporter);
        if (!config)
            return { status: 'failed' };
        const status = await runTasks(new TestRun(config, reporter), [
            ...createPluginSetupTasks(config),
            createClearCacheTask(config),
        ]);
        return { status };
    }
    async listFiles(userReporter, projects) {
        const reporter = new InternalReporter([userReporter]);
        const config = await this._loadConfigOrReportError(reporter);
        if (!config)
            return { status: 'failed' };
        const options = { projectFilter: projects?.length ? projects : undefined };
        const status = await runTasks(new TestRun(config, reporter, options), [
            createListFilesTask(),
            createReportBeginTask(),
        ]);
        return { status };
    }
    async listTests(userReporter, params) {
        let result;
        this._queue = this._queue.then(async () => {
            const { config, status } = await this._innerListTests(userReporter, params);
            if (config)
                await this._updateWatchedDirs(config);
            result = { status };
        }).catch(printInternalError);
        await this._queue;
        return result;
    }
    async _innerListTests(userReporter, params) {
        const overrides = {
            ...this._configCLIOverrides,
            repeatEach: 1,
            retries: 0,
        };
        const reporter = new InternalReporter([userReporter]);
        const config = await this._loadConfigOrReportError(reporter, overrides);
        if (!config)
            return { status: 'failed' };
        const options = {
            locations: params.locations?.length ? params.locations : undefined,
            grep: params.grep,
            grepInvert: params.grepInvert,
            projectFilter: params.projects?.length ? params.projects : undefined,
            onlyChanged: params.onlyChanged,
            listMode: true,
        };
        const status = await runTasks(new TestRun(config, reporter, options), [
            createLoadTask('out-of-process', { failOnLoadErrors: false, filterOnly: false, populateDependencies: this._populateDependenciesOnList }),
            createReportBeginTask(),
        ]);
        return { config, status };
    }
    async _updateWatchedDirs(config) {
        this._watchedProjectDirs = new Set();
        this._ignoredProjectOutputs = new Set();
        for (const p of config.projects) {
            this._watchedProjectDirs.add(p.project.testDir);
            this._ignoredProjectOutputs.add(p.project.outputDir);
        }
        const result = await resolveCtDirs(config);
        if (result) {
            this._watchedProjectDirs.add(result.templateDir);
            this._ignoredProjectOutputs.add(result.outDir);
        }
        if (this._watchTestDirs)
            await this._updateWatcher(false);
    }
    async _updateWatcher(reportPending) {
        await this._watcher.update([...this._watchedProjectDirs, ...this._watchedTestDependencies], [...this._ignoredProjectOutputs], reportPending);
    }
    async runTests(userReporter, params) {
        let result = { status: 'passed' };
        this._queue = this._queue.then(async () => {
            result = await this._innerRunTests(userReporter, params).catch(e => { printInternalError(e); return { status: 'failed' }; });
        });
        await this._queue;
        return result;
    }
    async _innerRunTests(userReporter, params) {
        await this.stopTests();
        const overrides = {
            ...this._configCLIOverrides,
            repeatEach: 1,
            retries: 0,
            timeout: params.timeout,
            reporter: params.reporters ? params.reporters.map(r => [r]) : undefined,
            use: {
                ...this._configCLIOverrides.use,
                ...(params.trace === 'on' ? { trace: { mode: 'on', sources: false, live: true } } : {}),
                ...(params.trace === 'off' ? { trace: 'off' } : {}),
                ...(params.video === 'on' || params.video === 'off' ? { video: params.video } : {}),
                ...(params.headed !== undefined ? { headless: !params.headed } : {}),
                _optionContextReuseMode: params.reuseContext ? 'when-possible' : undefined,
                _optionConnectOptions: params.connectWsEndpoint ? { wsEndpoint: params.connectWsEndpoint } : undefined,
                actionTimeout: params.actionTimeout,
            },
            ...(params.updateSnapshots ? { updateSnapshots: params.updateSnapshots } : {}),
            ...(params.updateSourceMethod ? { updateSourceMethod: params.updateSourceMethod } : {}),
            ...(params.workers ? { workers: params.workers } : {}),
        };
        const config = await this._loadConfigOrReportError(new InternalReporter([userReporter]), overrides);
        if (!config)
            return { status: 'failed' };
        const options = {
            passWithNoTests: true,
            locations: params.locations?.length ? params.locations : undefined,
            grep: params.grep,
            grepInvert: params.grepInvert,
            projectFilter: params.projects?.length ? params.projects : undefined,
            pauseOnError: params.pauseOnError,
            pauseAtEnd: params.pauseAtEnd,
            preserveOutputDir: true,
            onTestPaused: params => this.emit(TestRunnerEvent.TestPaused, params),
        };
        const configReporters = params.disableConfigReporters ? [] : await createReporters(config, 'test', undefined, options);
        const reporter = new InternalReporter([...configReporters, userReporter]);
        const stop = new ManualPromise();
        const testRun = new TestRun(config, reporter, options);
        if (params.testIds) {
            const testIdSet = new Set(params.testIds);
            testRun.preOnlyTestFilters.push(test => testIdSet.has(test.id));
        }
        const tasks = [
            createApplyRebaselinesTask(),
            createLoadTask('out-of-process', { filterOnly: true, failOnLoadErrors: !!params.failOnLoadErrors, doNotRunDepsOutsideProjectFilter: params.doNotRunDepsOutsideProjectFilter }),
            ...createRunTestsTasks(config),
        ];
        const run = runTasks(testRun, tasks, 0, stop).then(async (status) => {
            this._testRun = undefined;
            return status;
        });
        this._testRun = { run, stop };
        return { status: await run };
    }
    async watch(fileNames) {
        this._watchedTestDependencies = new Set();
        for (const fileName of fileNames) {
            this._watchedTestDependencies.add(fileName);
            cc.dependenciesForTestFile(fileName).forEach(file => this._watchedTestDependencies.add(file));
        }
        await this._updateWatcher(true);
    }
    async findRelatedTestFiles(files, userReporter) {
        const errorReporter = createErrorCollectingReporter(internalScreen);
        const reporter = new InternalReporter(userReporter ? [userReporter, errorReporter] : [errorReporter]);
        const config = await this._loadConfigOrReportError(reporter);
        if (!config)
            return { errors: errorReporter.errors(), testFiles: [] };
        const status = await runTasks(new TestRun(config, reporter), [
            ...createPluginSetupTasks(config),
            createLoadTask('out-of-process', { failOnLoadErrors: true, filterOnly: false, populateDependencies: true }),
        ]);
        if (status !== 'passed')
            return { errors: errorReporter.errors(), testFiles: [] };
        return { testFiles: cc.affectedTestFiles(files) };
    }
    async stopTests() {
        this._testRun?.stop?.resolve();
        await this._testRun?.run;
    }
    async closeGracefully() {
        gracefullyProcessExitDoNotHang(0);
    }
    async stop() {
        await this.runGlobalTeardown();
    }
    async _loadConfig(overrides) {
        try {
            const config = await configLoader.loadConfig(this.configLocation, overrides);
            // Preserve plugin instances between setup and build.
            if (!this._plugins) {
                webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p }));
                addGitCommitInfoPlugin(config);
                this._plugins = config.plugins || [];
            }
            else {
                config.plugins.splice(0, config.plugins.length, ...this._plugins);
            }
            return { config };
        }
        catch (e) {
            return { config: null, error: serializeError(e) };
        }
    }
    async _loadConfigOrReportError(reporter, overrides) {
        const { config, error } = await this._loadConfig(overrides);
        if (config)
            return config;
        // Produce dummy config when it has an error.
        reporter.onConfigure(baseFullConfig);
        reporter.onError(error);
        await reporter.onEnd({ status: 'failed' });
        await reporter.onExit();
        return null;
    }
}
function printInternalError(e) {
    // eslint-disable-next-line no-console
    console.error('Internal error:', e);
}
// TODO: remove CT dependency.
async function resolveCtDirs(config) {
    const use = config.config.projects[0].use;
    const relativeTemplateDir = use.ctTemplateDir || 'playwright';
    const templateDir = await fs.promises.realpath(path.normalize(path.join(config.configDir, relativeTemplateDir))).catch(() => undefined);
    if (!templateDir)
        return null;
    const outDir = use.ctCacheDir ? path.resolve(config.configDir, use.ctCacheDir) : path.resolve(templateDir, '.cache');
    return {
        outDir,
        templateDir
    };
}
export async function runAllTestsWithConfig(config, options) {
    setPlaywrightTestProcessEnv();
    addGitCommitInfoPlugin(config);
    // Legacy webServer support.
    webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p }));
    const filteredProjects = filterProjects(config.projects, options.projectFilter);
    const reporters = await createReporters(config, options.listMode ? 'list' : 'test', undefined, options);
    const lastRun = new LastRunReporter(filteredProjects, options.listMode);
    if (options.lastFailed) {
        const lastFailedTestIds = await lastRun.filterLastFailed();
        if (lastFailedTestIds.length)
            options = { ...options, lastFailedTestIds };
    }
    const reporter = new InternalReporter([...reporters, lastRun]);
    const tasks = options.listMode ? [
        createLoadTask('in-process', { failOnLoadErrors: true, filterOnly: false }),
        createReportBeginTask(),
    ] : [
        createApplyRebaselinesTask(),
        ...createGlobalSetupTasks(config),
        createLoadTask('in-process', { filterOnly: true, failOnLoadErrors: true }),
        ...createRunTestsTasks(config),
    ];
    const testRun = new TestRun(config, reporter, { ...options, pauseAtEnd: config.configCLIOverrides.pause, pauseOnError: config.configCLIOverrides.pause });
    const status = await runTasks(testRun, tasks, config.config.globalTimeout);
    // Calling process.exit() might truncate large stdout/stderr output.
    // See https://github.com/nodejs/node/issues/6456.
    // See https://github.com/nodejs/node/issues/12921
    // eslint-disable-next-line no-restricted-properties
    await new Promise(resolve => process.stdout.write('', () => resolve()));
    // eslint-disable-next-line no-restricted-properties
    await new Promise(resolve => process.stderr.write('', () => resolve()));
    return status;
}
//# sourceMappingURL=testRunner.js.map