// Copyright 2020 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 * as Platform from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import { describeWithEnvironment } from '../../testing/EnvironmentHelpers.js';
import { MockCDPConnection } from '../../testing/MockCDPConnection.js';
import { createResource, getMainFrame, mockResourceTree } from '../../testing/ResourceTreeHelpers.js';
import { TestUniverse } from '../../testing/TestUniverse.js';
import * as TextUtils from '../text_utils/text_utils.js';
import * as Workspace from '../workspace/workspace.js';
import * as Bindings from './bindings.js';
const { urlString } = Platform.DevToolsPath;
describeWithEnvironment('ResourceMapping', () => {
    let debuggerModel;
    let resourceMapping;
    let uiSourceCode;
    let workspace;
    let target;
    let universe;
    let connection;
    // This test simulates the behavior of the ResourceMapping with the
    // following document, which contains two inline <script>s, one with
    // a `//# sourceURL` annotation and one without.
    //
    //  <!DOCTYPE html>
    //  <html>
    //  <head>
    //  <meta charset=utf-8>
    //  <script>
    //  function foo() { console.log("foo"); }
    //  foo();
    //  //# sourceURL=webpack:///src/foo.js
    //  </script>
    //  </head>
    //  <body>
    //  <script>console.log("bar");</script>
    //  </body>
    //  </html>
    //
    const url = urlString `http://example.com/index.html`;
    const SCRIPTS = [
        {
            scriptId: '1',
            startLine: 4,
            startColumn: 8,
            endLine: 8,
            endColumn: 0,
            sourceURL: urlString `webpack:///src/foo.js`,
            hasSourceURLComment: true,
            source: `\nfunction foo() { console.log("foo"); }
  foo();
  //# sourceURL=webpack:///src/foo.js`,
        },
        {
            scriptId: '2',
            startLine: 11,
            startColumn: 8,
            endLine: 11,
            endColumn: 27,
            sourceURL: url,
            hasSourceURLComment: false,
            source: 'console.log("bar");',
        },
    ];
    const OTHER_SCRIPT_ID = '3';
    beforeEach(async () => {
        universe = new TestUniverse();
        connection = new MockCDPConnection();
        mockResourceTree(connection);
        target = universe.createTarget({ connection });
        const targetManager = target.targetManager();
        targetManager.setScopeTarget(target);
        workspace = universe.workspace;
        resourceMapping = new Bindings.ResourceMapping.ResourceMapping(targetManager, workspace);
        // Inject the HTML document resource.
        createResource(getMainFrame(target), url, 'text/html', '');
        uiSourceCode = workspace.uiSourceCodeForURL(url);
        assert.isNotNull(uiSourceCode);
        // Register the inline <script>s.
        const hash = '';
        const length = 0;
        const embedderName = url;
        const executionContextId = 1;
        debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel);
        SCRIPTS.forEach(({ scriptId, startLine, startColumn, endLine, endColumn, sourceURL, hasSourceURLComment }) => {
            debuggerModel.parsedScriptSource(scriptId, sourceURL, startLine, startColumn, endLine, endColumn, executionContextId, hash, undefined, false, undefined, hasSourceURLComment, false, length, false, null, null, null, null, embedderName, null);
        });
        assert.lengthOf(debuggerModel.scripts(), SCRIPTS.length);
        connection.setSuccessHandler('Debugger.getScriptSource', param => {
            return {
                scriptSource: SCRIPTS.find(s => s.scriptId === param.scriptId)?.source ?? '',
            };
        });
    });
    it('creates UISourceCode for added target', () => {
        const resourceTreeModel = target.model(SDK.ResourceTreeModel.ResourceTreeModel);
        resourceMapping.modelRemoved(resourceTreeModel);
        assert.isNull(workspace.uiSourceCodeForURL(url));
        resourceMapping.modelAdded(resourceTreeModel);
        assert.isNotNull(workspace.uiSourceCodeForURL(url));
    });
    it('creates UISourceCode for added out of scope target', () => {
        target.targetManager().setScopeTarget(null);
        const otherUrl = urlString `http://example.com/other.html`;
        createResource(getMainFrame(target), otherUrl, 'text/html', '');
        uiSourceCode = workspace.uiSourceCodeForURL(otherUrl);
        assert.isNotNull(uiSourceCode);
    });
    describe('uiLocationToJSLocations', () => {
        it('does not map locations outside of <script> tags', () => {
            assert.isEmpty(resourceMapping.uiLocationToJSLocations(uiSourceCode, 0, 0));
            SCRIPTS.forEach(({ startLine, startColumn, endLine, endColumn }) => {
                assert.isEmpty(resourceMapping.uiLocationToJSLocations(uiSourceCode, startLine, startColumn - 1));
                assert.isEmpty(resourceMapping.uiLocationToJSLocations(uiSourceCode, endLine, endColumn));
            });
            assert.isEmpty(resourceMapping.uiLocationToJSLocations(uiSourceCode, 12, 1));
        });
        it('correctly maps inline <script> with a //# sourceURL annotation', () => {
            const { scriptId, startLine, startColumn, endLine, endColumn } = SCRIPTS[0];
            // Debugger locations in scripts with sourceURL annotations are relative to the beginning
            // of the script, rather than relative to the start of the surrounding document.
            assert.deepEqual(resourceMapping.uiLocationToJSLocations(uiSourceCode, startLine, startColumn), [
                debuggerModel.createRawLocationByScriptId(scriptId, 0, 0),
            ]);
            assert.deepEqual(resourceMapping.uiLocationToJSLocations(uiSourceCode, startLine, startColumn + 3), [
                // This location does not actually exist in the simulated document, but
                // the ResourceMapping doesn't know (and shouldn't care) about that.
                debuggerModel.createRawLocationByScriptId(scriptId, 0, 3),
            ]);
            assert.deepEqual(resourceMapping.uiLocationToJSLocations(uiSourceCode, startLine + 1, 5), [
                debuggerModel.createRawLocationByScriptId(scriptId, 1, 5),
            ]);
            assert.deepEqual(resourceMapping.uiLocationToJSLocations(uiSourceCode, endLine - 1, endColumn), [
                debuggerModel.createRawLocationByScriptId(scriptId, endLine - startLine - 1, endColumn),
            ]);
        });
        it('correctly maps inline <script> without //# sourceURL annotation', () => {
            const { scriptId, startLine, startColumn, endLine, endColumn } = SCRIPTS[1];
            // Debugger locations in scripts without sourceURL annotations are relative to the
            // beginning of the surrounding document, so this is basically a 1-1 mapping.
            assert.strictEqual(endLine, startLine);
            for (let column = startColumn; column < endColumn; ++column) {
                assert.deepEqual(resourceMapping.uiLocationToJSLocations(uiSourceCode, startLine, column), [
                    debuggerModel.createRawLocationByScriptId(scriptId, startLine, column),
                ]);
            }
        });
    });
    describe('uiLocationRangeToRSLocationRanges', () => {
        it('correctly reports all inline <script>s when querying the whole document', () => {
            const rawLocationRanges = resourceMapping.uiLocationRangeToJSLocationRanges(uiSourceCode, new TextUtils.TextRange.TextRange(0, 0, 14, 0));
            assert.exists(rawLocationRanges);
            assert.lengthOf(rawLocationRanges, SCRIPTS.length);
            for (let i = 0; i < SCRIPTS.length; ++i) {
                let { startLine, startColumn, endLine, endColumn } = SCRIPTS[i];
                const { scriptId, hasSourceURLComment } = SCRIPTS[i];
                const { start, end } = rawLocationRanges[i];
                assert.strictEqual(start.scriptId, scriptId);
                assert.strictEqual(end.scriptId, scriptId);
                if (hasSourceURLComment) {
                    if (endLine === startLine) {
                        endColumn -= startColumn;
                    }
                    endLine -= startLine;
                    startLine = 0;
                    startColumn = 0;
                }
                assert.strictEqual(start.lineNumber, startLine);
                assert.strictEqual(start.columnNumber, startColumn);
                assert.strictEqual(end.lineNumber, endLine);
                assert.strictEqual(end.columnNumber, endColumn);
            }
        });
    });
    describe('jsLocationToUILocation', () => {
        it('does not map locations of unrelated scripts', () => {
            assert.isNull(resourceMapping.jsLocationToUILocation(debuggerModel.createRawLocationByScriptId(OTHER_SCRIPT_ID, 1, 1)));
            SCRIPTS.forEach(({ startLine, startColumn, endLine, endColumn }) => {
                // Check that we also don't reverse map locations that overlap with the existing script locations.
                assert.isNull(resourceMapping.jsLocationToUILocation(debuggerModel.createRawLocationByScriptId(OTHER_SCRIPT_ID, startLine, startColumn)));
                assert.isNull(resourceMapping.jsLocationToUILocation(debuggerModel.createRawLocationByScriptId(OTHER_SCRIPT_ID, endLine, endColumn)));
            });
        });
        it('correctly maps inline <script> with //# sourceURL annotation', () => {
            const { scriptId, startLine, startColumn, endLine, endColumn } = SCRIPTS[0];
            // Debugger locations in scripts with sourceURL annotations are relative to the beginning
            // of the script, rather than relative to the start of the surrounding document.
            assert.deepEqual(resourceMapping.jsLocationToUILocation(debuggerModel.createRawLocationByScriptId(scriptId, 0, 0)), new Workspace.UISourceCode.UILocation(uiSourceCode, startLine, startColumn));
            assert.deepEqual(resourceMapping.jsLocationToUILocation(debuggerModel.createRawLocationByScriptId(scriptId, 0, 55)), 
            // This location does not actually exist in the simulated document, but
            // the ResourceMapping doesn't know (and shouldn't care) about that.
            new Workspace.UISourceCode.UILocation(uiSourceCode, startLine, startColumn + 55));
            assert.deepEqual(resourceMapping.jsLocationToUILocation(debuggerModel.createRawLocationByScriptId(scriptId, 2, 0)), new Workspace.UISourceCode.UILocation(uiSourceCode, startLine + 2, 0));
            assert.deepEqual(resourceMapping.jsLocationToUILocation(debuggerModel.createRawLocationByScriptId(scriptId, endLine - startLine, endColumn)), new Workspace.UISourceCode.UILocation(uiSourceCode, endLine, endColumn));
        });
        it('correctly maps inline <script> without //# sourceURL annotation', () => {
            const { scriptId, startLine, startColumn, endLine, endColumn } = SCRIPTS[1];
            // Debugger locations in scripts without sourceURL annotations are relative to the
            // beginning of the surrounding document, so this is basically a 1-1 mapping.
            assert.strictEqual(endLine, startLine);
            for (let column = startColumn; column < endColumn; ++column) {
                assert.deepEqual(resourceMapping.jsLocationToUILocation(debuggerModel.createRawLocationByScriptId(scriptId, startLine, column)), new Workspace.UISourceCode.UILocation(uiSourceCode, startLine, column));
            }
        });
    });
    describe('getMappedLines', () => {
        it('reports line numbers for all inline scripts', () => {
            const expectedLines = new Set();
            SCRIPTS.forEach(({ startLine, endLine }) => {
                for (let line = startLine; line <= endLine; ++line) {
                    expectedLines.add(line);
                }
            });
            const mappedLines = resourceMapping.getMappedLines(uiSourceCode);
            assert.deepEqual(mappedLines, expectedLines);
        });
    });
    describe('functionBoundsAtRawLocation', () => {
        function makeLocation(script, line, column) {
            return new SDK.DebuggerModel.Location(debuggerModel, script.scriptId, line, column);
        }
        it('finds the function bounds for an inline script', async () => {
            const functionBounds = await resourceMapping.functionBoundsAtRawLocation(makeLocation(SCRIPTS[0], 5, 16));
            assert.isOk(functionBounds);
            // TODO(crbug.com/452333154)
            // assert.strictEqual(functionBounds.name, 'foo');
            assert.strictEqual(functionBounds.range.startLine, 5);
            assert.strictEqual(functionBounds.range.startColumn, 12);
            assert.strictEqual(functionBounds.range.endLine, 5);
            assert.strictEqual(functionBounds.range.endColumn, 38);
        });
        it('finds no function bounds for an inline script with no functions', async () => {
            const functionBounds = await resourceMapping.functionBoundsAtRawLocation(makeLocation(SCRIPTS[1], 11, 9));
            assert.isNotOk(functionBounds);
        });
    });
});
//# sourceMappingURL=ResourceMapping.test.js.map