"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerApiRequestStep = registerApiRequestStep;
const attachments_1 = require("../../reporter/attachments");
const form_data_1 = __importDefault(require("form-data"));
const crypto_1 = __importDefault(require("crypto"));
const fs_1 = __importDefault(require("fs"));
const https_1 = __importDefault(require("https"));
const path_1 = __importDefault(require("path"));
const qs_1 = __importDefault(require("qs"));
const TMP_DIR = "/tmp/.solidx-testing-files";
const MAX_REDIRECTS = 5;
function isPlainObject(value) {
    return !!value && typeof value === "object" && !Array.isArray(value);
}
function deepMerge(target, source) {
    if (isPlainObject(target) && isPlainObject(source)) {
        const out = { ...target };
        for (const [key, value] of Object.entries(source)) {
            if (key in out) {
                out[key] = deepMerge(out[key], value);
            }
            else {
                out[key] = value;
            }
        }
        return out;
    }
    return source;
}
function stripUndefined(value) {
    if (Array.isArray(value)) {
        return value.map(stripUndefined);
    }
    if (isPlainObject(value)) {
        const out = {};
        for (const [key, val] of Object.entries(value)) {
            if (val === undefined)
                continue;
            out[key] = stripUndefined(val);
        }
        return out;
    }
    return value;
}
function ensureTmpDir() {
    fs_1.default.mkdirSync(TMP_DIR, { recursive: true });
}
function buildTempFilePath(urlObj) {
    const ext = path_1.default.extname(urlObj.pathname);
    const base = path_1.default.basename(urlObj.pathname, ext) || "file";
    const safeBase = base.replace(/[^a-zA-Z0-9-_]/g, "_");
    const suffix = `${Date.now()}-${crypto_1.default.randomBytes(6).toString("hex")}`;
    return path_1.default.join(TMP_DIR, `${safeBase}-${suffix}${ext}`);
}
function downloadUrlToFile(urlValue, redirectCount = 0) {
    if (!urlValue.startsWith("https://")) {
        throw new Error(`Only https URLs are allowed for file downloads: ${urlValue}`);
    }
    if (redirectCount > MAX_REDIRECTS) {
        throw new Error(`Too many redirects while downloading file: ${urlValue}`);
    }
    const urlObj = new URL(urlValue);
    ensureTmpDir();
    const filePath = buildTempFilePath(urlObj);
    return new Promise((resolve, reject) => {
        const fileStream = fs_1.default.createWriteStream(filePath);
        const request = https_1.default.get(urlObj, (response) => {
            const status = response.statusCode ?? 0;
            const location = response.headers.location;
            if (status >= 300 && status < 400 && location) {
                response.resume();
                fileStream.close(() => fs_1.default.unlink(filePath, () => { }));
                const nextUrl = new URL(location, urlObj).toString();
                downloadUrlToFile(nextUrl, redirectCount + 1).then(resolve).catch(reject);
                return;
            }
            if (status >= 400) {
                response.resume();
                fileStream.close(() => fs_1.default.unlink(filePath, () => { }));
                reject(new Error(`Failed to download file. HTTP ${status}`));
                return;
            }
            response.pipe(fileStream);
        });
        request.on("error", (err) => {
            fileStream.close(() => fs_1.default.unlink(filePath, () => { }));
            reject(err);
        });
        fileStream.on("error", (err) => {
            request.destroy();
            fs_1.default.unlink(filePath, () => { });
            reject(err);
        });
        fileStream.on("finish", () => {
            fileStream.close(() => resolve(filePath));
        });
    });
}
function resolveFilePath(rawValue) {
    const value = rawValue.startsWith("file:") ? rawValue.slice(5) : rawValue;
    if (!value) {
        throw new Error('Invalid file reference. Use "file:/absolute/path".');
    }
    if (!path_1.default.isAbsolute(value)) {
        throw new Error(`File path must be absolute: ${value}`);
    }
    if (!fs_1.default.existsSync(value)) {
        throw new Error(`File does not exist: ${value}`);
    }
    return value;
}
async function resolveFileReference(rawValue) {
    if (rawValue.startsWith("url:")) {
        const urlValue = rawValue.slice(4);
        if (!urlValue) {
            throw new Error('Invalid url reference. Use "url:https://...".');
        }
        const filePath = await downloadUrlToFile(urlValue);
        return { filePath, source: "url", url: urlValue };
    }
    return { filePath: resolveFilePath(rawValue), source: "file" };
}
function formItemsFromRecord(record) {
    return Object.entries(record).map(([name, value]) => {
        if (typeof value === "string" && (value.startsWith("file:") || value.startsWith("url:"))) {
            return { name, value, type: "file" };
        }
        return { name, value };
    });
}
async function buildFormData(items) {
    const form = new form_data_1.default();
    const logItems = [];
    for (const item of items) {
        if (!item?.name) {
            throw new Error('Form item is missing required "name".');
        }
        const rawValue = item.value;
        const isFile = item.type === "file" || (typeof rawValue === "string" && (rawValue.startsWith("file:") || rawValue.startsWith("url:")));
        if (isFile) {
            if (typeof rawValue !== "string") {
                throw new Error(`Form file value must be a string for field: ${item.name}`);
            }
            const resolved = await resolveFileReference(rawValue);
            const filename = item.filename ?? path_1.default.basename(resolved.filePath);
            form.append(item.name, fs_1.default.createReadStream(resolved.filePath), {
                filename,
                contentType: item.contentType,
            });
            logItems.push({
                name: item.name,
                type: "file",
                source: resolved.source,
                url: resolved.url,
                path: resolved.filePath,
                filename,
                contentType: item.contentType,
            });
        }
        else {
            const textValue = rawValue === undefined || rawValue === null ? "" : typeof rawValue === "string" ? rawValue : JSON.stringify(rawValue);
            form.append(item.name, textValue);
            logItems.push({
                name: item.name,
                type: "text",
                value: textValue,
            });
        }
    }
    return { form, logItems };
}
function registerApiRequestStep(registry) {
    registry.register("api.request", async (ctx, step) => {
        if (!ctx.api) {
            throw new Error('Missing API adapter on context for op "api.request"');
        }
        const input = (step.with ?? {});
        if (!input.method) {
            throw new Error('Missing "method" in step.with for op "api.request"');
        }
        if (!input.url) {
            throw new Error('Missing "url" in step.with for op "api.request"');
        }
        const rawFormData = input.formData ?? input.body;
        const formItems = Array.isArray(rawFormData) ? rawFormData : isPlainObject(rawFormData) ? formItemsFromRecord(rawFormData) : undefined;
        if (rawFormData !== undefined && !formItems) {
            throw new Error('formData/body must be an array of items or an object, for op "api.request".');
        }
        if (formItems && (input.json !== undefined || input.bodyText !== undefined)) {
            throw new Error('Use either formData/body or json/bodyText, not both, for op "api.request".');
        }
        const rawUrl = input.url;
        const absolute = /^[a-z][a-z0-9+.-]*:/i.test(rawUrl);
        const urlObj = new URL(rawUrl, "http://solid.local");
        const urlQuery = urlObj.search
            ? qs_1.default.parse(urlObj.search, { ignoreQueryPrefix: true, allowDots: true, depth: 10 })
            : {};
        let stepQuery = {};
        if (input.query !== undefined) {
            stepQuery =
                typeof input.query === "string"
                    ? qs_1.default.parse(input.query, { ignoreQueryPrefix: true, allowDots: true, depth: 10 })
                    : input.query;
        }
        const mergedQuery = deepMerge(urlQuery, stepQuery);
        const queryString = qs_1.default.stringify(mergedQuery, { addQueryPrefix: true, allowDots: true });
        urlObj.search = queryString;
        const finalUrl = absolute
            ? urlObj.toString()
            : `${urlObj.pathname}${urlObj.search}${urlObj.hash}`;
        let formData;
        let formLogItems;
        if (formItems) {
            const built = await buildFormData(formItems);
            formData = built.form;
            formLogItems = built.logItems;
        }
        const req = {
            method: input.method,
            url: finalUrl,
            headers: input.headers,
            json: input.json,
            bodyText: input.bodyText,
            formData,
        };
        const startedAt = Date.now();
        const printApiLogs = ctx.options?.printApiLogs ?? false;
        const logName = `api.request ${req.method} ${finalUrl}`;
        const requestLog = stripUndefined({
            method: req.method,
            url: finalUrl,
            headers: req.headers,
            query: mergedQuery,
            queryString: urlObj.search ? urlObj.search.slice(1) : "",
            json: input.json,
            bodyText: input.bodyText,
            formData: formLogItems,
        });
        let response;
        try {
            response = await ctx.api.http(req);
        }
        catch (err) {
            if (printApiLogs) {
                (0, attachments_1.attachJson)(ctx, logName, {
                    request: requestLog,
                    durationMs: Date.now() - startedAt,
                    error: err instanceof Error ? err.message : String(err),
                });
            }
            throw err;
        }
        const durationMs = Date.now() - startedAt;
        ctx.last = { ...(ctx.last ?? {}), apiResponse: response };
        if (printApiLogs) {
            const responseLog = stripUndefined({
                status: response.status,
                headers: response.headers,
                bodyJson: response.bodyJson,
                bodyText: response.bodyJson === undefined ? response.bodyText : undefined,
            });
            (0, attachments_1.attachJson)(ctx, logName, {
                request: requestLog,
                response: responseLog,
                durationMs,
            });
        }
        return {
            status: response.status,
            headers: response.headers,
            bodyText: response.bodyText,
            bodyJson: response.bodyJson,
            body: response.bodyJson ?? response.bodyText,
        };
    });
}
//# sourceMappingURL=request.step.js.map