// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import { assert } from 'chai';
import sinon from 'sinon';
import { createTarget } from '../../testing/EnvironmentHelpers.js';
import { describeWithMockConnection } from '../../testing/MockConnection.js';
import { setupRuntimeHooks } from '../../testing/RuntimeHelpers.js';
import { setupSettingsHooks } from '../../testing/SettingsHelpers.js';
import { setupPageResourceLoaderForSourceMap } from '../../testing/SourceMapHelpers.js';
import * as Platform from '../platform/platform.js';
import * as SDK from './sdk.js';
const { urlString } = Platform.DevToolsPath;
const content = JSON.stringify({
    version: 3,
    file: '/script.js',
    mappings: '',
    sources: [
        '/original-script.js',
    ],
});
describeWithMockConnection('SourceMapManager', () => {
    it('uses url for a worker\'s source maps from frame', async () => {
        setupPageResourceLoaderForSourceMap(content);
        const frameUrl = urlString `https://frame-host/index.html`;
        const scriptUrl = urlString `https://script-host/script.js`;
        const sourceUrl = urlString `script.js`;
        const sourceMapUrl = urlString `script.js.map`;
        const mainTarget = createTarget({ id: 'main', name: 'main', type: SDK.Target.Type.FRAME });
        mainTarget.setInspectedURL(frameUrl);
        const workerTarget = createTarget({
            id: 'worker',
            name: 'worker',
            type: SDK.Target.Type.Worker,
            parentTarget: mainTarget,
        });
        const debuggerModel = workerTarget.model(SDK.DebuggerModel.DebuggerModel);
        assert.isNotNull(debuggerModel);
        const sourceMapManager = debuggerModel.sourceMapManager();
        const script = new SDK.Script.Script(debuggerModel, '1', scriptUrl, 0, 0, 0, 0, 0, '', false, false, sourceMapUrl, false, 0, null, null, null, null, null, null, null);
        sourceMapManager.attachSourceMap(script, sourceUrl, sourceMapUrl);
        const sourceMap = await sourceMapManager.sourceMapForClientPromise(script);
        // Check that the URLs are resolved relative to the frame.
        assert.strictEqual(sourceMap?.url(), urlString `https://frame-host/script.js.map`);
        assert.deepEqual(sourceMap?.sourceURLs(), [urlString `https://frame-host/original-script.js`]);
    });
    it('can handle source maps in a data URL frame', async () => {
        setupPageResourceLoaderForSourceMap(content);
        const sourceUrl = urlString `script.js`;
        const sourceMapUrl = urlString `${`data:test/html;base64,${btoa(content)}`}`;
        const frameSource = '<script>0\n//# sourceURL=' + sourceUrl + '\n//# sourceMappingURL=' + sourceMapUrl + '</script>';
        const frameUrl = urlString `${`data:test/html;base64,${btoa(frameSource)}`}`;
        const scriptUrl = urlString `https://script-host/script.js`;
        const mainTarget = createTarget({ id: 'main', name: 'main', type: SDK.Target.Type.FRAME });
        mainTarget.setInspectedURL(frameUrl);
        const debuggerModel = mainTarget.model(SDK.DebuggerModel.DebuggerModel);
        assert.isNotNull(debuggerModel);
        const sourceMapManager = debuggerModel.sourceMapManager();
        const script = new SDK.Script.Script(debuggerModel, '1', scriptUrl, 0, 0, 0, 0, 0, '', false, false, sourceMapUrl, false, 0, null, null, null, null, null, null, null);
        sourceMapManager.attachSourceMap(script, sourceUrl, sourceMapUrl);
        const sourceMap = await sourceMapManager.sourceMapForClientPromise(script);
        assert.deepEqual(sourceMap?.sourceURLs(), [urlString `/original-script.js`]);
    });
});
describe('SourceMapManager', () => {
    const sourceURL = urlString `http://localhost/foo.js`;
    const sourceMappingURL = `${sourceURL}.map`;
    setupRuntimeHooks();
    setupSettingsHooks();
    beforeEach(() => {
        SDK.TargetManager.TargetManager.instance({ forceNew: true });
        SDK.PageResourceLoader.PageResourceLoader.instance({ forceNew: true, loadOverride: null, maxConcurrentLoads: 1 });
    });
    afterEach(() => {
        SDK.PageResourceLoader.PageResourceLoader.removeInstance();
        SDK.TargetManager.TargetManager.removeInstance();
    });
    class MockClient {
        target;
        debugIdInternal;
        initiatorUrl;
        constructor(target, debugIdInternal = null, initiatorUrl = null) {
            this.target = target;
            this.debugIdInternal = debugIdInternal;
            this.initiatorUrl = initiatorUrl;
        }
        createPageResourceLoadInitiator() {
            return { target: this.target, frameId: null, initiatorUrl: this.initiatorUrl };
        }
        debugId() {
            return this.debugIdInternal;
        }
    }
    describe('attachSourceMap', () => {
        it('catches attempts to attach twice for the same client', async () => {
            const target = createTarget();
            const sourceMapManager = new SDK.SourceMapManager.SourceMapManager(target);
            const client = new MockClient(target);
            sinon.stub(SDK.PageResourceLoader.PageResourceLoader.instance(), 'loadResource').resolves({ content });
            sourceMapManager.attachSourceMap(client, sourceURL, sourceMappingURL);
            assert.throws(() => sourceMapManager.attachSourceMap(client, sourceURL, sourceMappingURL));
            await sourceMapManager.sourceMapForClientPromise(client);
        });
        it('triggers the correct lifecycle events when loading succeeds', async () => {
            const target = createTarget();
            const sourceMapManager = new SDK.SourceMapManager.SourceMapManager(target);
            const client = new MockClient(target);
            const sourceMapWillAttach = sinon.spy();
            sourceMapManager.addEventListener(SDK.SourceMapManager.Events.SourceMapWillAttach, sourceMapWillAttach);
            const sourceMapAttached = sinon.spy();
            sourceMapManager.addEventListener(SDK.SourceMapManager.Events.SourceMapAttached, sourceMapAttached);
            sinon.stub(SDK.PageResourceLoader.PageResourceLoader.instance(), 'loadResource').resolves({ content });
            sourceMapManager.attachSourceMap(client, sourceURL, sourceMappingURL);
            assert.strictEqual(sourceMapWillAttach.callCount, 1, 'SourceMapWillAttach events');
            sinon.assert.calledWith(sourceMapWillAttach, sinon.match.hasNested('data.client', client));
            const sourceMap = await sourceMapManager.sourceMapForClientPromise(client);
            assert.strictEqual(sourceMapAttached.callCount, 1, 'SourceMapAttached events');
            sinon.assert.calledWith(sourceMapAttached, sinon.match.hasNested('data.client', client));
            sinon.assert.calledWith(sourceMapAttached, sinon.match.hasNested('data.sourceMap', sourceMap));
            assert.isTrue(sourceMapAttached.calledAfter(sourceMapWillAttach));
        });
        it('triggers the correct lifecycle events when loading fails', async () => {
            const target = createTarget();
            const sourceMapManager = new SDK.SourceMapManager.SourceMapManager(target);
            const client = new MockClient(target);
            const sourceMapWillAttach = sinon.spy();
            sourceMapManager.addEventListener(SDK.SourceMapManager.Events.SourceMapWillAttach, sourceMapWillAttach);
            const sourceMapFailedToAttach = sinon.spy();
            sourceMapManager.addEventListener(SDK.SourceMapManager.Events.SourceMapFailedToAttach, sourceMapFailedToAttach);
            sinon.stub(SDK.PageResourceLoader.PageResourceLoader.instance(), 'loadResource').rejects('Error');
            sourceMapManager.attachSourceMap(client, sourceURL, sourceMappingURL);
            assert.strictEqual(sourceMapWillAttach.callCount, 1, 'SourceMapWillAttach events');
            sinon.assert.calledWith(sourceMapWillAttach, sinon.match.hasNested('data.client', client));
            await sourceMapManager.sourceMapForClientPromise(client);
            assert.strictEqual(sourceMapFailedToAttach.callCount, 1, 'SourceMapFailedToAttach events');
            sinon.assert.calledWith(sourceMapFailedToAttach, sinon.match.hasNested('data.client', client));
            assert.isTrue(sourceMapFailedToAttach.calledAfter(sourceMapWillAttach));
        });
        it('correctly handles the case where sourcemap reattaches immediately', async () => {
            const target = createTarget();
            const sourceMapManager = new SDK.SourceMapManager.SourceMapManager(target);
            const client = new MockClient(target);
            const sourceMapAttached = sinon.spy();
            sourceMapManager.addEventListener(SDK.SourceMapManager.Events.SourceMapAttached, sourceMapAttached);
            const sourceMapFailedToAttach = sinon.spy();
            sourceMapManager.addEventListener(SDK.SourceMapManager.Events.SourceMapFailedToAttach, sourceMapFailedToAttach);
            sinon.stub(SDK.PageResourceLoader.PageResourceLoader.instance(), 'loadResource').resolves({ content });
            sourceMapManager.attachSourceMap(client, sourceURL, sourceMappingURL);
            sourceMapManager.detachSourceMap(client);
            sinon.assert.calledWith(sourceMapFailedToAttach, sinon.match.hasNested('data.client', client));
            sourceMapManager.attachSourceMap(client, sourceURL, sourceMappingURL);
            await sourceMapManager.sourceMapForClientPromise(client);
            assert.strictEqual(sourceMapAttached.callCount, 1, 'SourceMapAttached events');
            sinon.assert.calledWith(sourceMapAttached, sinon.match.hasNested('data.client', client));
            assert.isTrue(sourceMapAttached.calledAfter(sourceMapFailedToAttach));
        });
        it('correctly handles separate clients with same sourceURL and sourceMappingURL', async () => {
            const target = createTarget();
            const sourceMapManager = new SDK.SourceMapManager.SourceMapManager(target);
            const client1 = new MockClient(target);
            const client2 = new MockClient(target);
            sinon.stub(SDK.PageResourceLoader.PageResourceLoader.instance(), 'loadResource').resolves({ content });
            sourceMapManager.attachSourceMap(client1, sourceURL, sourceMappingURL);
            sourceMapManager.attachSourceMap(client2, sourceURL, sourceMappingURL);
            const [sourceMap1, sourceMap2] = await Promise.all([
                sourceMapManager.sourceMapForClientPromise(client1),
                sourceMapManager.sourceMapForClientPromise(client2),
            ]);
            assert.notStrictEqual(sourceMap1, sourceMap2);
        });
        it('defers loading sourcemaps while disabled', async () => {
            const target = createTarget();
            const sourceMapManager = new SDK.SourceMapManager.SourceMapManager(target);
            sourceMapManager.setEnabled(false);
            const client = new MockClient(target);
            const loadResource = sinon.stub(SDK.PageResourceLoader.PageResourceLoader.instance(), 'loadResource');
            sourceMapManager.attachSourceMap(client, sourceURL, sourceMappingURL);
            assert.strictEqual(loadResource.callCount, 0, 'loadResource calls');
            assert.isUndefined(sourceMapManager.sourceMapForClient(client));
            assert.isUndefined(await sourceMapManager.sourceMapForClientPromise(client));
            sourceMapManager.setEnabled(true);
            assert.strictEqual(loadResource.callCount, 1, 'loadResource calls');
            await sourceMapManager.sourceMapForClientPromise(client);
        });
        it('does not attempt to load when attach is cancelled', async () => {
            const target = createTarget();
            const sourceMapManager = new SDK.SourceMapManager.SourceMapManager(target);
            const client = new MockClient(target);
            sourceMapManager.addEventListener(SDK.SourceMapManager.Events.SourceMapWillAttach, ({ data: { client } }) => sourceMapManager.cancelAttachSourceMap(client));
            const sourceMapFailedToAttach = sinon.spy();
            sourceMapManager.addEventListener(SDK.SourceMapManager.Events.SourceMapFailedToAttach, sourceMapFailedToAttach);
            const loadResource = sinon.spy(SDK.PageResourceLoader.PageResourceLoader.instance(), 'loadResource');
            sourceMapManager.attachSourceMap(client, sourceURL, sourceMappingURL);
            assert.strictEqual(loadResource.callCount, 0, 'loadResource calls');
            await sourceMapManager.sourceMapForClientPromise(client);
            assert.strictEqual(sourceMapFailedToAttach.callCount, 1, 'SourceMapFailedToAttach events');
            sinon.assert.calledWith(sourceMapFailedToAttach, sinon.match.hasNested('data.client', client));
        });
    });
    describe('detachSourceMap', () => {
        it('silently ignores unknown clients', () => {
            const target = createTarget();
            const sourceMapManager = new SDK.SourceMapManager.SourceMapManager(target);
            const client = new MockClient(target);
            sourceMapManager.detachSourceMap(client);
        });
        it('triggers the correct lifecycle events', async () => {
            const target = createTarget();
            const sourceMapManager = new SDK.SourceMapManager.SourceMapManager(target);
            const client = new MockClient(target);
            const sourceMapDetached = sinon.spy();
            sourceMapManager.addEventListener(SDK.SourceMapManager.Events.SourceMapDetached, sourceMapDetached);
            sinon.stub(SDK.PageResourceLoader.PageResourceLoader.instance(), 'loadResource').resolves({ content });
            sourceMapManager.attachSourceMap(client, sourceURL, sourceMappingURL);
            const sourceMap = await sourceMapManager.sourceMapForClientPromise(client);
            sourceMapManager.detachSourceMap(client);
            assert.strictEqual(sourceMapDetached.callCount, 1, 'SourceMapDetached events');
            sinon.assert.calledWith(sourceMapDetached, sinon.match.hasNested('data.client', client));
            sinon.assert.calledWith(sourceMapDetached, sinon.match.hasNested('data.sourceMap', sourceMap));
        });
        it('triggers the correct lifecycle events when disabled', async () => {
            const target = createTarget();
            const sourceMapManager = new SDK.SourceMapManager.SourceMapManager(target);
            const client = new MockClient(target);
            sourceMapManager.setEnabled(false);
            sourceMapManager.attachSourceMap(client, sourceURL, sourceMappingURL);
            const sourceMapFailedToAttach = sinon.spy();
            sourceMapManager.addEventListener(SDK.SourceMapManager.Events.SourceMapFailedToAttach, sourceMapFailedToAttach);
            const sourceMapDetached = sinon.spy();
            sourceMapManager.addEventListener(SDK.SourceMapManager.Events.SourceMapDetached, sourceMapDetached);
            sourceMapManager.detachSourceMap(client);
            assert.strictEqual(sourceMapFailedToAttach.callCount, 0, 'SourceMapFailedToAttach events');
            assert.strictEqual(sourceMapDetached.callCount, 0, 'SourceMapDetached events');
        });
    });
    describe('setEnabled', () => {
        it('triggers the correct lifecycle events when disabling while attaching', () => {
            const target = createTarget();
            const sourceMapManager = new SDK.SourceMapManager.SourceMapManager(target);
            const client = new MockClient(target);
            sinon.stub(SDK.PageResourceLoader.PageResourceLoader.instance(), 'loadResource').returns(new Promise(() => { }));
            sourceMapManager.attachSourceMap(client, sourceURL, sourceMappingURL);
            const sourceMapFailedToAttach = sinon.spy();
            sourceMapManager.addEventListener(SDK.SourceMapManager.Events.SourceMapFailedToAttach, sourceMapFailedToAttach);
            sourceMapManager.setEnabled(false);
            assert.strictEqual(sourceMapFailedToAttach.callCount, 1, 'SourceMapFailedToAttach events');
            sinon.assert.calledWith(sourceMapFailedToAttach, sinon.match.hasNested('data.client', client));
        });
        it('triggers the correct lifecycle events when disabling once attached', async () => {
            const target = createTarget();
            const sourceMapManager = new SDK.SourceMapManager.SourceMapManager(target);
            const client = new MockClient(target);
            sinon.stub(SDK.PageResourceLoader.PageResourceLoader.instance(), 'loadResource').resolves({ content });
            sourceMapManager.attachSourceMap(client, sourceURL, sourceMappingURL);
            const sourceMap = await sourceMapManager.sourceMapForClientPromise(client);
            const sourceMapDetached = sinon.spy();
            sourceMapManager.addEventListener(SDK.SourceMapManager.Events.SourceMapDetached, sourceMapDetached);
            sourceMapManager.setEnabled(false);
            assert.strictEqual(sourceMapDetached.callCount, 1, 'SourceMapDetached events');
            sinon.assert.calledWith(sourceMapDetached, sinon.match.hasNested('data.client', client));
            sinon.assert.calledWith(sourceMapDetached, sinon.match.hasNested('data.sourceMap', sourceMap));
        });
        it('triggers the correct lifecycle events when re-enabling', async () => {
            const target = createTarget();
            const sourceMapManager = new SDK.SourceMapManager.SourceMapManager(target);
            const client = new MockClient(target);
            sinon.stub(SDK.PageResourceLoader.PageResourceLoader.instance(), 'loadResource').resolves({ content });
            sourceMapManager.attachSourceMap(client, sourceURL, sourceMappingURL);
            await sourceMapManager.sourceMapForClientPromise(client);
            sourceMapManager.setEnabled(false);
            const sourceMapDetached = sinon.spy();
            sourceMapManager.addEventListener(SDK.SourceMapManager.Events.SourceMapDetached, sourceMapDetached);
            const sourceMapWillAttach = sinon.spy();
            sourceMapManager.addEventListener(SDK.SourceMapManager.Events.SourceMapWillAttach, sourceMapWillAttach);
            const sourceMapFailedToAttach = sinon.spy();
            sourceMapManager.addEventListener(SDK.SourceMapManager.Events.SourceMapFailedToAttach, sourceMapFailedToAttach);
            const sourceMapAttached = sinon.spy();
            sourceMapManager.addEventListener(SDK.SourceMapManager.Events.SourceMapAttached, sourceMapAttached);
            sourceMapManager.setEnabled(true);
            const sourceMap = await sourceMapManager.sourceMapForClientPromise(client);
            assert.strictEqual(sourceMapDetached.callCount, 0, 'SourceMapDetached events');
            assert.strictEqual(sourceMapFailedToAttach.callCount, 0, 'SourceMapFailedToAttach events');
            assert.strictEqual(sourceMapWillAttach.callCount, 1, 'SourceMapWillAttach events');
            sinon.assert.calledWith(sourceMapWillAttach, sinon.match.hasNested('data.client', client));
            assert.isTrue(sourceMapAttached.calledAfter(sourceMapWillAttach));
            assert.strictEqual(sourceMapAttached.callCount, 1, 'SourceMapAttached events');
            sinon.assert.calledWith(sourceMapAttached, sinon.match.hasNested('data.client', client));
            sinon.assert.calledWith(sourceMapAttached, sinon.match.hasNested('data.sourceMap', sourceMap));
        });
    });
    describe('SourceMapCache integration', () => {
        let sourceMapCache;
        beforeEach(async () => {
            sourceMapCache = SDK.SourceMapCache.SourceMapCache.instance();
            await sourceMapCache.disposeForTest();
        });
        afterEach(async () => {
            await sourceMapCache.disposeForTest();
        });
        it('uses cached source map when debugId matches and origin matches', async () => {
            const target = createTarget();
            const sourceMapManager = new SDK.SourceMapManager.SourceMapManager(target);
            const debugId = 'test-debug-id';
            const origin = urlString `https://example.com`;
            const client = new MockClient(target, debugId, origin);
            const cachedMap = {
                version: 3,
                sources: ['cached.ts'],
                mappings: '',
            };
            await sourceMapCache.set(debugId, origin, cachedMap);
            const loadResourceStub = sinon.stub(SDK.PageResourceLoader.PageResourceLoader.instance(), 'loadResource');
            sourceMapManager.attachSourceMap(client, sourceURL, sourceMappingURL);
            const sourceMap = await sourceMapManager.sourceMapForClientPromise(client);
            assert.isNotNull(sourceMap);
            assert.deepEqual(sourceMap?.sourceURLs(), [urlString `http://localhost/cached.ts`]);
            assert.strictEqual(loadResourceStub.callCount, 0, 'loadResource should not have been called');
        });
        it('does NOT use cached source map when debugId matches but origin does NOT match', async () => {
            const target = createTarget();
            const sourceMapManager = new SDK.SourceMapManager.SourceMapManager(target);
            const debugId = 'test-debug-id';
            const cachedOrigin = urlString `https://example.com`;
            const clientOrigin = urlString `https://malicious.com`;
            const client = new MockClient(target, debugId, clientOrigin);
            const cachedMap = {
                version: 3,
                sources: ['cached.ts'],
                mappings: '',
            };
            const networkMap = {
                version: 3,
                sources: ['network.ts'],
                mappings: '',
                debugId,
            };
            await sourceMapCache.set(debugId, cachedOrigin, cachedMap);
            const loadResourceStub = sinon.stub(SDK.PageResourceLoader.PageResourceLoader.instance(), 'loadResource').resolves({
                content: JSON.stringify(networkMap)
            });
            sourceMapManager.attachSourceMap(client, sourceURL, sourceMappingURL);
            const sourceMap = await sourceMapManager.sourceMapForClientPromise(client);
            assert.isNotNull(sourceMap);
            assert.deepEqual(sourceMap?.sourceURLs(), [urlString `http://localhost/network.ts`]);
            assert.strictEqual(loadResourceStub.callCount, 1, 'loadResource should have been called');
            const storedMap = await sourceMapCache.get(debugId, clientOrigin);
            assert.deepEqual(storedMap, networkMap);
        });
    });
});
//# sourceMappingURL=SourceMapManager.test.js.map