/**
 * Copyright (c) Microsoft Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { getActionGroup, renderTitleForCall } from '../protocolFormatter';
const contextSymbol = Symbol('context');
const nextInContextSymbol = Symbol('nextInContext');
const prevByEndTimeSymbol = Symbol('prevByEndTime');
const nextByStartTimeSymbol = Symbol('nextByStartTime');
const eventsSymbol = Symbol('events');
export class TraceModel {
    startTime;
    endTime;
    browserName;
    channel;
    platform;
    playwrightVersion;
    wallTime;
    title;
    options;
    pages;
    actions;
    attachments;
    visibleAttachments;
    events;
    stdio;
    errors;
    errorDescriptors;
    hasSource;
    hasStepData;
    sdkLanguage;
    testIdAttributeName;
    sources;
    resources;
    actionCounters;
    traceUri;
    testTimeout;
    constructor(traceUri, contexts) {
        contexts.forEach(contextEntry => indexModel(contextEntry));
        const libraryContext = contexts.find(context => context.origin === 'library');
        this.traceUri = traceUri;
        this.browserName = libraryContext?.browserName || '';
        this.sdkLanguage = libraryContext?.sdkLanguage;
        this.channel = libraryContext?.channel;
        this.testIdAttributeName = libraryContext?.testIdAttributeName;
        this.platform = libraryContext?.platform || '';
        this.playwrightVersion = contexts.find(c => c.playwrightVersion)?.playwrightVersion;
        this.title = libraryContext?.title || '';
        this.options = libraryContext?.options || {};
        this.testTimeout = contexts.find(c => c.origin === 'testRunner')?.testTimeout;
        // Next call updates all timestamps for all events in library contexts, so it must be done first.
        this.actions = mergeActionsAndUpdateTiming(contexts);
        this.pages = [].concat(...contexts.map(c => c.pages));
        this.wallTime = contexts.map(c => c.wallTime).reduce((prev, cur) => Math.min(prev || Number.MAX_VALUE, cur), Number.MAX_VALUE);
        this.startTime = contexts.map(c => c.startTime).reduce((prev, cur) => Math.min(prev, cur), Number.MAX_VALUE);
        this.endTime = contexts.map(c => c.endTime).reduce((prev, cur) => Math.max(prev, cur), Number.MIN_VALUE);
        this.events = [].concat(...contexts.map(c => c.events));
        this.stdio = [].concat(...contexts.map(c => c.stdio));
        this.errors = [].concat(...contexts.map(c => c.errors));
        this.hasSource = contexts.some(c => c.hasSource);
        this.hasStepData = contexts.some(context => context.origin === 'testRunner');
        this.resources = [...contexts.map(c => c.resources)].flat().map(entry => ({ ...entry, id: `${entry.pageref}-${entry.startedDateTime}-${entry.request.url}` }));
        this.attachments = this.actions.flatMap(action => action.attachments?.map(attachment => ({ ...attachment, callId: action.callId, traceUri })) ?? []);
        this.visibleAttachments = this.attachments.filter(attachment => !attachment.name.startsWith('_'));
        this.events.sort((a1, a2) => a1.time - a2.time);
        this.resources.sort((a1, a2) => a1._monotonicTime - a2._monotonicTime);
        this.errorDescriptors = this.hasStepData ? this._errorDescriptorsFromTestRunner() : this._errorDescriptorsFromActions();
        this.sources = collectSources(this.actions, this.errorDescriptors);
        this.actionCounters = new Map();
        for (const action of this.actions) {
            action.group = action.group ?? getActionGroup({ type: action.class, method: action.method });
            if (action.group)
                this.actionCounters.set(action.group, 1 + (this.actionCounters.get(action.group) || 0));
        }
    }
    createRelativeUrl(path) {
        const url = new URL('http://localhost/' + path);
        url.searchParams.set('trace', this.traceUri);
        return url.toString().substring('http://localhost/'.length);
    }
    failedAction() {
        // This find innermost action for nested ones.
        return this.actions.findLast(a => a.error);
    }
    filteredActions(actionsFilter) {
        const filter = new Set(actionsFilter);
        return this.actions.filter(action => !action.group || filter.has(action.group));
    }
    renderActionTree(filter) {
        const actions = this.filteredActions(filter ?? []);
        const { rootItem } = buildActionTree(actions);
        const actionTree = [];
        const visit = (actionItem, indent) => {
            const title = renderTitleForCall({ ...actionItem.action, type: actionItem.action.class });
            actionTree.push(`${indent}${title || actionItem.id}`);
            for (const child of actionItem.children)
                visit(child, indent + '  ');
        };
        rootItem.children.forEach(a => visit(a, ''));
        return actionTree;
    }
    _errorDescriptorsFromActions() {
        const errors = [];
        for (const action of this.actions || []) {
            if (!action.error?.message)
                continue;
            errors.push({
                action,
                stack: action.stack,
                message: action.error.message,
            });
        }
        return errors;
    }
    _errorDescriptorsFromTestRunner() {
        return this.errors.filter(e => !!e.message).map((error, i) => ({
            stack: error.stack,
            message: error.message,
        }));
    }
}
function indexModel(context) {
    for (const page of context.pages)
        page[contextSymbol] = context;
    for (let i = 0; i < context.actions.length; ++i) {
        const action = context.actions[i];
        action[contextSymbol] = context;
    }
    let lastNonRouteAction = undefined;
    for (let i = context.actions.length - 1; i >= 0; i--) {
        const action = context.actions[i];
        action[nextInContextSymbol] = lastNonRouteAction;
        if (action.class !== 'Route')
            lastNonRouteAction = action;
    }
    for (const event of context.events)
        event[contextSymbol] = context;
    for (const resource of context.resources)
        resource[contextSymbol] = context;
}
function mergeActionsAndUpdateTiming(contexts) {
    const result = [];
    const actions = mergeActionsAndUpdateTimingSameTrace(contexts);
    result.push(...actions);
    result.sort((a1, a2) => {
        if (a2.parentId === a1.callId)
            return 1;
        if (a1.parentId === a2.callId)
            return -1;
        return a1.endTime - a2.endTime;
    });
    for (let i = 1; i < result.length; ++i)
        result[i][prevByEndTimeSymbol] = result[i - 1];
    result.sort((a1, a2) => {
        if (a2.parentId === a1.callId)
            return -1;
        if (a1.parentId === a2.callId)
            return 1;
        return a1.startTime - a2.startTime;
    });
    for (let i = 0; i + 1 < result.length; ++i)
        result[i][nextByStartTimeSymbol] = result[i + 1];
    return result;
}
let lastTmpStepId = 0;
function mergeActionsAndUpdateTimingSameTrace(contexts) {
    const map = new Map();
    const libraryContexts = contexts.filter(context => context.origin === 'library');
    const testRunnerContexts = contexts.filter(context => context.origin === 'testRunner');
    // With library-only or test-runner-only traces there is nothing to match.
    if (!testRunnerContexts.length || !libraryContexts.length) {
        return contexts.map(context => {
            return context.actions.map(action => ({ ...action, context }));
        }).flat();
    }
    for (const context of libraryContexts) {
        for (const action of context.actions) {
            // Never merge stepless events.
            map.set(action.stepId || `tmp-step@${++lastTmpStepId}`, { ...action, context });
        }
    }
    // Protocol call aka library contexts have startTime/endTime as server-side times.
    // Step aka test runner contexts have startTime/endTime as client-side times.
    // Adjust startTime/endTime on the library contexts to align them with the test
    // runner steps.
    const delta = monotonicTimeDeltaBetweenLibraryAndRunner(testRunnerContexts, map);
    if (delta)
        adjustMonotonicTime(libraryContexts, delta);
    const nonPrimaryIdToPrimaryId = new Map();
    for (const context of testRunnerContexts) {
        for (const action of context.actions) {
            const existing = action.stepId && map.get(action.stepId);
            if (existing) {
                nonPrimaryIdToPrimaryId.set(action.callId, existing.callId);
                if (action.error)
                    existing.error = action.error;
                if (action.attachments)
                    existing.attachments = action.attachments;
                if (action.annotations)
                    existing.annotations = action.annotations;
                if (action.parentId)
                    existing.parentId = nonPrimaryIdToPrimaryId.get(action.parentId) ?? action.parentId;
                if (action.group)
                    existing.group = action.group;
                // For the events that are present in the test runner context, always take
                // their time from the test runner context to preserve client side order.
                existing.startTime = action.startTime;
                existing.endTime = action.endTime;
                continue;
            }
            if (action.parentId)
                action.parentId = nonPrimaryIdToPrimaryId.get(action.parentId) ?? action.parentId;
            map.set(action.stepId || `tmp-step@${++lastTmpStepId}`, { ...action, context });
        }
    }
    return [...map.values()];
}
function adjustMonotonicTime(contexts, monotonicTimeDelta) {
    for (const context of contexts) {
        context.startTime += monotonicTimeDelta;
        context.endTime += monotonicTimeDelta;
        for (const action of context.actions) {
            if (action.startTime)
                action.startTime += monotonicTimeDelta;
            if (action.endTime)
                action.endTime += monotonicTimeDelta;
        }
        for (const event of context.events)
            event.time += monotonicTimeDelta;
        for (const event of context.stdio)
            event.timestamp += monotonicTimeDelta;
        for (const page of context.pages) {
            for (const frame of page.screencastFrames)
                frame.timestamp += monotonicTimeDelta;
        }
        for (const resource of context.resources) {
            if (resource._monotonicTime)
                resource._monotonicTime += monotonicTimeDelta;
        }
    }
}
function monotonicTimeDeltaBetweenLibraryAndRunner(nonPrimaryContexts, libraryActions) {
    // We cannot rely on wall time or monotonic time to be the in sync
    // between library and test runner contexts. So we find first action
    // that is present in both runner and library contexts and use it
    // to calculate the time delta, assuming the two events happened at the
    // same instant.
    for (const context of nonPrimaryContexts) {
        for (const action of context.actions) {
            if (!action.startTime)
                continue;
            const libraryAction = action.stepId ? libraryActions.get(action.stepId) : undefined;
            if (libraryAction)
                return action.startTime - libraryAction.startTime;
        }
    }
    return 0;
}
export function buildActionTree(actions) {
    const itemMap = new Map();
    for (const action of actions) {
        itemMap.set(action.callId, {
            id: action.callId,
            parent: undefined,
            children: [],
            action,
        });
    }
    const rootItem = { action: { ...kFakeRootAction }, id: '', parent: undefined, children: [] };
    for (const item of itemMap.values()) {
        rootItem.action.startTime = Math.min(rootItem.action.startTime, item.action.startTime);
        rootItem.action.endTime = Math.max(rootItem.action.endTime, item.action.endTime);
        const parent = item.action.parentId ? itemMap.get(item.action.parentId) || rootItem : rootItem;
        parent.children.push(item);
        item.parent = parent;
    }
    const inheritStack = (item) => {
        for (const child of item.children) {
            child.action.stack = child.action.stack ?? item.action.stack;
            inheritStack(child);
        }
    };
    inheritStack(rootItem);
    return { rootItem, itemMap };
}
export function context(action) {
    return action[contextSymbol];
}
function nextInContext(action) {
    return action[nextInContextSymbol];
}
export function previousActionByEndTime(action) {
    return action[prevByEndTimeSymbol];
}
export function nextActionByStartTime(action) {
    return action[nextByStartTimeSymbol];
}
export function stats(action) {
    let errors = 0;
    let warnings = 0;
    for (const event of eventsForAction(action)) {
        if (event.type === 'console') {
            const type = event.messageType;
            if (type === 'warning')
                ++warnings;
            else if (type === 'error')
                ++errors;
        }
        if (event.type === 'event' && event.method === 'pageError')
            ++errors;
    }
    return { errors, warnings };
}
export function eventsForAction(action) {
    let result = action[eventsSymbol];
    if (result)
        return result;
    const nextAction = nextInContext(action);
    result = context(action).events.filter(event => {
        return event.time >= action.startTime && (!nextAction || event.time < nextAction.startTime);
    });
    action[eventsSymbol] = result;
    return result;
}
function collectSources(actions, errorDescriptors) {
    const result = new Map();
    for (const action of actions) {
        for (const frame of action.stack || []) {
            let source = result.get(frame.file);
            if (!source) {
                source = { errors: [], content: undefined };
                result.set(frame.file, source);
            }
        }
    }
    for (const error of errorDescriptors) {
        const { action, stack, message } = error;
        if (!action || !stack)
            continue;
        result.get(stack[0].file)?.errors.push({
            line: stack[0].line || 0,
            message
        });
    }
    return result;
}
const kFakeRootAction = {
    type: 'action',
    callId: '',
    startTime: 0,
    endTime: 0,
    class: '',
    method: '',
    params: {},
    log: [],
    context: {
        origin: 'library',
        startTime: 0,
        endTime: 0,
        browserName: '',
        wallTime: 0,
        options: {},
        pages: [],
        resources: [],
        actions: [],
        events: [],
        stdio: [],
        errors: [],
        hasSource: false,
        contextId: '',
    },
};
//# sourceMappingURL=traceModel.js.map