/* eslint-disable no-redeclare */
/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
import { EventEmitter } from 'events';
import { Kind as EnvironmentKind } from '../../models/Environment.js';
import { RequestLog } from '../../models/RequestLog.js';
import { Kind as ProjectRequestKind } from '../../models/ProjectRequest.js';
import { SentRequest } from '../../models/SentRequest.js';
import { ErrorResponse } from '../../models/ErrorResponse.js';
import { VariablesProcessor } from '../variables/VariablesProcessor.js';
import { State } from './enums.js';
import { HttpRequestRunner } from '../http-runner/HttpRequestRunner.js';
import { ProjectFolderKind } from '../../models/kinds.js';
/**
 * Runs requests in a project.
 * Developers can run the entire project with the `recursive` flag set. They can also
 * set the starting point with the `parent` options.
 *
 * Requests are executed in order defined in the folder.
 */
export class ProjectRequestRunner extends EventEmitter {
    logger;
    project;
    /**
     * An instance of a cookie jar (store) to put/read cookies.
     */
    cookies;
    masterEnvironment;
    extraVariables;
    /**
     * The variables processor instance.
     */
    variablesProcessor = new VariablesProcessor();
    _state = State.Idle;
    get state() {
        return this._state;
    }
    constructor(project, opts = {}) {
        super();
        this.project = project;
        this.logger = opts.logger;
        this.masterEnvironment = opts.environment;
        this.extraVariables = opts.variables;
        this.cookies = opts.cookies;
    }
    /**
     * Runs the request from the project root or a specified folder.
     * @param options Run options.
     * @returns A promise with the run result.
     */
    async run(options = {}) {
        const { project } = this;
        const root = options.parent ? project.findFolder(options.parent) : project;
        if (!root) {
            throw new Error(`The parent folder not found: ${options.parent}.`);
        }
        const variables = await this.getVariables(root);
        const executed = [];
        for await (const result of this.runIterator(options, variables)) {
            executed.push(result);
        }
        return {
            items: executed,
            variables,
        };
    }
    /**
     * Creates an async iterator that allows to iterate over execution results.
     * The result is yielded after the request is executed.
     *
     * @param options The iterator configuration.
     */
    async *runIterator(options = {}, variables) {
        const { project } = this;
        const root = options.parent ? project.findFolder(options.parent) : project;
        if (!root) {
            throw new Error(`The parent folder not found: ${options.parent}.`);
        }
        this._state = State.Running;
        const envVariables = variables || (await this.getVariables(root));
        const { items } = root;
        const it = this._runIterator(items, envVariables, options);
        for await (const request of it) {
            yield request;
        }
    }
    async *_runIterator(items, variables, options) {
        for (const item of items) {
            const current = item.getItem();
            if (!current) {
                continue;
            }
            if (current.kind === ProjectRequestKind) {
                if (Array.isArray(options.ignore) && options.ignore.includes(current.key)) {
                    continue;
                }
                if (Array.isArray(options.requests) &&
                    !options.requests.includes(current.key) &&
                    !options.requests.includes(current.info.name || '')) {
                    continue;
                }
                const info = await this._runItem(current, variables);
                yield info;
            }
            else if (current.kind === ProjectFolderKind) {
                if (!options.recursive) {
                    continue;
                }
                const parent = current;
                // make a copy so the variables executed in the encapsulated environment
                // won't leak out to the current environment.
                let encapsulated = false;
                // we restore the `baseUri` after exiting the folder.
                const currentBaseUri = variables.baseUri;
                const { items: parentItems } = parent;
                const envItem = parentItems.find((i) => i.kind === EnvironmentKind);
                let parentVariables = {};
                let childVariables = variables;
                if (envItem) {
                    const parentEnv = envItem.getItem();
                    if (parentEnv) {
                        encapsulated = parentEnv.encapsulated;
                        parentVariables = await this.applyVariables([parentEnv]);
                        if (encapsulated) {
                            childVariables = parentVariables;
                        }
                        else {
                            childVariables = { ...childVariables, ...parentVariables };
                        }
                    }
                }
                const it = this._runIterator(parentItems, childVariables, options);
                for await (const request of it) {
                    yield request;
                }
                if (!encapsulated) {
                    // now we set the variables set on children to the main variables object, except for these
                    // declared on the child environments, so these won't leak to other folders.
                    // However, variables set by a request in a folder always is propagated to the main environment.
                    const ignore = Object.keys(parentVariables);
                    Object.keys(childVariables).forEach((key) => {
                        if (!ignore.includes(key)) {
                            variables[key] = childVariables[key];
                        }
                    });
                }
                variables.baseUri = currentBaseUri;
            }
        }
    }
    /**
     * Aborts the current run.
     * The promise returned by the `run()` method will reject if not yet resolved.
     */
    abort() {
        this._state = State.Aborted;
    }
    /**
     * Allows to iterate over project requests recursively and execute each request
     * in order. The generator yields the `RunResult` for the request.
     *
     * Example:
     *
     * ```javascript
     * const runner = new ProjectRequestRunner(...);
     * for await (let runResult of runner) {
     *  console.log(runResult);
     * }
     * ```
     */
    async *[Symbol.asyncIterator]() {
        for await (const result of this.runIterator({ recursive: true })) {
            yield result;
        }
    }
    async _runItem(request, variables) {
        if (this._state === State.Aborted) {
            throw new Error(`The execution has been aborted.`);
        }
        const info = await this.execute(request, variables);
        const folder = request.getParent();
        if (folder && folder !== this.project) {
            info.parent = folder.key;
        }
        return info;
    }
    async execute(request, variables) {
        const config = request.getConfig();
        const factory = new HttpRequestRunner();
        factory.variables = variables;
        const auth = request.readAuthorization();
        if (auth.length) {
            factory.authorization = auth.map((i) => i.toJSON());
        }
        if (request.flows) {
            factory.flows = request.flows;
        }
        if (request.assertions) {
            factory.assertions = request.assertions;
        }
        if (request.clientCertificate) {
            factory.certificates = [request.clientCertificate.toJSON()];
        }
        if (config.enabled !== false) {
            factory.config = config.toJSON();
        }
        if (this.logger) {
            factory.logger = this.logger;
        }
        if (this.cookies) {
            factory.cookies = this.cookies;
        }
        const info = {
            key: request.key,
        };
        let url;
        const typedProject = request;
        if (typeof typedProject.readUrl === 'function') {
            url = typedProject.readUrl({ variables });
        }
        const requestData = request.expects.toJSON();
        try {
            // Below replaces the single call to the `run()` function of the factory to
            // report via the events a request object that has evaluated with the Jexl library.
            requestData.url = url || factory.prepareRequestUrl(requestData.url, variables);
            const requestCopy = await factory.applyVariables(requestData);
            await factory.applyAuthorization(requestCopy);
            await factory.applyCookies(requestCopy);
            this.emit('request', request.key, { ...requestCopy });
            await factory.runRequestFlows(requestCopy);
            const result = await factory.executeRequest(requestCopy);
            result.requestId = request.key;
            await factory.processResponse(result);
            request.setLog(result);
            info.log = result;
            this.emit('response', request.key, { ...result });
        }
        catch (e) {
            const cause = e;
            info.error = true;
            info.errorMessage = cause.message;
            const sent = new SentRequest({ ...requestData, startTime: 0, endTime: 0 });
            const response = ErrorResponse.fromError(info.errorMessage);
            const log = RequestLog.fromRequestResponse(sent.toJSON(), response.toJSON()).toJSON();
            this.emit('error', request.key, log, info.errorMessage);
        }
        return info;
    }
    async getVariables(parent) {
        if (this.masterEnvironment) {
            return this.applyVariables([this.masterEnvironment]);
        }
        return this.createEnvironment(parent);
    }
    async createEnvironment(parent) {
        const envs = await this.readEnvironments(parent);
        return this.applyVariables(envs);
    }
    /**
     * Reads the list of the environments to apply to this runtime.
     */
    async readEnvironments(parent) {
        const folderKey = parent.kind === ProjectFolderKind ? parent.key : undefined;
        return this.project.readEnvironments({ parent: folderKey });
    }
    /**
     * Reads the variables and the base URI from the passed environments.
     */
    async applyVariables(environments) {
        let baseUri = '';
        const variables = [];
        environments.forEach((environment) => {
            const { server, variables: envVariables } = environment;
            if (server) {
                baseUri = server.readUri();
            }
            if (envVariables.length) {
                envVariables.forEach((item) => {
                    const defined = variables.findIndex((i) => i.name === item.name);
                    if (defined >= 0) {
                        variables[defined] = item;
                    }
                    else {
                        variables.push(item);
                    }
                });
            }
        });
        const { extraVariables } = this;
        const ctx = VariablesProcessor.createContextFromProperties(variables);
        if (extraVariables) {
            Object.keys(extraVariables).forEach((key) => {
                ctx[key] = extraVariables[key];
            });
        }
        // the `baseUri` is reserved and always set to the environment's `baseUri`.
        ctx.baseUri = baseUri || '';
        return this.variablesProcessor.buildContext(ctx);
    }
}
//# sourceMappingURL=ProjectRequestRunner.js.map