// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0

import { RunnerStatus, TestStatus } from './batchedtestrunner.js';

// Sends messages to the running emrun instance via POST requests.
export class EmrunCommunication {
    static #BATCHING_DELAY = 300;

    #indexOfMessage = 0;
    #postOutputPromise;
    // Accumulate output in a batch that gets sent with a delay so that the emrun http server
    // does not get pounded with requests.
    #nextOutputBatch = null;

    #post(body) {
        return fetch('stdio.html', {
            method: 'POST',
            body
        });
    }

    // Waits for the output sending to finish, if any output transfer is still in progress.
    async waitUntilAllSent()
    {
        if (this.#postOutputPromise)
            await this.#postOutputPromise;
    }

    // Posts the exit status to the running emrun instance. Emrun will drop connection unless it is
    // run with --serve_after_exit, therefore this method will throw most of the times.
    postExit(status) {
        return this.#post(`^exit^${status}`);
    }

    // Posts an indexed output chunk to the running emrun instance. Each consecutive call to this
    // method increments the output index by 1.
    postOutput(output)
    {
        if (this.#nextOutputBatch) {
            this.#nextOutputBatch += output;
        } else {
            this.#nextOutputBatch = output;
            this.#postOutputPromise = new Promise(resolve =>
            {
                window.setTimeout(() =>
                {
                    const toSend = this.#nextOutputBatch;
                    this.#nextOutputBatch = null;
                    this.#post(`^out^${this.#indexOfMessage++}^${toSend}$`)
                        .finally(resolve);
                }, EmrunCommunication.#BATCHING_DELAY);
            });
        }

        return this.#postOutputPromise;
    }
}

// Wraps a test module runner; forwards its output and resolution state to the running emrun
// instance.
export class EmrunAdapter {
    #communication;
    #batchedTestRunner;
    #sentLines = 0;
    #onExitSent;

    constructor(communication, batchedTestRunner, onExitSent) {
        this.#communication = communication;
        this.#batchedTestRunner = batchedTestRunner;
        this.#onExitSent = onExitSent;
    }

    // Starts listening to test module runner's state changes. When the test module runner finishes
    // or reports output, sends suitable messages to the emrun instance.
    run() {
        this.#batchedTestRunner.onStatusChanged.addEventListener(
            status => this.#onRunnerStatusChanged(status));
        this.#batchedTestRunner.onTestStatusChanged.addEventListener(
            (test, status) => this.#onTestStatusChanged(test, status));
        this.#batchedTestRunner.onTestOutputChanged.addEventListener(
            (test, output) => this.#onTestOutputChanged(test, output));

        const currentTest = [...this.#batchedTestRunner.results.entries()].find(
            entry => entry[1].status === TestStatus.Running)?.[0];

        const output = this.#batchedTestRunner.results.get(currentTest)?.output;
        if (output)
            this.#onTestOutputChanged(testName, output);
        this.#onRunnerStatusChanged(this.#batchedTestRunner.status);
    }

    #toExitCode(status) {
        switch (status) {
            case RunnerStatus.Error:
                return -1;
            case RunnerStatus.Passed:
                return 0;
            case RunnerStatus.Running:
                throw new Error('No exit code when still running');
            case RunnerStatus.TestCrashed:
                return -2;
            case RunnerStatus.TestsFailed:
                return this.#batchedTestRunner.numberOfFailed;
        }
    }

    async #onRunnerStatusChanged(status) {
        if (RunnerStatus.Running === status)
            return;

        const exit = this.#toExitCode(status);
        if (RunnerStatus.Error === status)
            this.#communication.postOutput(this.#batchedTestRunner.errorDetails);

        await this.#communication.waitUntilAllSent();
        try {
            await this.#communication.postExit(exit);
        } catch {
            // no-op: The remote end will drop connection on exit.
        } finally {
            this.#onExitSent?.();
        }
    }

    async #onTestOutputChanged(_, output) {
        const notSent = output.slice(this.#sentLines);
        for (const out of notSent)
            this.#communication.postOutput(out);
        this.#sentLines = output.length;
    }

    async #onTestStatusChanged(_, status) {
        if (status === TestStatus.Running)
            this.#sentLines = 0;
    }
}
