/**
 * Browser-compatible test detection utilities.
 * This module provides pure parsing functionality that works with strings/objects,
 * without dependencies on Node.js file system or path modules.
 */
import YAML from "yaml";
import { validate, transformToSchemaKey } from "./validate.js";
import { defaultFileTypes, detectFileTypeFromContent } from "./fileTypes.js";
/**
 * Creates a RegExp from a pattern string with safety checks against ReDoS.
 * Returns null if the pattern is invalid or potentially unsafe.
 *
 * The pattern is reconstructed character-by-character to establish a
 * sanitization boundary, since these patterns come from trusted file type
 * configuration rather than arbitrary user input.
 */
function safeRegExp(pattern, flags) {
    if (typeof pattern !== 'string' || pattern.length === 0)
        return null;
    // Reject excessively long patterns
    if (pattern.length > 1500)
        return null;
    // Reconstruct pattern to establish sanitization boundary
    const sanitized = Array.from(pattern, c => String.fromCharCode(c.charCodeAt(0))).join('');
    try {
        return new RegExp(sanitized, flags);
    }
    catch {
        return null;
    }
}
// Keys excluded when hashing a test/step definition for an ID: IDs are what
// the hash computes (or other generated IDs), and `location` shifts whenever
// unrelated content above moves in the file — including either would defeat
// run-over-run stability.
const HASH_EXCLUDED_KEYS = new Set([
    "location",
    "testId",
    "stepId",
    "contextId",
]);
/**
 * Deterministic 8-hex-char content hash (FNV-1a, 32-bit). Used to derive
 * stable fallback IDs from a test's or step's authored definition so the same
 * content produces the same ID on every run — that's what makes run-over-run
 * result comparison possible. Pure JS (no node:crypto) so it stays
 * browser-compatible.
 */
export function contentHash(value) {
    // JSON.stringify returns undefined for unserializable inputs (undefined,
    // functions, symbols) and throws for circular references or BigInt. Fall
    // back to "" in both cases so ID generation degrades to a stable hash
    // instead of crashing.
    let input;
    if (typeof value === "string") {
        input = value;
    }
    else {
        try {
            input =
                JSON.stringify(value, (key, v) => HASH_EXCLUDED_KEYS.has(key) ? undefined : v) ?? "";
        }
        catch {
            input = "";
        }
    }
    let hash = 0x811c9dc5;
    for (let i = 0; i < input.length; i++) {
        hash ^= input.charCodeAt(i);
        hash = Math.imul(hash, 0x01000193);
    }
    return (hash >>> 0).toString(16).padStart(8, "0");
}
// Pure-string version of core's generateSpecId path normalization (no
// node:path so it stays browser-compatible). Used as the ID base when the
// caller doesn't supply `testIdBase`.
function normalizePathForId(filePath) {
    if (!filePath)
        return "";
    return filePath
        .replace(/\\/g, "/")
        .replace(/^\.\//, "")
        .replace(/[^a-zA-Z0-9._/-]/g, "_");
}
/**
 * Precomputes an array of line-start character offsets for the given content.
 * Each entry is the index of the first character on that line (0-indexed offsets, 1-indexed lines).
 */
export function getLineStarts(content) {
    const starts = [0];
    for (let i = 0; i < content.length; i++) {
        if (content[i] === '\n')
            starts.push(i + 1);
    }
    return starts;
}
/**
 * Returns the 1-indexed line number for a given character index.
 * Uses binary search over precomputed line starts, or scans linearly if none provided.
 */
export function getLineNumber(content, index, lineStarts) {
    if (lineStarts) {
        let lo = 0, hi = lineStarts.length - 1;
        while (lo <= hi) {
            const mid = (lo + hi) >>> 1;
            if (lineStarts[mid] <= index)
                lo = mid + 1;
            else
                hi = mid - 1;
        }
        return lo; // 1-indexed: lo is the count of starts <= index
    }
    let line = 1;
    for (let i = 0; i < index; i++) {
        if (content[i] === '\n')
            line++;
    }
    return line;
}
/**
 * Browser-compatible test detection function.
 * Detects tests from content string using specified file type configuration.
 *
 * This is the main entry point for test detection in Common.
 * It works with content strings rather than file paths, making it browser-compatible.
 *
 * @param input - Detection input
 * @param input.content - Content string to parse for tests
 * @param input.filePath - File path (for metadata only, not file I/O)
 * @param input.fileType - File type configuration with parsing rules
 * @param input.config - Optional configuration
 * @returns Array of detected tests
 *
 * @example
 * ```typescript
 * const tests = await detectTests({
 *   content: markdownContent,
 *   filePath: 'docs/test.md',
 *   fileType: { extensions: ['md'], markup: [...] },
 *   config: { detectSteps: true }
 * });
 * ```
 */
export async function detectTests(input) {
    return parseContent({
        config: input.config,
        content: input.content,
        filePath: input.filePath,
        fileType: input.fileType,
        testIdBase: input.testIdBase,
    });
}
/**
 * Parses XML-style attributes to an object.
 * Example: 'wait=500' becomes { wait: 500 }
 * Example: 'testId="myTestId" detectSteps=false' becomes { testId: "myTestId", detectSteps: false }
 * Example: 'httpRequest.url="https://example.com"' becomes { httpRequest: { url: "https://example.com" } }
 */
export function parseXmlAttributes({ stringifiedObject }) {
    if (typeof stringifiedObject !== "string") {
        return null;
    }
    const str = stringifiedObject.trim();
    // Check if it looks like JSON or YAML - if so, return null to let JSON/YAML parsers handle it
    if (str.startsWith("{") || str.startsWith("[")) {
        return null;
    }
    // Check if it looks like YAML (key: value pattern)
    const yamlPattern = /^\w+:\s/;
    if (yamlPattern.test(str)) {
        return null;
    }
    if (str.startsWith("-")) {
        return null;
    }
    // Parse XML-style attributes
    const result = {};
    const attrRegex = /([\w.]+)=(?:"([^"]*)"|'([^']*)'|(\S+))/g;
    let match;
    let hasMatches = false;
    while ((match = attrRegex.exec(str)) !== null) {
        hasMatches = true;
        const keyPath = match[1];
        let value = match[2] !== undefined ? match[2] : match[3] !== undefined ? match[3] : match[4];
        // Try to parse as boolean
        if (value === "true") {
            value = true;
        }
        else if (value === "false") {
            value = false;
        }
        else if (!isNaN(value) && value !== "") {
            value = Number(value);
        }
        // Handle dot notation for nested objects
        if (keyPath.includes(".")) {
            const keys = keyPath.split(".");
            // Skip paths that could cause prototype pollution
            if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype'))
                continue;
            let current = result;
            for (let i = 0; i < keys.length - 1; i++) {
                const key = keys[i];
                /* c8 ignore next - unreachable: the keys.some() guard above already skips any keyPath containing these segments */
                if (key === '__proto__' || key === 'constructor' || key === 'prototype')
                    break;
                if (!current[key] || typeof current[key] !== "object") {
                    current[key] = {};
                }
                current = current[key];
            }
            const lastKey = keys[keys.length - 1];
            if (lastKey !== '__proto__' && lastKey !== 'constructor' && lastKey !== 'prototype') {
                current[lastKey] = value;
            }
        }
        else if (keyPath !== '__proto__' && keyPath !== 'constructor' && keyPath !== 'prototype') {
            result[keyPath] = value;
        }
    }
    return hasMatches ? result : null;
}
/**
 * Parses a JSON or YAML object from a string.
 */
export function parseObject({ stringifiedObject }) {
    if (typeof stringifiedObject === "string") {
        // Decode XML/HTML entities (e.g., &#34; → " and &#39; → ') that may be
        // introduced by DITA OT or other XML processors when normalizing attribute
        // values. &amp; must be decoded last so author-escaped sequences like
        // "&amp;lt;" (meant to render as the literal text "&lt;") aren't double-decoded
        // into "<".
        if (/&(?:#\d+|#x[0-9a-fA-F]+|amp|lt|gt|quot|apos);/.test(stringifiedObject)) {
            stringifiedObject = stringifiedObject
                .replace(/&#39;|&apos;/g, "'")
                .replace(/&#34;|&quot;/g, '"')
                .replace(/&lt;/g, "<")
                .replace(/&gt;/g, ">")
                .replace(/&amp;/g, "&");
        }
        // First, try to parse as XML attributes
        const xmlAttrs = parseXmlAttributes({ stringifiedObject });
        if (xmlAttrs !== null) {
            return xmlAttrs;
        }
        // Try to parse as JSON
        try {
            const json = JSON.parse(stringifiedObject);
            if (typeof json !== "object" || json === null || Array.isArray(json))
                return null;
            return json;
        }
        catch (jsonError) {
            // JSON parsing failed - check if this looks like escaped JSON
            const trimmedString = stringifiedObject.trim();
            const looksLikeEscapedJson = (trimmedString.startsWith("{") || trimmedString.startsWith("[")) &&
                trimmedString.includes('\\"');
            if (looksLikeEscapedJson) {
                try {
                    const stringToParse = JSON.parse('"' + stringifiedObject + '"');
                    const result = JSON.parse(stringToParse);
                    if (typeof result !== "object" || result === null || Array.isArray(result))
                        return null;
                    return result;
                }
                catch {
                    // Fallback to simple quote replacement
                    try {
                        const unescaped = stringifiedObject.replace(/\\"/g, '"');
                        const result = JSON.parse(unescaped);
                        if (typeof result !== "object" || result === null || Array.isArray(result))
                            return null;
                        return result;
                    }
                    catch {
                        // Continue to YAML parsing
                    }
                }
            }
            // Try to parse as YAML
            try {
                const yaml = YAML.parse(stringifiedObject);
                if (typeof yaml !== "object" || yaml === null || Array.isArray(yaml))
                    return null;
                return yaml;
            }
            catch (yamlError) {
                return null;
            }
        }
    }
    return stringifiedObject;
}
/**
 * Replaces numeric variables ($0, $1, etc.) in strings and objects with provided values.
 */
export function replaceNumericVariables(stringOrObjectSource, values) {
    let stringOrObject = JSON.parse(JSON.stringify(stringOrObjectSource));
    if (typeof stringOrObject !== "string" && typeof stringOrObject !== "object") {
        throw new Error("Invalid stringOrObject type");
    }
    if (typeof values !== "object") {
        throw new Error("Invalid values type");
    }
    if (typeof stringOrObject === "string") {
        const matches = stringOrObject.match(/\$[0-9]+/g);
        if (matches) {
            const allExist = matches.every((variable) => {
                const index = variable.substring(1);
                return Object.hasOwn(values, index) && typeof values[index] !== "undefined";
            });
            if (!allExist) {
                return null;
            }
            else {
                stringOrObject = stringOrObject.replace(/\$[0-9]+/g, (variable) => {
                    const index = variable.substring(1);
                    return values[index];
                });
            }
        }
    }
    if (typeof stringOrObject === "object") {
        Object.keys(stringOrObject).forEach((key) => {
            if (typeof stringOrObject[key] === "object") {
                const result = replaceNumericVariables(stringOrObject[key], values);
                /* c8 ignore next 3 - defensive guard: recursive calls on objects can't return null currently */
                if (result === null) {
                    delete stringOrObject[key];
                }
                else {
                    stringOrObject[key] = result;
                }
            }
            else if (typeof stringOrObject[key] === "string") {
                const matches = stringOrObject[key].match(/\$[0-9]+/g);
                if (matches) {
                    const allExist = matches.every((variable) => {
                        const index = variable.substring(1);
                        return Object.hasOwn(values, index) && typeof values[index] !== "undefined";
                    });
                    if (!allExist) {
                        delete stringOrObject[key];
                    }
                    else {
                        stringOrObject[key] = stringOrObject[key].replace(/\$[0-9]+/g, (variable) => {
                            const index = variable.substring(1);
                            return values[index];
                        });
                    }
                }
            }
        });
    }
    return stringOrObject;
}
/**
 * Parses raw test content into an array of structured test objects.
 * This is a browser-compatible function that works with strings and doesn't require file system access.
 *
 * @param options - Options for parsing
 * @param options.config - Test configuration object
 * @param options.content - Raw file content as a string
 * @param options.filePath - Path to the file being parsed (for metadata, not file I/O)
 * @param options.fileType - File type definition containing parsing rules
 * @returns Array of parsed and validated test objects
 */
export async function parseContent({ config = {}, content, filePath = "", fileType, testIdBase, }) {
    const statements = [];
    const statementTypes = ["testStart", "testEnd", "ignoreStart", "ignoreEnd", "step"];
    function findTest({ tests, testId }) {
        let test = tests.find((t) => t.testId === testId);
        if (!test) {
            test = { testId, steps: [] };
            tests.push(test);
        }
        return test;
    }
    // Determine file type based on provided fileType, file extension, or content detection
    const ext = (filePath?.split('.').pop() || "").toLowerCase();
    fileType = fileType
        || Object.values(defaultFileTypes).find(ft => ft.extensions.includes(ext))
        || detectFileTypeFromContent(content);
    // Precompute line starts for efficient line number lookups
    const lineStarts = getLineStarts(content);
    // Test for each statement type
    statementTypes.forEach((statementType) => {
        if (typeof fileType.inlineStatements === "undefined" ||
            typeof fileType.inlineStatements[statementType] === "undefined")
            return;
        fileType.inlineStatements[statementType].forEach((statementRegex) => {
            const regex = safeRegExp(statementRegex, "g");
            if (!regex)
                return;
            const matches = [...content.matchAll(regex)];
            matches.forEach((match) => {
                match.type = statementType;
                match.sortIndex = match[1] ? match.index + match[1].length : match.index;
                match._startIndex = match.index;
                match._endIndex = match.index + match[0].length;
                match._line = getLineNumber(content, match.index, lineStarts);
            });
            statements.push(...matches);
        });
    });
    if ((config.detectSteps ?? true) && fileType.markup) {
        fileType.markup.forEach((markup) => {
            markup.regex.forEach((pattern) => {
                const regex = safeRegExp(pattern, "g");
                if (!regex)
                    return;
                const matches = [...content.matchAll(regex)];
                if (matches.length > 0 && markup.batchMatches) {
                    const startIdx = Math.min(...matches.map((m) => m.index));
                    const endIdx = Math.max(...matches.map((m) => m.index + m[0].length));
                    const combinedMatch = {
                        1: matches.map((match) => match[1] || match[0]).join("\n"),
                        type: "detectedStep",
                        markup: markup,
                        sortIndex: startIdx,
                        _startIndex: startIdx,
                        _endIndex: endIdx,
                        _line: getLineNumber(content, startIdx, lineStarts),
                    };
                    statements.push(combinedMatch);
                }
                else if (matches.length > 0) {
                    matches.forEach((match) => {
                        match.type = "detectedStep";
                        match.markup = markup;
                        match.sortIndex = match[1] ? match.index + match[1].length : match.index;
                        match._startIndex = match.index;
                        match._endIndex = match.index + match[0].length;
                        match._line = getLineNumber(content, match.index, lineStarts);
                    });
                    statements.push(...matches);
                }
            });
        });
    }
    // Sort statements by index
    statements.sort((a, b) => a.sortIndex - b.sortIndex);
    // Process statements into tests and steps. Tests without an explicit
    // testId are grouped under an ephemeral pending ID during processing;
    // once all steps are collected, the pending IDs are replaced with
    // content-derived ones (see below) so the emitted IDs are stable across
    // runs and never positional.
    let tests = [];
    const pendingTestIds = new Set();
    let pendingCounter = 0;
    // Salt the ephemeral pending-ID prefix with a hash of the file content so a
    // user can't author a `testId` that collides with one — the replacement
    // pass below only overwrites IDs in `pendingTestIds`, and an authored value
    // would have to match this content-derived salt exactly to land there.
    const pendingSalt = contentHash(content);
    const nextPendingTestId = () => {
        const id = `__dd-pending-${pendingSalt}-${++pendingCounter}`;
        pendingTestIds.add(id);
        return id;
    };
    let testId = nextPendingTestId();
    let ignore = false;
    statements.forEach((statement) => {
        let test;
        let statementContent = "";
        let stepsCleanup = false;
        switch (statement.type) {
            case "testStart": {
                statementContent = statement[1] || statement[0];
                const parsedTest = parseObject({ stringifiedObject: statementContent });
                if (!parsedTest || typeof parsedTest !== 'object')
                    break;
                test = parsedTest;
                // If v2 schema, convert to v3
                if (test.id || test.file || test.setup || test.cleanup) {
                    if (!test.steps) {
                        test.steps = [{ action: "goTo", url: "https://doc-detective.com" }];
                        stepsCleanup = true;
                    }
                    const transformed = transformToSchemaKey({
                        object: test,
                        currentSchema: "test_v2",
                        targetSchema: "test_v3",
                    });
                    test = transformed;
                    if (stepsCleanup && test) {
                        test.steps = [];
                    }
                }
                if (test.testId) {
                    testId = test.testId;
                }
                else {
                    test.testId = testId;
                }
                if (test.detectSteps === "false") {
                    test.detectSteps = false;
                }
                else if (test.detectSteps === "true") {
                    test.detectSteps = true;
                }
                if (!test.steps) {
                    test.steps = [];
                }
                tests.push(test);
                break;
            }
            case "testEnd":
                testId = nextPendingTestId();
                ignore = false;
                break;
            case "ignoreStart":
                ignore = true;
                break;
            case "ignoreEnd":
                ignore = false;
                break;
            case "detectedStep":
                if (ignore)
                    break;
                test = findTest({ tests, testId });
                if (typeof test.detectSteps !== "undefined" && !test.detectSteps) {
                    break;
                }
                if (statement?.markup?.actions) {
                    statement.markup.actions.forEach((action) => {
                        let step = {};
                        if (typeof action === "string") {
                            if (action === "runCode")
                                return;
                            step[action] = statement[1] || statement[0];
                            if (config.origin && (action === "goTo" || action === "checkLink")) {
                                step[action] = { url: step[action], origin: config.origin };
                            }
                            // Attach sourceIntegration for Heretto
                            if (action === "screenshot" && config._herettoPathMapping) {
                                const herettoIntegration = findHerettoIntegration(config, filePath);
                                if (herettoIntegration) {
                                    const screenshotPath = step[action];
                                    step[action] = {
                                        path: screenshotPath,
                                        sourceIntegration: {
                                            type: "heretto",
                                            integrationName: herettoIntegration,
                                            filePath: screenshotPath,
                                            contentPath: filePath,
                                        },
                                    };
                                }
                            }
                        }
                        else {
                            const replacedStep = replaceNumericVariables(action, statement);
                            /* c8 ignore next - typeof string check is defensive; object actions always return objects */
                            if (!replacedStep || typeof replacedStep === 'string')
                                return;
                            step = replacedStep;
                            // Attach sourceIntegration for Heretto
                            if (step.screenshot && config._herettoPathMapping) {
                                const herettoIntegration = findHerettoIntegration(config, filePath);
                                if (herettoIntegration) {
                                    attachHerettoScreenshotSourceIntegration(step, herettoIntegration, filePath);
                                }
                            }
                        }
                        // Normalize step field formats
                        if (step.httpRequest?.request) {
                            if (typeof step.httpRequest.request.headers === "string") {
                                try {
                                    const headers = {};
                                    step.httpRequest.request.headers.split("\n").forEach((header) => {
                                        const colonIndex = header.indexOf(":");
                                        if (colonIndex === -1)
                                            return;
                                        const key = header.substring(0, colonIndex).trim();
                                        const value = header.substring(colonIndex + 1).trim();
                                        /* c8 ignore next 3 - V8 phantom branch in && short-circuit */
                                        if (key && value) {
                                            headers[key] = value;
                                        }
                                    });
                                    step.httpRequest.request.headers = headers;
                                    /* c8 ignore next 2 - string split/forEach can't throw */
                                }
                                catch (error) {
                                }
                            }
                            if (typeof step.httpRequest.request.body === "string" &&
                                (step.httpRequest.request.body.trim().startsWith("{") ||
                                    step.httpRequest.request.body.trim().startsWith("["))) {
                                try {
                                    step.httpRequest.request.body = JSON.parse(step.httpRequest.request.body);
                                }
                                catch (error) {
                                    // Ignore parsing errors
                                }
                            }
                        }
                        // Attach source location
                        if (typeof statement._startIndex === 'number') {
                            step.location = {
                                line: statement._line,
                                startIndex: statement._startIndex,
                                endIndex: statement._endIndex,
                            };
                        }
                        // Validate step
                        const valid = validate({
                            schemaKey: "step_v3",
                            object: step,
                            addDefaults: false,
                        });
                        if (!valid.valid) {
                            log(config, "warn", `Step ${JSON.stringify(step)} isn't a valid step. Skipping.`);
                            return;
                        }
                        step = valid.object;
                        test.steps.push(step);
                    });
                }
                break;
            case "step": {
                if (ignore)
                    break;
                test = findTest({ tests, testId });
                statementContent = statement[1] || statement[0];
                const parsedStep = parseObject({ stringifiedObject: statementContent });
                if (!parsedStep || typeof parsedStep !== 'object')
                    break;
                let step = parsedStep;
                // Attach source location
                if (typeof statement._startIndex === 'number') {
                    step.location = {
                        line: statement._line,
                        startIndex: statement._startIndex,
                        endIndex: statement._endIndex,
                    };
                }
                // Attach sourceIntegration for Heretto on screenshot steps
                if (step.screenshot && config._herettoPathMapping) {
                    const herettoIntegration = findHerettoIntegration(config, filePath);
                    if (herettoIntegration) {
                        attachHerettoScreenshotSourceIntegration(step, herettoIntegration, filePath);
                    }
                }
                const validation = validate({
                    schemaKey: "step_v3",
                    object: step,
                    addDefaults: false,
                });
                /* c8 ignore start - V8 phantom branch on if-else/switch-case */
                if (!validation.valid) {
                    log(config, "warn", `Step ${JSON.stringify(step)} isn't a valid step. Skipping.`);
                    return;
                }
                step = validation.object;
                test.steps.push(step);
                break;
                /* c8 ignore stop */
            }
            /* c8 ignore next 2 - all statement types are handled above */
            default:
                break;
        }
    });
    // Replace ephemeral pending IDs with content-derived ones:
    // `<base>~<hash>` where base is the caller-supplied testIdBase (core
    // passes the spec's path-derived ID) or the normalized file path, and the
    // hash covers the test's authored definition. Identical tests in one file
    // get an ordinal suffix so IDs stay unique. Explicit testIds are untouched.
    const usedTestIds = new Set(tests
        .filter((test) => test.testId && !pendingTestIds.has(test.testId))
        .map((test) => test.testId));
    const idBase = testIdBase || normalizePathForId(filePath) || "detected";
    tests.forEach((test) => {
        if (!test.testId || !pendingTestIds.has(test.testId))
            return;
        const baseId = `${idBase}~${contentHash(test)}`;
        let id = baseId;
        let suffix = 2;
        while (usedTestIds.has(id)) {
            id = `${baseId}-${suffix++}`;
        }
        usedTestIds.add(id);
        test.testId = id;
    });
    // Set contentPath on tests when filePath is provided
    if (filePath) {
        tests.forEach((test) => {
            test.contentPath = filePath;
        });
    }
    // Validate test objects
    const validatedTests = [];
    tests.forEach((test) => {
        const validation = validate({
            schemaKey: "test_v3",
            object: test,
            addDefaults: false,
        });
        if (!validation.valid) {
            log(config, "warn", `Couldn't convert test in ${filePath} to valid test. Skipping.`);
            return;
        }
        validatedTests.push(validation.object);
    });
    return validatedTests;
}
/**
 * Attaches a Heretto sourceIntegration descriptor to a screenshot step in place.
 * Normalizes non-object screenshot values (string paths, booleans, other primitives)
 * to an object shape before assigning sourceIntegration so downstream validation
 * has a predictable structure to reject or accept.
 */
function attachHerettoScreenshotSourceIntegration(step, integrationName, contentPath) {
    if (typeof step.screenshot === "string") {
        step.screenshot = { path: step.screenshot };
    }
    else if (step.screenshot === null ||
        typeof step.screenshot !== "object" ||
        Array.isArray(step.screenshot)) {
        step.screenshot = {};
    }
    step.screenshot.sourceIntegration = {
        type: "heretto",
        integrationName,
        filePath: step.screenshot.path || "",
        contentPath,
    };
}
/**
 * Helper function to find which Heretto integration a file belongs to.
 */
function findHerettoIntegration(config, filePath) {
    /* c8 ignore next - callers always check _herettoPathMapping before calling */
    if (!config._herettoPathMapping)
        return null;
    // Simple string matching since we don't have path.resolve in browser
    const normalizedFilePath = filePath.replace(/\\/g, "/");
    for (const [outputPath, integrationName] of Object.entries(config._herettoPathMapping)) {
        const normalizedOutputPath = outputPath.replace(/\\/g, "/");
        if (normalizedFilePath.startsWith(normalizedOutputPath)) {
            return integrationName;
        }
    }
    return null;
}
/**
 * Simple browser-compatible logging function.
 */
export function log(config, level, message) {
    const logLevels = ["silent", "error", "warn", "info", "debug"];
    // Normalize 'warning' to 'warn' for both config and message levels
    const configLevel = (config.logLevel || "info") === "warning" ? "warn" : (config.logLevel || "info");
    const normalizedLevel = level === "warning" ? "warn" : level;
    const configLevelIndex = logLevels.indexOf(configLevel);
    const messageLevelIndex = logLevels.indexOf(normalizedLevel);
    if (configLevelIndex < 0 || messageLevelIndex < 0)
        return;
    if (messageLevelIndex > configLevelIndex)
        return;
    // Treat message-level 'silent' as a no-op to avoid calling an undefined console method
    if (normalizedLevel === "silent")
        return;
    if (typeof message === "object") {
        console[normalizedLevel](JSON.stringify(message, null, 2));
    }
    else {
        console[normalizedLevel](message);
    }
}
//# sourceMappingURL=detectTests.js.map