import { assert } from '@esm-bundle/chai';
import { HttpRequest } from '../../models/HttpRequest.js';
import { ErrorResponse } from '../../models/ErrorResponse.js';
import { FlowRequestDataEnum, FlowResponseDataEnum, FlowSourceEnum } from '../../models/http-flows/HttpFlows.js';
import { ReadDataSourceStep, ReadDataSourceStepKind, } from '../../models/http-flows/steps/data/ReadDataSourceStep.js';
import { ReadValueStep, ReadValueStepKind } from '../../models/http-flows/steps/data/ReadValueStep.js';
import { HttpCookie } from '../../models/HttpCookie.js';
import { HttpResponse } from '../../models/HttpResponse.js';
import { Headers } from '../../lib/headers/Headers.js';
import { DataExtractor } from '../../data/DataExtractor.js';
import { RequestDataExtractor } from '../../data/RequestDataExtractor.js';
import { AsLowerCaseStepKind } from '../../models/http-flows/steps/transformation/AsLowerCaseStep.js';
import { AsUpperCaseStepKind } from '../../models/http-flows/steps/transformation/AsUpperCaseStep.js';
import { AsNumberStepKind } from '../../models/http-flows/steps/transformation/AsNumberStep.js';
import { RoundStepKind } from '../../models/http-flows/steps/transformation/RoundStep.js';
import { SubstringStepKind } from '../../models/http-flows/steps/transformation/SubstringStep.js';
import { TrimStepKind } from '../../models/http-flows/steps/transformation/TrimStep.js';
import { EqualAssertionKind } from '../../models/http-flows/steps/assertion/EqualAssertion.js';
import { NotEqualAssertionKind } from '../../models/http-flows/steps/assertion/NotEqualAssertion.js';
import { ApiSchemaValues } from '../../amf/ApiSchemaValues.js';
import { GreaterThanAssertionKind, } from '../../models/http-flows/steps/assertion/GreaterThanAssertion.js';
import { LessThanAssertionKind } from '../../models/http-flows/steps/assertion/LessThanAssertion.js';
import { IncludesAssertionKind } from '../../models/http-flows/steps/assertion/IncludesAssertion.js';
import { NotIncludesAssertionKind, } from '../../models/http-flows/steps/assertion/NotIncludesAssertion.js';
import { OkAssertionKind } from '../../models/http-flows/steps/assertion/OkAssertion.js';
import { NotOkAssertionKind } from '../../models/http-flows/steps/assertion/NotOkAssertion.js';
import { ToBeAssertionKind, ToBeTypes } from '../../models/http-flows/steps/assertion/ToBeAssertion.js';
import { NotToBeAssertionKind } from '../../models/http-flows/steps/assertion/NotToBeAssertion.js';
import { IsEmptyAssertionKind } from '../../models/http-flows/steps/assertion/IsEmptyAssertion.js';
import { IsNotEmptyAssertionKind } from '../../models/http-flows/steps/assertion/IsNotEmptyAssertion.js';
import { LengthOfAssertionKind } from '../../models/http-flows/steps/assertion/LengthOfAssertion.js';
import { ReadDataStep, ReadDataStepKind } from '../../models/http-flows/steps/data/ReadDataStep.js';
import { SetDataStep, SetDataStepKind } from '../../models/http-flows/steps/data/SetDataStep.js';
import { SetVariableStep, SetVariableStepKind, } from '../../models/http-flows/steps/data/SetVariableStep.js';
import { SetCookieStep, SetCookieStepKind } from '../../models/http-flows/steps/data/SetCookieStep.js';
import { DeleteCookieStep, DeleteCookieStepKind, } from '../../models/http-flows/steps/data/DeleteCookieStep.js';
/**
 * A class that supports both assertions and flows.
 * The derived classes specializes in a specific user flow (either performing flow actions or running assertions).
 */
export class HttpFlows {
    /**
     * The cumulative list of all variables to be applied to the request and other properties.
     * The variables must be already processed for variables in values (evaluated).
     *
     * These variables are passed by reference. Changes made anywhere to the variables will result
     * with updating this list.
     */
    variables;
    /**
     * An instance of a cookie jar (store) to put/read cookies.
     */
    cookies;
    async runStep(request, step, input, response) {
        switch (step.kind) {
            // data reading
            case ReadDataSourceStepKind:
                return this.readDataSource(request, step, response);
            case ReadValueStepKind:
                return this.readValue(input, step);
            case ReadDataStepKind:
                return this.runReadDataStep(input, step, request, response);
            case SetDataStepKind:
                return this.runSetDataStep(input, step);
            case SetVariableStepKind:
                return this.runSetVariableStep(input, step);
            case SetCookieStepKind:
                return this.runSetCookieStep(input, step, request);
            case DeleteCookieStepKind:
                return this.runDeleteCookieStep(input, step, request);
            // transformations
            case AsLowerCaseStepKind:
                return this.toLowerCase(input);
            case AsUpperCaseStepKind:
                return this.toUpperCase(input);
            case AsNumberStepKind:
                return this.asNumber(input);
            case RoundStepKind:
                return this.round(input, step);
            case TrimStepKind:
                return this.trim(input);
            case SubstringStepKind:
                return this.substring(input, step);
            // assertions
            case EqualAssertionKind:
                return this.assertEqual(input, step);
            case NotEqualAssertionKind:
                return this.assertNotEqual(input, step);
            case GreaterThanAssertionKind:
                return this.assertGreaterThan(input, step);
            case LessThanAssertionKind:
                return this.assertLessThan(input, step);
            case IncludesAssertionKind:
                return this.assertIncludes(input, step);
            case NotIncludesAssertionKind:
                return this.assertNotIncludes(input, step);
            case OkAssertionKind:
                return this.assertOk(input);
            case NotOkAssertionKind:
                return this.assertNotOk(input);
            case ToBeAssertionKind:
                return this.toBe(input, step);
            case NotToBeAssertionKind:
                return this.notToBe(input, step);
            case IsEmptyAssertionKind:
                return this.isEmpty(input);
            case IsNotEmptyAssertionKind:
                return this.isNotEmpty(input);
            case LengthOfAssertionKind:
                return this.lengthOf(input, step);
            default:
                return { value: undefined };
        }
    }
    /**
     * Produces the data source.
     */
    async readDataSource(request, step, response) {
        const instance = new ReadDataSourceStep(step);
        if (instance.enabled === false || !instance.checkValidity()) {
            return { value: undefined };
        }
        const { source, data } = step;
        if (data && [FlowRequestDataEnum.headers, FlowResponseDataEnum.headers].includes(data)) {
            return {
                value: DataExtractor.readHeaders(source === FlowSourceEnum.request ? request : response),
                type: data,
            };
        }
        if (data && [FlowRequestDataEnum.url, FlowResponseDataEnum.url].includes(data)) {
            return {
                value: request.method,
                type: data,
            };
        }
        if (source === FlowSourceEnum.request) {
            if (data === FlowRequestDataEnum.method) {
                return {
                    value: request.method,
                    type: data,
                };
            }
            if (data === FlowRequestDataEnum.body) {
                const instance = new HttpRequest(request);
                const value = await this.processBody(instance);
                return {
                    ...value,
                    type: data,
                };
            }
        }
        if (source === FlowSourceEnum.response) {
            if (!response) {
                return {
                    type: data,
                    value: undefined,
                };
            }
            if (data === FlowResponseDataEnum.status) {
                return {
                    value: response.status,
                    type: data,
                };
            }
            if (data === FlowResponseDataEnum.statusText) {
                return {
                    value: response.statusText,
                    type: data,
                };
            }
            if (data === FlowResponseDataEnum.body) {
                let instance;
                if (ErrorResponse.isErrorResponse(response)) {
                    instance = new ErrorResponse(response);
                }
                else {
                    instance = new HttpResponse(response);
                }
                const value = await this.processBody(instance);
                return {
                    ...value,
                    type: data,
                };
            }
        }
        return { value: undefined };
    }
    async processBody(input) {
        const { headers } = input;
        if (!headers) {
            return { value: undefined };
        }
        const body = await input.readPayloadAsString();
        if (!body) {
            return { value: undefined };
        }
        const parser = new Headers(headers);
        const mime = parser.get('content-type');
        if (!mime) {
            return { value: undefined };
        }
        return {
            value: body,
            bodyMime: mime,
        };
    }
    /**
     * Produces the value from the current input.
     */
    async readValue(input, step) {
        const instance = new ReadValueStep(step);
        if (instance.enabled === false || !instance.checkValidity() || input.value === undefined || input.value === null) {
            return { value: undefined };
        }
        const { path } = step;
        const { type, value } = input;
        const args = path ? path.split('.') : [];
        if (type === FlowRequestDataEnum.url || type === FlowResponseDataEnum.url) {
            if (!value) {
                return { value: undefined };
            }
            const v = String(input.value);
            return {
                value: DataExtractor.getDataUrl(v, args),
            };
        }
        if (type === FlowRequestDataEnum.headers || type === FlowResponseDataEnum.headers) {
            if (!value) {
                return { value: undefined };
            }
            const v = String(input.value);
            return {
                value: DataExtractor.getDataHeaders(v, args),
            };
        }
        if (type === FlowRequestDataEnum.body || type === FlowResponseDataEnum.body) {
            if (!input.bodyMime || !path) {
                return { value: undefined };
            }
            const readValue = await DataExtractor.extractBody(String(value), input.bodyMime, path);
            return {
                value: readValue,
            };
        }
        if (type === FlowResponseDataEnum.status) {
            const v = Number(input.value);
            if (Number.isNaN(v)) {
                return { value: undefined };
            }
            return {
                value: v,
            };
        }
        if (type === FlowResponseDataEnum.statusText) {
            if (!value) {
                return { value: undefined };
            }
            return {
                value,
            };
        }
        return { value: undefined };
    }
    async toLowerCase(input) {
        if (input.value === null || input.value === undefined) {
            return { value: undefined };
        }
        const value = String(input.value);
        return { value: value.toLowerCase() };
    }
    async toUpperCase(input) {
        if (input.value === null || input.value === undefined) {
            return { value: undefined };
        }
        const value = String(input.value);
        return { value: value.toUpperCase() };
    }
    async asNumber(input) {
        if (input.value === null || input.value === undefined) {
            return { value: undefined };
        }
        const value = String(input.value);
        const parsed = Number(value);
        if (Number.isNaN(parsed)) {
            return { value: undefined };
        }
        return { value: parsed };
    }
    async round(input, step) {
        if (input.value === null || input.value === undefined) {
            return { value: undefined };
        }
        const value = String(input.value);
        const parsed = typeof input.value === 'number' ? input.value : Number(value);
        if (Number.isNaN(parsed)) {
            return { value: undefined };
        }
        return { value: parsed.toFixed(step.precision) };
    }
    async trim(input) {
        if (input.value === null || input.value === undefined) {
            return { value: undefined };
        }
        const value = String(input.value);
        return { ...input, value: value.trim() };
    }
    async substring(input, step) {
        if (input.value === null || input.value === undefined) {
            return { value: undefined };
        }
        if (typeof step.start !== 'number') {
            return input;
        }
        const value = String(input.value);
        return { ...input, value: value.substring(step.start, step.end) };
    }
    async assertEqual(input, step) {
        const { value } = input;
        if (step.strict) {
            const comparator = ApiSchemaValues.readTypedValue(step.value, step.dataType);
            assert.strictEqual(value, comparator);
        }
        else {
            assert.equal(String(value), step.value);
        }
        return input;
    }
    async assertNotEqual(input, step) {
        const { value } = input;
        if (step.strict) {
            const comparator = ApiSchemaValues.readTypedValue(step.value, step.dataType);
            assert.notStrictEqual(value, comparator);
        }
        else {
            assert.notEqual(value, step.value);
        }
        return input;
    }
    async assertGreaterThan(input, step) {
        assert.typeOf(input.value, 'number');
        const value = input.value;
        if (step.equal) {
            assert.isAtLeast(value, step.value);
        }
        else {
            assert.isAbove(value, step.value);
        }
        return input;
    }
    async assertLessThan(input, step) {
        assert.typeOf(input.value, 'number');
        const value = input.value;
        if (step.equal) {
            assert.isAtMost(value, step.value);
        }
        else {
            assert.isBelow(value, step.value);
        }
        return input;
    }
    async assertIncludes(input, step) {
        if (this.isPrimitive(input.value)) {
            assert.include(String(input.value), String(step.value));
        }
        else {
            assert.include(JSON.stringify(input.value), String(step.value));
        }
        return input;
    }
    async assertNotIncludes(input, step) {
        if (this.isPrimitive(input.value)) {
            assert.notInclude(String(input.value), String(step.value));
        }
        else {
            assert.notInclude(JSON.stringify(input.value), String(step.value));
        }
        return input;
    }
    async assertOk(input) {
        assert.ok(input.value);
        return input;
    }
    async assertNotOk(input) {
        assert.notOk(input.value);
        return input;
    }
    async toBe(input, step) {
        assert.typeOf(input.value, step.value);
        return input;
    }
    async notToBe(input, step) {
        if (Array.isArray(input.value) && step.value === ToBeTypes.object) {
            assert(false, 'expected [Array] to be an object');
        }
        assert.notTypeOf(input.value, step.value);
        return input;
    }
    async isEmpty(input) {
        assert.isEmpty(input.value);
        return input;
    }
    async isNotEmpty(input) {
        assert.isNotEmpty(input.value);
        return input;
    }
    async lengthOf(input, step) {
        assert.lengthOf(input.value, step.value);
        return input;
    }
    isPrimitive(value) {
        return (typeof value !== 'object' && typeof value !== 'function') || value === null;
    }
    /**
     * A step that reads data from the request, response, or variables.
     *
     * @param step The step configuration.
     * @param request The HttpRequest or the SentRequest (for responses)
     * @param response When available, the response data
     * @returns The read value or undefined.
     */
    async runReadDataStep(input, step, request, response) {
        const instance = new ReadDataStep(step);
        if (instance.enabled === false || !instance.checkValidity()) {
            return input;
        }
        const { source, data, path } = step;
        if (source === FlowSourceEnum.request) {
            const extractor = new RequestDataExtractor(request);
            const value = await extractor.extract(source, data, path);
            return { ...input, value };
        }
        if (source === FlowSourceEnum.response) {
            const extractor = new RequestDataExtractor(request, response);
            const value = await extractor.extract(source, data, path);
            return { ...input, value };
        }
        if (source === FlowSourceEnum.variable && path && this.variables) {
            const value = this.variables[path];
            return { ...input, value };
        }
        return { ...input, value: undefined };
    }
    /**
     * A step that returns a pre-configured value.
     *
     * @param step The step configuration.
     * @returns The read value with applied data type.
     */
    runSetDataStep(input, step) {
        const instance = new SetDataStep(step);
        if (instance.enabled === false || !instance.checkValidity()) {
            return input;
        }
        const { value, dataType } = step;
        if (value === null || value === undefined) {
            return { ...input, value: undefined };
        }
        const result = ApiSchemaValues.parseScalar(value, dataType);
        return {
            ...input,
            value: result,
        };
    }
    /**
     * A step that sets a variable in the current environment.
     * Note, this passes the step value through.
     *
     * @param step The step configuration.
     * @param value The value to set. A value that is `null` or `undefined` deletes the variable.
     */
    runSetVariableStep(input, step) {
        const instance = new SetVariableStep(step);
        if (instance.enabled === false || !instance.checkValidity() || !this.variables) {
            return input;
        }
        if (input.value === null || input.value === undefined) {
            return input;
        }
        const { name } = step;
        this.variables[name] = String(input.value);
        return input;
    }
    /**
     * A step that sets a cookie.
     * Note, this passes the step value through.
     *
     * @param step The step configuration.
     * @param value The value to sent on the cookie.
     * @param request The request object.
     */
    async runSetCookieStep(input, step, request) {
        const instance = new SetCookieStep(step);
        const { cookies } = this;
        if (instance.enabled === false || !instance.checkValidity() || !cookies) {
            return input;
        }
        if (input.value === null || input.value === undefined) {
            return input;
        }
        const { name, expires, hostOnly, httpOnly, sameSite, secure, session } = step;
        const typedValue = String(input.value);
        let url;
        if (step.url) {
            url = step.url;
        }
        else {
            url = request.url;
        }
        const cookie = new HttpCookie({
            name: name,
            value: typedValue,
            sameSite: 'unspecified',
        });
        if (sameSite) {
            const typed = sameSite;
            switch (typed.toLowerCase()) {
                case 'lax':
                    cookie.sameSite = 'lax';
                    break;
                case 'strict':
                    cookie.sameSite = 'strict';
                    break;
                case 'none':
                    cookie.sameSite = 'no_restriction';
                    break;
            }
        }
        if (expires) {
            cookie.expirationDate = expires;
        }
        if (typeof hostOnly === 'boolean') {
            cookie.hostOnly = hostOnly;
        }
        if (typeof httpOnly === 'boolean') {
            cookie.httpOnly = httpOnly;
        }
        if (typeof secure === 'boolean') {
            cookie.secure = secure;
        }
        if (typeof session === 'boolean') {
            cookie.session = session;
        }
        await cookies.setCookies(url, [cookie]);
        return input;
    }
    /**
     * A step to delete cookies from the cookie jar.
     *
     * Note, this passes the step value through.
     */
    async runDeleteCookieStep(input, step, request) {
        const instance = new DeleteCookieStep(step);
        const { cookies } = this;
        if (instance.enabled === false || !instance.checkValidity() || !cookies) {
            return input;
        }
        let url;
        if (step.url) {
            url = step.url;
        }
        else {
            url = request.url;
        }
        await cookies.deleteCookies(url, step.name);
        return input;
    }
}
//# sourceMappingURL=HttpFlows.js.map