// Copyright 2023 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 * as Common from '../../core/common/common.js';
import * as Platform from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as Bindings from '../../models/bindings/bindings.js';
import * as Logs from '../../models/logs/logs.js';
import * as Workspace from '../../models/workspace/workspace.js';
import { createConsoleViewMessageWithStubDeps, createStackTrace, } from '../../testing/ConsoleHelpers.js';
import { raf } from '../../testing/DOMHelpers.js';
import { createTarget } from '../../testing/EnvironmentHelpers.js';
import { describeWithMockConnection } from '../../testing/MockConnection.js';
import { MockProtocolBackend } from '../../testing/MockScopeChain.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as Console from './console.js';
const { urlString } = Platform.DevToolsPath;
describe('PromptBuilder', () => {
    describe('allowHeader', () => {
        it('disallows cookie headers', () => {
            assert.isNotOk(Console.PromptBuilder.allowHeader({ name: 'Cookie', value: '' }));
            assert.isNotOk(Console.PromptBuilder.allowHeader({ name: 'cookiE', value: '' }));
            assert.isNotOk(Console.PromptBuilder.allowHeader({ name: 'cookie', value: '' }));
            assert.isNotOk(Console.PromptBuilder.allowHeader({ name: 'set-cookie', value: '' }));
            assert.isNotOk(Console.PromptBuilder.allowHeader({ name: 'Set-cOokie', value: '' }));
        });
        it('disallows authorization headers', () => {
            assert.isNotOk(Console.PromptBuilder.allowHeader({ name: 'AuthoRization', value: '' }));
            assert.isNotOk(Console.PromptBuilder.allowHeader({ name: 'authorization', value: '' }));
        });
        it('disallows custom headers', () => {
            assert.isNotOk(Console.PromptBuilder.allowHeader({ name: 'X-smth', value: '' }));
            assert.isNotOk(Console.PromptBuilder.allowHeader({ name: 'X-', value: '' }));
            assert.isNotOk(Console.PromptBuilder.allowHeader({ name: 'x-smth', value: '' }));
            assert.isNotOk(Console.PromptBuilder.allowHeader({ name: 'x-', value: '' }));
        });
    });
    const NETWORK_REQUEST = {
        url() {
            return urlString `https://example.com`;
        },
        requestHeaders() {
            return [{
                    name: 'Origin',
                    value: 'https://example.com',
                }];
        },
        statusCode: 404,
        statusText: 'Not found',
        responseHeaders: [{
                name: 'Origin',
                value: 'https://example.com',
            }],
    };
    describe('format formatNetworkRequest', () => {
        it('formats a network request', () => {
            assert.strictEqual(Console.PromptBuilder.formatNetworkRequest(NETWORK_REQUEST), `Request: https://example.com

Request headers:
Origin: https://example.com

Response headers:
Origin: https://example.com

Response status: 404 Not found`);
        });
    });
    describe('formatRelatedCode', () => {
        it('formats a single line code', () => {
            assert.strictEqual(Console.PromptBuilder.formatRelatedCode({
                text: '12345678901234567890',
                columnNumber: 10,
                lineNumber: 0,
            }, 
            /* maxLength=*/ 5), '89012');
            assert.strictEqual(Console.PromptBuilder.formatRelatedCode({
                text: '12345678901234567890',
                columnNumber: 10,
                lineNumber: 0,
            }, 
            /* maxLength=*/ 6), '890123');
            assert.strictEqual(Console.PromptBuilder.formatRelatedCode({
                text: '12345678901234567890',
                columnNumber: 10,
                lineNumber: 0,
            }, 
            /* maxLength=*/ 30), '12345678901234567890');
        });
        it('formats a multiline code', () => {
            assert.strictEqual(Console.PromptBuilder.formatRelatedCode({
                text: '123\n456\n789\n123\n456\n789\n',
                columnNumber: 1,
                lineNumber: 1,
            }, 
            /* maxLength=*/ 5), '456');
            assert.strictEqual(Console.PromptBuilder.formatRelatedCode({
                text: '123\n456\n789\n123\n456\n789\n',
                columnNumber: 1,
                lineNumber: 1,
            }, 
            /* maxLength=*/ 10), '456\n789\n123');
            assert.strictEqual(Console.PromptBuilder.formatRelatedCode({
                text: '123\n456\n789\n123\n456\n789\n',
                columnNumber: 1,
                lineNumber: 1,
            }, 
            /* maxLength=*/ 16), '123\n456\n789\n123');
        });
        it('uses indentation to select blocks or functions', () => {
            // Somewhat realistic code
            const text = `import something;
import anotherthing;

const x = 1;
function f1() {
  // a

  // b
}

function bigger() {
  // x
  if (true) {
    // y

    // zzzzzz
  }

  let y = x + 2;

  if (false) {
    // a

    f1();
    if (x == x) {
      // z
    }
  }
}

export const y = "";
`;
            assert.strictEqual(Console.PromptBuilder.formatRelatedCode({ text, columnNumber: 4, lineNumber: 11 }, /* maxLength=*/ 233), '  // x\n  if (true) {\n    // y\n\n    // zzzzzz\n  }\n\n  let y = x + 2;\n\n  if (false) {\n    // a\n\n    f1();\n    if (x == x) {\n      // z\n    }\n  }');
            assert.strictEqual(Console.PromptBuilder.formatRelatedCode({ text, columnNumber: 4, lineNumber: 11 }, /* maxLength=*/ 232), '  // x\n  if (true) {\n    // y\n\n    // zzzzzz\n  }\n\n  let y = x + 2;');
            assert.strictEqual(Console.PromptBuilder.formatRelatedCode({ text, columnNumber: 4, lineNumber: 11 }, /* maxLength=*/ 600), text.trim());
            assert.strictEqual(Console.PromptBuilder.formatRelatedCode({ text, columnNumber: 4, lineNumber: 11 }, /* maxLength=*/ 50), '  // x\n  if (true) {\n    // y\n\n    // zzzzzz\n  }');
            assert.strictEqual(Console.PromptBuilder.formatRelatedCode({ text, columnNumber: 4, lineNumber: 11 }, /* maxLength=*/ 40), '  // x');
            assert.strictEqual(Console.PromptBuilder.formatRelatedCode({ text, columnNumber: 4, lineNumber: 18 }, /* maxLength=*/ 50), '  let y = x + 2;');
        });
    });
    it('Extracts expected whitespace from beginnings of lines', () => {
        assert.strictEqual(Console.PromptBuilder.lineWhitespace(' a'), ' ');
        assert.strictEqual(Console.PromptBuilder.lineWhitespace('a'), '');
        assert.isNull(Console.PromptBuilder.lineWhitespace(' '));
        assert.isNull(Console.PromptBuilder.lineWhitespace(''));
        assert.strictEqual(Console.PromptBuilder.lineWhitespace('\t\ta'), '\t\t');
    });
    describeWithMockConnection('buildPrompt', () => {
        let target;
        let backend;
        beforeEach(() => {
            target = createTarget();
            const targetManager = target.targetManager();
            const workspace = Workspace.Workspace.WorkspaceImpl.instance();
            const resourceMapping = new Bindings.ResourceMapping.ResourceMapping(targetManager, workspace);
            const ignoreListManager = Workspace.IgnoreListManager.IgnoreListManager.instance({ forceNew: true });
            Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance({
                forceNew: true,
                resourceMapping,
                targetManager,
                ignoreListManager,
                workspace,
            });
            backend = new MockProtocolBackend();
        });
        const PROMPT_PREFIX = 'Please explain the following console error or warning:';
        const RELATED_CODE_PREFIX = 'For the following code:';
        const RELATED_NETWORK_REQUEST_PREFIX = 'For the following network request:';
        it('builds a simple prompt', async () => {
            const runtimeModel = target.model(SDK.RuntimeModel.RuntimeModel);
            const messageDetails = {
                type: "log" /* Protocol.Runtime.ConsoleAPICalledEventType.Log */,
            };
            const ERROR_MESSAGE = 'kaboom!';
            const rawMessage = new SDK.ConsoleModel.ConsoleMessage(runtimeModel, Common.Console.FrontendMessageSource.ConsoleAPI, /* level */ null, ERROR_MESSAGE, messageDetails);
            const { message } = createConsoleViewMessageWithStubDeps(rawMessage);
            const promptBuilder = new Console.PromptBuilder.PromptBuilder(message);
            const { prompt, sources } = await promptBuilder.buildPrompt();
            assert.strictEqual(prompt, [
                PROMPT_PREFIX,
                '',
                '```',
                ERROR_MESSAGE,
                '```',
            ].join('\n'));
            assert.deepEqual(sources, [{ type: 'message', value: ERROR_MESSAGE }]);
        });
        it('builds a prompt with related code', async () => {
            const runtimeModel = target.model(SDK.RuntimeModel.RuntimeModel);
            const LINE_NUMBER = 42;
            const URL = urlString `http://example.com/script.js`;
            const RELATED_CODE = `${'\n'.repeat(LINE_NUMBER)}console.error('kaboom!')`;
            const script = await backend.addScript(target, { url: URL, content: RELATED_CODE }, null);
            const SCRIPT_ID = script.scriptId;
            const stackTrace = createStackTrace([
                `${SCRIPT_ID}::userNestedFunction::${URL}::${LINE_NUMBER}::15`,
                `${SCRIPT_ID}::userFunction::http://example.com/script.js::10::2`,
                `${SCRIPT_ID}::entry::http://example.com/app.js::25::10`,
            ]);
            const messageDetails = {
                type: "log" /* Protocol.Runtime.ConsoleAPICalledEventType.Log */,
                stackTrace,
            };
            const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel);
            assert.exists(debuggerModel);
            const ERROR_MESSAGE = 'kaboom!';
            const rawMessage = new SDK.ConsoleModel.ConsoleMessage(runtimeModel, Common.Console.FrontendMessageSource.ConsoleAPI, /* level */ null, ERROR_MESSAGE, messageDetails);
            const { message } = createConsoleViewMessageWithStubDeps(rawMessage);
            const promptBuilder = new Console.PromptBuilder.PromptBuilder(message);
            const { prompt, sources } = await promptBuilder.buildPrompt();
            assert.strictEqual(prompt, [
                PROMPT_PREFIX,
                '',
                '```',
                ERROR_MESSAGE,
                '```',
                RELATED_CODE_PREFIX,
                '',
                '```',
                RELATED_CODE.trim(),
                '```',
            ].join('\n'));
            assert.deepEqual(sources, [{ type: 'message', value: ERROR_MESSAGE }, { type: 'relatedCode', value: RELATED_CODE.trim() }]);
        });
        it('builds a prompt with related code and stacktrace', async () => {
            const runtimeModel = target.model(SDK.RuntimeModel.RuntimeModel);
            const LINE_NUMBER = 42;
            const URL = urlString `http://example.com/script.js`;
            const RELATED_CODE = `${'\n'.repeat(LINE_NUMBER)}console.error('kaboom!')`;
            const script = await backend.addScript(target, { url: URL, content: RELATED_CODE }, null);
            const SCRIPT_ID = script.scriptId;
            const stackTrace = createStackTrace([
                `${SCRIPT_ID}::userNestedFunction::${URL}::${LINE_NUMBER}::15`,
                `${SCRIPT_ID}::userFunction::http://example.com/script.js::10::2`,
                `${SCRIPT_ID}_app::entry::http://example.com/app.js::25::10`,
            ]);
            const STACK_TRACE = ['userNestedFunction @ script.js:43', 'userFunction @ script.js:11', 'entry @ /app.js:26'].join('\n');
            const messageDetails = {
                type: "log" /* Protocol.Runtime.ConsoleAPICalledEventType.Log */,
                stackTrace,
            };
            const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel);
            assert.exists(debuggerModel);
            const ERROR_MESSAGE = 'kaboom!';
            const rawMessage = new SDK.ConsoleModel.ConsoleMessage(runtimeModel, Common.Console.FrontendMessageSource.ConsoleAPI, "error" /* Protocol.Log.LogEntryLevel.Error */, ERROR_MESSAGE, messageDetails);
            const { message } = createConsoleViewMessageWithStubDeps(rawMessage);
            message.toMessageElement(); // Trigger rendering.
            await raf();
            await UI.Widget.Widget.allUpdatesComplete;
            const promptBuilder = new Console.PromptBuilder.PromptBuilder(message);
            const { prompt, sources } = await promptBuilder.buildPrompt();
            assert.strictEqual(prompt, [
                PROMPT_PREFIX,
                '',
                '```',
                ERROR_MESSAGE,
                STACK_TRACE,
                '```',
                RELATED_CODE_PREFIX,
                '',
                '```',
                RELATED_CODE.trim(),
                '```',
            ].join('\n'));
            assert.deepEqual(sources, [
                { type: 'message', value: ERROR_MESSAGE },
                { type: 'stacktrace', value: STACK_TRACE },
                { type: 'relatedCode', value: RELATED_CODE.trim() },
            ]);
        });
        it('trims a very long network request', async () => {
            const runtimeModel = target.model(SDK.RuntimeModel.RuntimeModel);
            const REQUEST_ID = '29.1';
            const messageDetails = {
                type: "log" /* Protocol.Runtime.ConsoleAPICalledEventType.Log */,
                affectedResources: {
                    requestId: REQUEST_ID,
                },
            };
            const NETWORK_REQUEST = {
                url() {
                    return urlString `https://example.com`;
                },
                requestHeaders() {
                    return Array(100).fill({
                        name: 'Origin',
                        value: 'https://example.com',
                    });
                },
                statusCode: 404,
                statusText: 'Not found',
                responseHeaders: Array(100).fill({
                    name: 'Origin',
                    value: 'https://example.com',
                }),
            };
            sinon.stub(Logs.NetworkLog.NetworkLog.instance(), 'requestsForId').withArgs(REQUEST_ID).returns([
                NETWORK_REQUEST,
            ]);
            const RELATED_REQUEST = [
                'Request: https://example.com',
                '',
                'Request headers:',
                'Origin: https://example.com\n'.repeat(35),
                'Response headers:',
                'Origin: https://example.com\n'.repeat(35),
                'Response status: 404 Not found',
            ].join('\n');
            const ERROR_MESSAGE = 'kaboom!';
            const rawMessage = new SDK.ConsoleModel.ConsoleMessage(runtimeModel, Common.Console.FrontendMessageSource.ConsoleAPI, /* level */ null, ERROR_MESSAGE, messageDetails);
            const { message } = createConsoleViewMessageWithStubDeps(rawMessage);
            const promptBuilder = new Console.PromptBuilder.PromptBuilder(message);
            const { prompt, sources } = await promptBuilder.buildPrompt();
            assert.strictEqual(prompt, [
                PROMPT_PREFIX,
                '',
                '```',
                ERROR_MESSAGE,
                '```',
                RELATED_NETWORK_REQUEST_PREFIX,
                '',
                '```',
                RELATED_REQUEST,
                '```',
            ].join('\n'));
            assert.deepEqual(sources, [{ type: 'message', value: ERROR_MESSAGE }, { type: 'networkRequest', value: RELATED_REQUEST }]);
        });
        it('trims a very long console message', async () => {
            const runtimeModel = target.model(SDK.RuntimeModel.RuntimeModel);
            const messageDetails = {
                type: "log" /* Protocol.Runtime.ConsoleAPICalledEventType.Log */,
            };
            const ERROR_MESSAGE = 'a'.repeat(2000);
            const TRIMMED_ERROR_MESSAGE = 'a'.repeat(1000);
            const rawMessage = new SDK.ConsoleModel.ConsoleMessage(runtimeModel, Common.Console.FrontendMessageSource.ConsoleAPI, /* level */ null, ERROR_MESSAGE, messageDetails);
            const { message } = createConsoleViewMessageWithStubDeps(rawMessage);
            const promptBuilder = new Console.PromptBuilder.PromptBuilder(message);
            const { prompt, sources } = await promptBuilder.buildPrompt();
            assert.strictEqual(prompt, [
                PROMPT_PREFIX,
                '',
                '```',
                TRIMMED_ERROR_MESSAGE,
                '```',
            ].join('\n'));
            assert.deepEqual(sources, [{ type: 'message', value: TRIMMED_ERROR_MESSAGE }]);
        });
        it('trims a very long stack trace', async () => {
            const runtimeModel = target.model(SDK.RuntimeModel.RuntimeModel);
            const LINE_NUMBER = 0;
            const URL = urlString `${`http://example.com/${'a'.repeat(100)}.js`}`;
            const RELATED_CODE = 'console.error(\'kaboom!\')';
            const script = await backend.addScript(target, { url: URL, content: RELATED_CODE }, null);
            const SCRIPT_ID = script.scriptId;
            const STACK_FRAME = `${SCRIPT_ID}::userNestedFunction::${URL}::${LINE_NUMBER}::15`;
            const stackTrace = createStackTrace(Array(80).fill(STACK_FRAME));
            const STACK_TRACE = `userNestedFunction @ ${'a'.repeat(100)}.js:1\n`.repeat(7) +
                'userNestedFunction @ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
            const messageDetails = {
                type: "log" /* Protocol.Runtime.ConsoleAPICalledEventType.Log */,
                stackTrace,
            };
            const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel);
            assert.isNotNull(debuggerModel);
            const ERROR_MESSAGE = 'kaboom!';
            const rawMessage = new SDK.ConsoleModel.ConsoleMessage(runtimeModel, Common.Console.FrontendMessageSource.ConsoleAPI, "error" /* Protocol.Log.LogEntryLevel.Error */, ERROR_MESSAGE, messageDetails);
            const { message } = createConsoleViewMessageWithStubDeps(rawMessage);
            message.toMessageElement(); // Trigger rendering.
            await raf();
            await UI.Widget.Widget.allUpdatesComplete;
            const promptBuilder = new Console.PromptBuilder.PromptBuilder(message);
            const { prompt, sources } = await promptBuilder.buildPrompt();
            assert.strictEqual(prompt, [
                PROMPT_PREFIX,
                '',
                '```',
                ERROR_MESSAGE,
                STACK_TRACE,
                '```',
                RELATED_CODE_PREFIX,
                '',
                '```',
                RELATED_CODE.trim(),
                '```',
            ].join('\n'));
            assert.deepEqual(sources, [
                { type: 'message', value: ERROR_MESSAGE },
                { type: 'stacktrace', value: STACK_TRACE },
                { type: 'relatedCode', value: RELATED_CODE.trim() },
            ]);
        });
        it('builds a prompt with related request', async () => {
            const runtimeModel = target.model(SDK.RuntimeModel.RuntimeModel);
            const REQUEST_ID = '29.1';
            const messageDetails = {
                type: "log" /* Protocol.Runtime.ConsoleAPICalledEventType.Log */,
                affectedResources: {
                    requestId: REQUEST_ID,
                },
            };
            sinon.stub(Logs.NetworkLog.NetworkLog.instance(), 'requestsForId').withArgs(REQUEST_ID).returns([
                NETWORK_REQUEST,
            ]);
            const RELATED_REQUEST = [
                'Request: https://example.com',
                '',
                'Request headers:',
                'Origin: https://example.com',
                '',
                'Response headers:',
                'Origin: https://example.com',
                '',
                'Response status: 404 Not found',
            ].join('\n');
            const ERROR_MESSAGE = 'kaboom!';
            const rawMessage = new SDK.ConsoleModel.ConsoleMessage(runtimeModel, Common.Console.FrontendMessageSource.ConsoleAPI, /* level */ null, ERROR_MESSAGE, messageDetails);
            const { message } = createConsoleViewMessageWithStubDeps(rawMessage);
            const promptBuilder = new Console.PromptBuilder.PromptBuilder(message);
            const { prompt, sources, isPageReloadRecommended } = await promptBuilder.buildPrompt();
            assert.strictEqual(prompt, [
                PROMPT_PREFIX,
                '',
                '```',
                ERROR_MESSAGE,
                '```',
                RELATED_NETWORK_REQUEST_PREFIX,
                '',
                '```',
                RELATED_REQUEST,
                '```',
            ].join('\n'));
            assert.isNotTrue(isPageReloadRecommended, 'PromptBuilder did recommend reloading the page');
            assert.deepEqual(sources, [{ type: 'message', value: ERROR_MESSAGE }, { type: 'networkRequest', value: RELATED_REQUEST }]);
        });
        it('recommends page reload if the sources are not complete', async () => {
            const runtimeModel = target.model(SDK.RuntimeModel.RuntimeModel);
            const REQUEST_ID = '29.1';
            const messageDetails = {
                type: "log" /* Protocol.Runtime.ConsoleAPICalledEventType.Log */,
                affectedResources: {
                    requestId: REQUEST_ID,
                },
            };
            sinon.stub(Logs.NetworkLog.NetworkLog.instance(), 'requestsForId').withArgs(REQUEST_ID).returns([]);
            const ERROR_MESSAGE = 'kaboom!';
            const rawMessage = new SDK.ConsoleModel.ConsoleMessage(runtimeModel, Common.Console.FrontendMessageSource.ConsoleAPI, /* level */ null, ERROR_MESSAGE, messageDetails);
            const { message } = createConsoleViewMessageWithStubDeps(rawMessage);
            const promptBuilder = new Console.PromptBuilder.PromptBuilder(message);
            const { sources, isPageReloadRecommended } = await promptBuilder.buildPrompt();
            assert.isTrue(isPageReloadRecommended, 'PromptBuilder did not recommend reloading the page');
            assert.isNotTrue(sources.some(source => source.type === Console.PromptBuilder.SourceType.NETWORK_REQUEST));
        });
    });
    describeWithMockConnection('getSearchQuery', () => {
        let target;
        beforeEach(() => {
            target = createTarget();
        });
        it('builds a simple search query', async () => {
            const runtimeModel = target.model(SDK.RuntimeModel.RuntimeModel);
            const messageDetails = {
                type: "log" /* Protocol.Runtime.ConsoleAPICalledEventType.Log */,
            };
            const ERROR_MESSAGE = 'kaboom!';
            const rawMessage = new SDK.ConsoleModel.ConsoleMessage(runtimeModel, Common.Console.FrontendMessageSource.ConsoleAPI, "error" /* Protocol.Log.LogEntryLevel.Error */, ERROR_MESSAGE, messageDetails);
            const { message } = createConsoleViewMessageWithStubDeps(rawMessage);
            const promptBuilder = new Console.PromptBuilder.PromptBuilder(message);
            const query = await promptBuilder.getSearchQuery();
            assert.strictEqual(query, 'kaboom!');
        });
        it('builds a search query from an error without the callstack', async () => {
            const runtimeModel = target.model(SDK.RuntimeModel.RuntimeModel);
            const messageDetails = {
                type: "log" /* Protocol.Runtime.ConsoleAPICalledEventType.Log */,
            };
            const ERROR_MESSAGE = 'Got an error: ' + new Error('fail').stack;
            const rawMessage = new SDK.ConsoleModel.ConsoleMessage(runtimeModel, Common.Console.FrontendMessageSource.ConsoleAPI, "error" /* Protocol.Log.LogEntryLevel.Error */, ERROR_MESSAGE, messageDetails);
            const { message } = createConsoleViewMessageWithStubDeps(rawMessage);
            const promptBuilder = new Console.PromptBuilder.PromptBuilder(message);
            const query = await promptBuilder.getSearchQuery();
            assert.strictEqual(query, 'Got an error: Error: fail');
        });
    });
});
//# sourceMappingURL=PromptBuilder.test.js.map