"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.cleanupTestLogging = exports.setupTestLogging = exports.logWithElapsed = exports.defaultStopwatch = exports.Stopwatch = exports.logWithStopwatch = exports.logWithTime = exports.withLogging = exports.getCurrentTestContext = exports.clearTestContext = exports.setTestContext = void 0;
/* eslint-disable @typescript-eslint/no-explicit-any */
// ---------------------------
// Config & runtime state
// ---------------------------
const SLOW_CALL_THRESHOLD_MS = Number.parseInt(process.env.LOG_SLOW_THRESHOLD_MS ?? '500', 10);
// Use AsyncLocalStorage to maintain test context per worker thread
// This ensures each parallel test worker has its own isolated context
let currentTestContext = null;
//
// Identify the current worker (Playwright sets PLAYWRIGHT_WORKER_INDEX; fall back to pid)
const WORKER_ID = process.env.PLAYWRIGHT_WORKER_INDEX ??
    process.env.JEST_WORKER_ID ??
    String(process.pid);
function setTestContext(testName, workerId) {
    currentTestContext = {
        testName,
        workerId: workerId ?? WORKER_ID,
    };
}
exports.setTestContext = setTestContext;
function clearTestContext() {
    currentTestContext = null;
}
exports.clearTestContext = clearTestContext;
function getCurrentTestContext() {
    return currentTestContext;
}
exports.getCurrentTestContext = getCurrentTestContext;
const slowCalls = [];
process.once('exit', () => {
    if (slowCalls.length === 0)
        return;
    const now = new Date().toISOString();
    console.log(`\n\x1b[35m───── Slowest Calls (≥ ${SLOW_CALL_THRESHOLD_MS} ms) ─────\x1b[0m`);
    const top = slowCalls.sort((a, b) => b.duration - a.duration).slice(0, 10);
    for (const { name, duration, callsite, testContext, callStack } of top) {
        // Do NOT wrap the path itself in colour codes – that breaks link detection in
        // most terminals/IDEs.  We still colour the leading arrow, but leave the path
        // as plain text so it stays clickable.
        const site = callsite ? `  @ ${callsite}` : '';
        const testInfo = testContext ? ` [${testContext.testName}]` : '';
        console.log(`[${now}] ${colorLap(duration)} ${name}${testInfo}${site}`);
        // Show call stack if available
        if (callStack && callStack.length > 0) {
            console.log(`    \x1b[90mCall stack:\x1b[0m`);
            callStack.forEach((frame, index) => {
                const indent = '    ';
                const arrow = index === callStack.length - 1 ? '└─ ' : '├─ ';
                console.log(`${indent}${arrow}${frame}`);
            });
        }
    }
    console.log('\x1b[35m───────────────────────────────────────────────────────\x1b[0m');
});
// Some Playwright classes rely on their exact identity for instanceof / type checks
// (e.g., Locator) which break when wrapped in a Proxy. Detect these objects and
// skip proxying them.
function shouldSkipProxying(value) {
    if (!value || typeof value !== 'object') {
        return true;
    }
    // Best-effort detection based on constructor name to avoid importing Playwright types.
    // Playwright 1.60+ exposes these classes as `_Locator` / `_FrameLocator` / `_ElementHandle`.
    const ctorName = value.constructor?.name;
    return (ctorName === 'Locator' ||
        ctorName === '_Locator' ||
        ctorName === 'FrameLocator' ||
        ctorName === '_FrameLocator' ||
        ctorName === 'ElementHandle' ||
        ctorName === '_ElementHandle');
}
// Cache to avoid wrapping the same object multiple times (which can lead to duplicate logs)
const proxyCache = new WeakMap();
// ---------------------------
// Patch Playwright prototypes that we cannot safely proxy (e.g., Locator)
// ---------------------------
// Track prototypes we have already patched so we only wrap once
const patchedPrototypes = new WeakSet();
function workerPrefix() {
    const now = new Date();
    const testContext = getCurrentTestContext();
    const testInfo = testContext ? `[${testContext.testName}]` : '';
    return `[${now.toISOString()}] \x1b[90m[W${WORKER_ID}]\x1b[0m${testInfo} `; // dim grey with test name
}
// Helper to draw tree-style indentation (│└) so depth is obvious even when groups
// are not rendered (e.g. CI terminals that ignore console.group)
function treeIndent(depth) {
    if (depth === 0)
        return '';
    // Use │ for intermediate levels and └ for the leaf
    return Array(depth).fill('│ ').join('').replace(/│ $/, '└ ');
}
function patchPrototypeForLogging(proto, className) {
    if (patchedPrototypes.has(proto))
        return;
    patchedPrototypes.add(proto);
    for (const key of Object.getOwnPropertyNames(proto)) {
        if (key === 'constructor')
            continue;
        const desc = Object.getOwnPropertyDescriptor(proto, key);
        if (!desc?.value || typeof desc.value !== 'function')
            continue;
        const originalFn = desc.value;
        if (originalFn.__LOG_WRAPPED__)
            continue; // already wrapped
        const wrapped = function (...args) {
            const callerInfo = getCallerInfo();
            const name = `${className}.${String(key)}`;
            // Prepare argument preview once so we can reuse it across logs
            const argsPreview = args.map((a) => stringify(a)).join(', ');
            // No tree-indent available here – these are leaf calls
            console.groupCollapsed(`${workerPrefix()}\x1b[36m➡️  ${name}(${argsPreview})\x1b[0m ${callerInfo}`);
            const start = Date.now();
            let result;
            try {
                result = originalFn.apply(this, args);
                if (result instanceof Promise) {
                    return result
                        .then((r) => {
                        logSuccess('', name, argsPreview, stringify(r), Date.now() - start);
                        return r;
                    })
                        .catch((e) => {
                        logError('', name, argsPreview, e, Date.now() - start);
                        throw e;
                    });
                }
                else {
                    logSuccess('', name, argsPreview, stringify(result), Date.now() - start);
                    return result;
                }
            }
            catch (err) {
                const duration = Date.now() - start;
                console.error(`${workerPrefix()}\x1b[31m❌ ${name} threw ${colorLap(duration)}\x1b[0m`);
                console.error(workerPrefix(), err);
                throw err;
            }
            finally {
                console.groupEnd();
            }
        };
        wrapped.__LOG_WRAPPED__ = true;
        Object.defineProperty(proto, key, { ...desc, value: wrapped });
    }
}
function createLoggedInstance(instance, label = '', depth = 0) {
    // If we've already created a proxy for this exact object, reuse it
    const cached = proxyCache.get(instance);
    if (cached) {
        return cached;
    }
    const proxy = new Proxy(instance, {
        get(target, prop, receiver) {
            const original = Reflect.get(target, prop, receiver);
            if (typeof original === 'object' && original !== null) {
                if (!shouldSkipProxying(original)) {
                    return createLoggedInstance(original, `${label}.${String(prop)}`, depth + 1);
                }
                else {
                    // For Locator / FrameLocator / ElementHandle – patch their prototype so
                    // that method calls are still logged even though we cannot proxy them.
                    const ctorName = original.constructor?.name ?? 'Object';
                    patchPrototypeForLogging(Object.getPrototypeOf(original), ctorName);
                    return original;
                }
            }
            if (typeof original === 'function') {
                // Helper to safely stringify and truncate large values for readability
                return async function (...args) {
                    const indent = treeIndent(depth);
                    const name = `${label}.${String(prop)}`.replace(/^\./, '');
                    const argsPreview = args.map((a) => stringify(a)).join(', ');
                    // Add file-line info for easier navigation (feature #10)
                    const callerInfo = getCallerInfo();
                    // Start group – light blue arrow (feature #2)
                    console.groupCollapsed(`${workerPrefix()}${indent}\x1b[36m➡️  ${name}(${argsPreview})\x1b[0m ${callerInfo}`);
                    const start = Date.now();
                    try {
                        // Use the actual target object as the `this` context so that
                        // methods relying on private fields (like Playwright internals)
                        // work correctly. Using the proxy here breaks access to these
                        // private members and can throw at runtime.
                        const result = await original.apply(target, args);
                        const duration = Date.now() - start;
                        const resultPreview = stringify(result);
                        // Success log – green check mark with coloured duration
                        logSuccess(indent, name, argsPreview, resultPreview, duration);
                        // Slow-call tracking (feature #4)
                        if (duration >= SLOW_CALL_THRESHOLD_MS) {
                            slowCalls.push({
                                name,
                                duration,
                                callsite: callerInfo,
                                testContext: getCurrentTestContext() || undefined,
                                callStack: getCallStack(),
                            });
                        }
                        return result;
                    }
                    catch (err) {
                        const duration = Date.now() - start;
                        // Error log – red cross with coloured duration
                        logError(indent, name, argsPreview, err, duration);
                        throw err;
                    }
                    finally {
                        // Close the group (feature #2)
                        console.groupEnd();
                    }
                };
            }
            return original;
        },
    });
    // Store in cache before returning so that recursive/nested look-ups also reuse it
    proxyCache.set(instance, proxy);
    return proxy;
}
const stringify = (value, maxLength = 240) => {
    try {
        const str = typeof value === 'string' ? value : JSON.stringify(value);
        return str.length > maxLength ? str.slice(0, maxLength) + '…' : str;
    }
    catch {
        return String(value);
    }
};
const formatError = (error, maxLength = 10000) => {
    if (error instanceof Error) {
        // Preserve full stack; limit length to avoid gigantic dumps
        const basic = `${error.name}: ${error.message}`;
        const stack = error.stack ? '\n' + error.stack : '';
        const combined = basic + stack;
        return combined.length > maxLength
            ? combined.slice(0, maxLength) + '…'
            : combined;
    }
    // Fallback to util.inspect to get colours & depth
    try {
        // Dynamically import util to avoid extra bundle weight when not needed
        // eslint-disable-next-line @typescript-eslint/no-var-requires
        const util = require('util');
        return util.inspect(error, { colors: true, depth: 5, maxArrayLength: 10 });
    }
    catch {
        return stringify(error);
    }
};
const logSuccess = (indent, name, argsPreview, resultPreview, duration) => {
    // Green check mark, show args in dim colour, result in default colour, lap coloured.
    console.log(`${workerPrefix()}${indent}\x1b[32m✅ ${name}(\x1b[90m${argsPreview}\x1b[32m) → ${resultPreview} ${colorLap(duration)}\x1b[0m`);
};
const logError = (indent, name, argsPreview, error, duration) => {
    // Red cross, show args in dim colour to differentiate from the error text.
    console.error(`${workerPrefix()}${indent}\x1b[31m❌ ${name}(\x1b[90m${argsPreview}\x1b[31m) threw ${colorLap(duration)}\x1b[0m`);
    console.error(`${workerPrefix()}${indent}${formatError(error)}`);
};
function withLogging(instance, label) {
    if (process.env.DISABLE_LOGGING_PROXY === 'true') {
        // When the environment variable is set, skip wrapping with Proxy to avoid the runtime overhead.
        return instance;
    }
    return createLoggedInstance(instance, label);
}
exports.withLogging = withLogging;
function colorElapsed(ms) {
    const seconds = ms / 1000;
    if (seconds < 45) {
        return `\x1b[32m+${ms}ms\x1b[0m`; // green
    }
    else if (seconds < 60) {
        return `\x1b[33m+${ms}ms\x1b[0m`; // yellow
    }
    else {
        return `\x1b[31m+${ms}ms\x1b[0m`; // red
    }
}
function colorLap(ms) {
    if (ms < 1000) {
        return `\x1b[32mlap +${ms}ms\x1b[0m`; // green
    }
    else if (ms < 2000) {
        return `\x1b[33mlap +${ms}ms\x1b[0m`; // yellow
    }
    else {
        return `\x1b[31mlap +${ms}ms\x1b[0m`; // red
    }
}
function getCallerInfo() {
    const err = new Error();
    if (!err.stack)
        return '';
    const stackLines = err.stack.split('\n');
    const workspaceRoot = process.cwd();
    // IntelliJ detects links when absolute paths are used. Return absolute by default.
    // If you really want relative paths, set LOG_CALLER_RELATIVE_PATH=true.
    const useRelative = process.env.LOG_CALLER_RELATIVE_PATH === 'true';
    // Skip the first few lines which are typically the Error constructor and logging functions
    let skipCount = 0;
    for (const line of stackLines) {
        // Skip Error constructor
        if (line.includes('Error') || line.includes('getCallerInfo')) {
            skipCount++;
            continue;
        }
        // Skip logging utility functions
        if (line.includes('loggingUtils') ||
            line.includes('logSuccess') ||
            line.includes('logError') ||
            line.includes('createLoggedInstance') ||
            line.includes('patchPrototypeForLogging') ||
            line.includes('withLogging')) {
            skipCount++;
            continue;
        }
        // If we've skipped enough lines and this line contains 'at', it's likely our caller
        if (skipCount >= 2 && line.includes('at')) {
            const match = line.match(/\(([^)]+)\)/) || line.match(/at ([^ ]+)/);
            if (match) {
                let filePath = match[1];
                if (useRelative && filePath.startsWith(workspaceRoot)) {
                    filePath = filePath.slice(workspaceRoot.length);
                    if (filePath.startsWith('/'))
                        filePath = filePath.slice(1);
                }
                return filePath;
            }
        }
    }
    return '';
}
function getCallStack() {
    const err = new Error();
    if (!err.stack)
        return [];
    const stackLines = err.stack.split('\n');
    const workspaceRoot = process.cwd();
    const useRelative = process.env.LOG_CALLER_RELATIVE_PATH === 'true';
    const callStack = [];
    for (const line of stackLines) {
        // Skip Error constructor and logging utility functions
        if (line.includes('Error') ||
            line.includes('getCallStack') ||
            line.includes('loggingUtils') ||
            line.includes('logSuccess') ||
            line.includes('logError') ||
            line.includes('createLoggedInstance') ||
            line.includes('patchPrototypeForLogging') ||
            line.includes('withLogging')) {
            continue;
        }
        // Extract file path from stack line
        if (line.includes('at')) {
            const match = line.match(/\(([^)]+)\)/) || line.match(/at ([^ ]+)/);
            if (match) {
                let filePath = match[1];
                if (useRelative && filePath.startsWith(workspaceRoot)) {
                    filePath = filePath.slice(workspaceRoot.length);
                    if (filePath.startsWith('/'))
                        filePath = filePath.slice(1);
                }
                callStack.push(filePath);
            }
        }
    }
    return callStack.slice(0, 10); // Limit to first 10 frames to avoid overwhelming output
}
const logWithTime = (msg, startTime) => {
    const now = new Date();
    const elapsed = Date.now() - startTime;
    const caller = getCallerInfo();
    // Keep the file path plain (no surrounding brackets) to preserve clickability
    console.log(`[${now.toISOString()}] (${colorElapsed(elapsed)}) ${msg}  ${caller}`);
};
exports.logWithTime = logWithTime;
const logWithStopwatch = (msg, stopwatch) => {
    const now = new Date();
    const elapsed = stopwatch.elapsed();
    const lap = stopwatch.lap();
    const caller = getCallerInfo();
    // Keep the file path plain (no surrounding brackets) to preserve clickability
    console.log(`[${now.toISOString()}] (${colorElapsed(elapsed)}, ${colorLap(lap)}) ${msg}  ${caller}`);
    stopwatch._updateLap();
};
exports.logWithStopwatch = logWithStopwatch;
// Stopwatch class for timing durations
class Stopwatch {
    _startTime = null;
    _elapsed = 0;
    _running = false;
    _lastLap = 0;
    start() {
        if (!this._running) {
            this._startTime = Date.now();
            this._running = true;
            this._lastLap = 0;
        }
    }
    stop() {
        if (this._running && this._startTime !== null) {
            this._elapsed += Date.now() - this._startTime;
            this._startTime = null;
            this._running = false;
        }
    }
    reset() {
        this._elapsed = 0;
        this._startTime = this._running ? Date.now() : null;
        this._lastLap = 0;
    }
    elapsed() {
        if (this._running && this._startTime !== null) {
            return this._elapsed + (Date.now() - this._startTime);
        }
        return this._elapsed;
    }
    lap() {
        const nowElapsed = this.elapsed();
        return nowElapsed - this._lastLap;
    }
    _updateLap() {
        this._lastLap = this.elapsed();
    }
    log(msg) {
        (0, exports.logWithStopwatch)(msg, this);
    }
}
exports.Stopwatch = Stopwatch;
// Default/global stopwatch for convenience
exports.defaultStopwatch = new Stopwatch();
const logWithElapsed = (msg) => {
    if (!exports.defaultStopwatch['_running']) {
        exports.defaultStopwatch.start();
    }
    (0, exports.logWithStopwatch)(msg, exports.defaultStopwatch);
};
exports.logWithElapsed = logWithElapsed;
// ---------------------------
// Playwright test integration helpers
// ---------------------------
/**
 * Call this in test.beforeEach to set up test context for logging
 * @param testInfo - The testInfo object from Playwright test
 */
function setupTestLogging(testInfo) {
    setTestContext(testInfo.title, testInfo.workerIndex?.toString());
}
exports.setupTestLogging = setupTestLogging;
/**
 * Call this in test.afterEach to clean up test context
 */
function cleanupTestLogging() {
    clearTestContext();
}
exports.cleanupTestLogging = cleanupTestLogging;
