// *****************************************************************************
// Copyright (C) 2024 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import * as sinon from 'sinon';
import { ChatAgentServiceImpl } from './chat-agent-service';
import { ChatRequestParserImpl } from './chat-request-parser';
import { ChatAgent, ChatAgentLocation } from './chat-agents';
import { ChatContext, ChatRequest } from './chat-model';
import { expect } from 'chai';
import { AIVariable, DefaultAIVariableService, PromptService, ResolvedAIVariable, ToolInvocationRegistryImpl, ToolRequest } from '@theia/ai-core';
import { ILogger, Logger } from '@theia/core';
import { ParsedChatRequestAgentPart, ParsedChatRequestFunctionPart, ParsedChatRequestTextPart, ParsedChatRequestVariablePart } from './parsed-chat-request';
import { AgentDelegationTool } from '../browser/agent-delegation-tool';

describe('ChatRequestParserImpl', () => {
    /** Command names the stubbed `PromptService` knows about. Anything else is not a command. */
    const KNOWN_COMMANDS = ['hello', 'explain', 'compare', 'cmd', 'summarize', 'skill-one', 'skill-two'];
    /** Prompt fragments that are not marked as commands, but can still be invoked by their id. */
    const KNOWN_FRAGMENT_IDS = ['coder-system'];

    const promptService = {
        isKnownCommand: (name: string) => KNOWN_COMMANDS.includes(name) || KNOWN_FRAGMENT_IDS.includes(name),
        getCommands: () => KNOWN_COMMANDS.map(name => ({ id: `command-${name}`, template: '', isCommand: true, commandName: name }))
    } as unknown as PromptService;

    const chatAgentService = sinon.createStubInstance(ChatAgentServiceImpl);
    const variableService = sinon.createStubInstance(DefaultAIVariableService);
    const toolInvocationRegistry = sinon.createStubInstance(ToolInvocationRegistryImpl);
    const logger: ILogger = sinon.createStubInstance(Logger);
    const parser = new ChatRequestParserImpl(chatAgentService, variableService, toolInvocationRegistry, logger);
    // The parser injects the PromptService as a property, so it has to be assigned manually here.
    (parser as unknown as { promptService: PromptService }).promptService = promptService;

    beforeEach(() => {
        // Reset our stubs before each test
        sinon.reset();
    });

    it('parses simple text', async () => {
        const req: ChatRequest = {
            text: 'What is the best pizza topping?'
        };
        const context: ChatContext = { variables: [] };
        const result = await parser.parseChatRequest(req, ChatAgentLocation.Panel, context);
        expect(result.parts.length).to.equal(1);
        const part = result.parts[0] as ParsedChatRequestTextPart;
        expect(part.kind).to.equal('text');
        expect(part.text).to.equal('What is the best pizza topping?');
        expect(part.range).to.deep.equal({ start: 0, endExclusive: 31 });
    });

    it('parses text with variable name', async () => {
        const req: ChatRequest = {
            text: 'What is the #best pizza topping?'
        };
        const context: ChatContext = { variables: [] };
        const result = await parser.parseChatRequest(req, ChatAgentLocation.Panel, context);
        expect(result.parts.length).to.equal(3);

        const textPart1 = result.parts[0] as ParsedChatRequestTextPart;
        expect(textPart1.kind).to.equal('text');
        expect(textPart1.text).to.equal('What is the ');
        expect(textPart1.range).to.deep.equal({ start: 0, endExclusive: 12 });

        const varPart = result.parts[1] as ParsedChatRequestVariablePart;
        expect(varPart.kind).to.equal('var');
        expect(varPart.variableName).to.equal('best');
        expect(varPart.variableArg).to.be.undefined;
        expect(varPart.range).to.deep.equal({ start: 12, endExclusive: 17 });

        const textPart2 = result.parts[2] as ParsedChatRequestTextPart;
        expect(textPart2.kind).to.equal('text');
        expect(textPart2.text).to.equal(' pizza topping?');
        expect(textPart2.range).to.deep.equal({ start: 17, endExclusive: 32 });
    });

    it('parses text with variable name with argument', async () => {
        const req: ChatRequest = {
            text: 'What is the #best:by-poll pizza topping?'
        };
        const context: ChatContext = { variables: [] };
        const result = await parser.parseChatRequest(req, ChatAgentLocation.Panel, context);
        expect(result.parts.length).to.equal(3);

        const textPart1 = result.parts[0] as ParsedChatRequestTextPart;
        expect(textPart1.kind).to.equal('text');
        expect(textPart1.text).to.equal('What is the ');
        expect(textPart1.range).to.deep.equal({ start: 0, endExclusive: 12 });

        const varPart = result.parts[1] as ParsedChatRequestVariablePart;
        expect(varPart.kind).to.equal('var');
        expect(varPart.variableName).to.equal('best');
        expect(varPart.variableArg).to.equal('by-poll');
        expect(varPart.range).to.deep.equal({ start: 12, endExclusive: 25 });

        const textPart2 = result.parts[2] as ParsedChatRequestTextPart;
        expect(textPart2.kind).to.equal('text');
        expect(textPart2.text).to.equal(' pizza topping?');
        expect(textPart2.range).to.deep.equal({ start: 25, endExclusive: 40 });
    });

    it('parses text with variable name with numeric argument', async () => {
        const req: ChatRequest = {
            text: '#size-class:2'
        };
        const context: ChatContext = { variables: [] };
        const result = await parser.parseChatRequest(req, ChatAgentLocation.Panel, context);
        expect(result.parts[0]).to.contain(
            {
                variableName: 'size-class',
                variableArg: '2'
            }
        );
    });

    it('parses text with variable name with POSIX path argument', async () => {
        const req: ChatRequest = {
            text: '#file:/path/to/file.ext'
        };
        const context: ChatContext = { variables: [] };
        const result = await parser.parseChatRequest(req, ChatAgentLocation.Panel, context);
        expect(result.parts[0]).to.contain(
            {
                variableName: 'file',
                variableArg: '/path/to/file.ext'
            }
        );
    });

    it('parses text with variable name with Win32 path argument', async () => {
        const req: ChatRequest = {
            text: '#file:c:\\path\\to\\file.ext'
        };
        const context: ChatContext = { variables: [] };
        const result = await parser.parseChatRequest(req, ChatAgentLocation.Panel, context);
        expect(result.parts[0]).to.contain(
            {
                variableName: 'file',
                variableArg: 'c:\\path\\to\\file.ext'
            }
        );
    });

    it('resolves variable and extracts tool functions from resolved variable', async () => {
        // Set up two test tool requests that will be referenced in the variable content
        const testTool1: ToolRequest = {
            id: 'testTool1',
            name: 'Test Tool 1',
            handler: async () => undefined,
            parameters: {
                type: 'object',
                properties: {}
            },
        };
        const testTool2: ToolRequest = {
            id: 'testTool2',
            name: 'Test Tool 2',
            handler: async () => undefined,
            parameters: {
                type: 'object',
                properties: {}
            },
        };
        // Configure the tool registry to return our test tools
        toolInvocationRegistry.getFunction.withArgs(testTool1.id).returns(testTool1);
        toolInvocationRegistry.getFunction.withArgs(testTool2.id).returns(testTool2);

        // Set up the test variable to include in the request
        const testVariable: AIVariable = {
            id: 'testVariable',
            name: 'testVariable',
            description: 'A test variable',
        };
        // Configure the variable service to return our test variable
        // One tool reference uses chat format and one uses prompt format because the parser needs to handle both.
        variableService.getVariable.withArgs(testVariable.name).returns(testVariable);
        variableService.resolveVariable.withArgs(
            { variable: testVariable.name, arg: 'myarg' },
            sinon.match.any
        ).resolves({
            variable: testVariable,
            arg: 'myarg',
            value: 'This is a test with ~testTool1 and **~{testTool2}** and more text.',
        });

        // Create a request with the test variable
        const req: ChatRequest = {
            text: 'Test with #testVariable:myarg'
        };
        const context: ChatContext = { variables: [] };

        // Parse the request
        const result = await parser.parseChatRequest(req, ChatAgentLocation.Panel, context);

        // Verify the variable part contains the correct properties
        expect(result.parts.length).to.equal(2);
        expect(result.parts[0] instanceof ParsedChatRequestTextPart).to.be.true;
        expect(result.parts[1] instanceof ParsedChatRequestVariablePart).to.be.true;
        const variablePart = result.parts[1] as ParsedChatRequestVariablePart;
        expect(variablePart).to.have.property('resolution');
        expect(variablePart.resolution).to.deep.equal({
            variable: testVariable,
            arg: 'myarg',
            value: 'This is a test with ~testTool1 and **~{testTool2}** and more text.',
        } satisfies ResolvedAIVariable);

        // Verify both tool functions were extracted from the variable content
        expect(result.toolRequests.size).to.equal(2);
        expect(result.toolRequests.has(testTool1.id)).to.be.true;
        expect(result.toolRequests.has(testTool2.id)).to.be.true;

        // Verify the result contains the tool requests returned by the registry
        expect(result.toolRequests.get(testTool1.id)).to.deep.equal(testTool1);
        expect(result.toolRequests.get(testTool2.id)).to.deep.equal(testTool2);
    });

    it('parses simple command without arguments', async () => {
        const req: ChatRequest = {
            text: '/hello'
        };
        const context: ChatContext = { variables: [] };
        const result = await parser.parseChatRequest(req, ChatAgentLocation.Panel, context);

        expect(result.parts.length).to.equal(1);
        expect(result.parts[0] instanceof ParsedChatRequestVariablePart).to.be.true;
        const varPart = result.parts[0] as ParsedChatRequestVariablePart;
        expect(varPart.variableName).to.equal('prompt');
        expect(varPart.variableArg).to.equal('hello');
    });

    it('parses command with single argument', async () => {
        const req: ChatRequest = {
            text: '/explain topic'
        };
        const context: ChatContext = { variables: [] };
        const result = await parser.parseChatRequest(req, ChatAgentLocation.Panel, context);

        expect(result.parts.length).to.equal(1);
        const varPart = result.parts[0] as ParsedChatRequestVariablePart;
        expect(varPart.variableName).to.equal('prompt');
        expect(varPart.variableArg).to.equal('explain|topic');
    });

    it('parses command with multiple arguments', async () => {
        const req: ChatRequest = {
            text: '/compare item1 item2'
        };
        const context: ChatContext = { variables: [] };
        const result = await parser.parseChatRequest(req, ChatAgentLocation.Panel, context);

        const varPart = result.parts[0] as ParsedChatRequestVariablePart;
        expect(varPart.variableName).to.equal('prompt');
        expect(varPart.variableArg).to.equal('compare|item1 item2');
    });

    it('parses command with quoted arguments', async () => {
        const req: ChatRequest = {
            text: '/cmd "arg with spaces" other'
        };
        const context: ChatContext = { variables: [] };
        const result = await parser.parseChatRequest(req, ChatAgentLocation.Panel, context);

        const varPart = result.parts[0] as ParsedChatRequestVariablePart;
        expect(varPart.variableArg).to.equal('cmd|"arg with spaces" other');
    });

    it('handles command with escaped quotes', async () => {
        const req: ChatRequest = {
            text: '/cmd "arg with \\"quote\\"" other'
        };
        const context: ChatContext = { variables: [] };
        const result = await parser.parseChatRequest(req, ChatAgentLocation.Panel, context);

        const varPart = result.parts[0] as ParsedChatRequestVariablePart;
        expect(varPart.variableArg).to.equal('cmd|"arg with \\"quote\\"" other');
    });

    it('parses multiple commands in one message', async () => {
        const req: ChatRequest = {
            text: '/skill-one /skill-two'
        };
        const context: ChatContext = { variables: [] };
        const result = await parser.parseChatRequest(req, ChatAgentLocation.Panel, context);

        expect(result.parts.length).to.equal(3);
        const firstCommand = result.parts[0] as ParsedChatRequestVariablePart;
        const separator = result.parts[1] as ParsedChatRequestTextPart;
        const secondCommand = result.parts[2] as ParsedChatRequestVariablePart;
        expect(firstCommand.variableName).to.equal('prompt');
        expect(firstCommand.variableArg).to.equal('skill-one');
        expect(separator.text).to.equal(' ');
        expect(secondCommand.variableName).to.equal('prompt');
        expect(secondCommand.variableArg).to.equal('skill-two');
    });

    it('inserts a separator between two argument-taking commands', async () => {
        const req: ChatRequest = {
            text: '/summarize foo /hello bar'
        };
        const context: ChatContext = { variables: [] };
        const result = await parser.parseChatRequest(req, ChatAgentLocation.Panel, context);

        expect(result.parts.length).to.equal(3);
        const firstCommand = result.parts[0] as ParsedChatRequestVariablePart;
        const separator = result.parts[1] as ParsedChatRequestTextPart;
        const secondCommand = result.parts[2] as ParsedChatRequestVariablePart;
        expect(firstCommand.variableArg).to.equal('summarize|foo');
        expect(separator.kind).to.equal('text');
        expect(separator.text).to.equal(' ');
        expect(secondCommand.variableArg).to.equal('hello|bar');
    });

    it('keeps path-like slash arguments as command arguments', async () => {
        const req: ChatRequest = {
            text: '/explain /path/to/file'
        };
        const context: ChatContext = { variables: [] };
        const result = await parser.parseChatRequest(req, ChatAgentLocation.Panel, context);

        expect(result.parts.length).to.equal(1);
        const varPart = result.parts[0] as ParsedChatRequestVariablePart;
        expect(varPart.variableName).to.equal('prompt');
        expect(varPart.variableArg).to.equal('explain|/path/to/file');
    });

    it('does not treat path segments as commands', async () => {
        const req: ChatRequest = {
            text: 'please look at /home/user/notes.txt and fix the bug'
        };
        const context: ChatContext = { variables: [] };
        const result = await parser.parseChatRequest(req, ChatAgentLocation.Panel, context);

        expect(result.parts.length).to.equal(1);
        expect(result.parts[0].kind).to.equal('text');
        expect(result.parts[0].text).to.equal(req.text);
    });

    it('treats the first @agent mention as the selector and does not allow later mentions to override it', async () => {
        const createAgent = (id: string): ChatAgent => ({
            id,
            name: id,
            description: '',
            tags: [],
            variables: [],
            prompts: [],
            agentSpecificVariables: [],
            functions: [],
            languageModelRequirements: [],
            locations: [ChatAgentLocation.Panel],
            invoke: async () => undefined,
        });
        const req: ChatRequest = {
            text: '@agentA do X @agentB do Y'
        };
        const context: ChatContext = { variables: [] };

        chatAgentService.getAgents.returns([
            createAgent('agentA'),
            createAgent('agentB'),
        ]);

        const result = await parser.parseChatRequest(req, ChatAgentLocation.Panel, context);
        const agentParts = result.parts.filter(p => p instanceof ParsedChatRequestAgentPart) as ParsedChatRequestAgentPart[];

        expect(agentParts.length).to.equal(1);
        expect(agentParts[0].agentId).to.equal('agentA');
        expect(agentParts[0].agentName).to.equal('agentA');
    });

    it('delegateToAgent(agentId, prompt) composes a request that forces selecting agentId even if prompt mentions other agents', async () => {
        const createAgent = (id: string): ChatAgent => ({
            id,
            name: id,
            description: '',
            tags: [],
            variables: [],
            prompts: [],
            agentSpecificVariables: [],
            functions: [],
            languageModelRequirements: [],
            locations: [ChatAgentLocation.Panel],
            invoke: async () => undefined,
        });

        // Set up the parser's agent service so it can recognise agents during parsing
        chatAgentService.getAgents.returns([createAgent('agentA'), createAgent('agentB')]);

        const tool = new AgentDelegationTool();
        (tool as unknown as { getChatAgentService: () => unknown }).getChatAgentService = () => ({
            getAgent: sinon.stub().withArgs('agentA').returns(createAgent('agentA')),
            getAgents: sinon.stub().returns([createAgent('agentA')]),
        });

        const sendRequest = sinon.stub().callsFake(async (_sessionId: string, request: ChatRequest) => {
            const parseResult = await parser.parseChatRequest(request, ChatAgentLocation.Panel, { variables: [] });
            const agentParts = parseResult.parts.filter(p => p instanceof ParsedChatRequestAgentPart) as ParsedChatRequestAgentPart[];
            expect(agentParts.length).to.equal(1);
            expect(agentParts[0].agentId).to.equal('agentA');

            return {
                requestCompleted: Promise.resolve({ cancel: () => undefined }),
                responseCompleted: Promise.resolve({ response: { content: [{ kind: 'text', content: 'ok', asString: () => 'ok' }] } }),
            };
        });

        (tool as unknown as { getChatService: () => unknown }).getChatService = () => ({
            getActiveSession: sinon.stub().returns(undefined),
            setActiveSession: sinon.stub(),
            createSession: sinon.stub().returns({
                id: 'session-1',
                model: {
                    changeSet: {
                        onDidChange: sinon.stub().returns({ dispose: sinon.stub() }),
                        getElements: sinon.stub().returns([]),
                        setTitle: sinon.stub(),
                        addElements: sinon.stub(),
                    },
                    onDidChange: sinon.stub().returns({ dispose: sinon.stub() })
                }
            }),
            sendRequest,
            deleteSession: sinon.stub().resolves(undefined),
        });

        const toolRequest = tool.getTool();
        await toolRequest.handler(
            JSON.stringify({ agentId: 'agentA', prompt: 'do X @agentB do Y' }),
            {
                cancellationToken: { isCancellationRequested: false, onCancellationRequested: sinon.stub() },
                request: {
                    session: { changeSet: { setTitle: sinon.stub(), addElements: sinon.stub() } },
                },
                response: {
                    cancellationToken: { isCancellationRequested: false, onCancellationRequested: sinon.stub() },
                    response: { addContent: sinon.stub() },
                },
            } as unknown as Parameters<typeof toolRequest.handler>[1]
        );

        expect(sendRequest.calledOnce).to.be.true;
        const delegatedChatRequest = sendRequest.firstCall.args[1] as ChatRequest;
        expect(delegatedChatRequest.text).to.equal('@agentA do X @agentB do Y');
    });

    describe('deferred tool references', () => {
        const deferredTool: ToolRequest = {
            id: 'deferredTool',
            name: 'Deferred Tool',
            handler: async () => undefined,
            parameters: { type: 'object', properties: {} }
        };
        const eagerTool: ToolRequest = {
            id: 'eagerTool',
            name: 'Eager Tool',
            handler: async () => undefined,
            parameters: { type: 'object', properties: {} }
        };

        beforeEach(() => {
            toolInvocationRegistry.getFunction.withArgs(deferredTool.id).returns(deferredTool);
            toolInvocationRegistry.getFunction.withArgs(eagerTool.id).returns(eagerTool);
        });

        it('parses ~?toolId chat-format syntax as a deferred tool reference', async () => {
            const req: ChatRequest = { text: 'Please use ~?deferredTool here' };
            const result = await parser.parseChatRequest(req, ChatAgentLocation.Panel, { variables: [] });

            const fnPart = result.parts.find(p => p instanceof ParsedChatRequestFunctionPart) as ParsedChatRequestFunctionPart;
            expect(fnPart, 'expected a function part').to.not.be.undefined;
            expect(fnPart.toolRequest.id).to.equal('deferredTool');
            expect(fnPart.deferred).to.be.true;
            expect(fnPart.text).to.equal('~?deferredTool');

            expect(result.toolRequests.size).to.equal(1);
            expect(result.deferredToolIds?.has('deferredTool')).to.be.true;
        });

        it('parses ~{?toolId} prompt-format syntax as a deferred tool reference', async () => {
            const req: ChatRequest = { text: 'Use ~{?deferredTool} now.' };
            const result = await parser.parseChatRequest(req, ChatAgentLocation.Panel, { variables: [] });

            const fnPart = result.parts.find(p => p instanceof ParsedChatRequestFunctionPart) as ParsedChatRequestFunctionPart;
            expect(fnPart, 'expected a function part').to.not.be.undefined;
            expect(fnPart.toolRequest.id).to.equal('deferredTool');
            expect(fnPart.deferred).to.be.true;

            expect(result.deferredToolIds?.has('deferredTool')).to.be.true;
        });

        it('does not mark plain references as deferred', async () => {
            const req: ChatRequest = { text: 'Use ~eagerTool and ~{eagerTool} please.' };
            const result = await parser.parseChatRequest(req, ChatAgentLocation.Panel, { variables: [] });

            const fnParts = result.parts.filter(p => p instanceof ParsedChatRequestFunctionPart) as ParsedChatRequestFunctionPart[];
            expect(fnParts.length).to.equal(2);
            for (const part of fnParts) {
                expect(part.deferred).to.be.false;
            }
            expect(result.deferredToolIds?.size ?? 0).to.equal(0);
        });

        it('mixes deferred and non-deferred references in a single request', async () => {
            const req: ChatRequest = { text: 'Use ~eagerTool first, then ~?deferredTool please' };
            const result = await parser.parseChatRequest(req, ChatAgentLocation.Panel, { variables: [] });

            const fnParts = result.parts.filter(p => p instanceof ParsedChatRequestFunctionPart) as ParsedChatRequestFunctionPart[];
            expect(fnParts.length).to.equal(2);
            const eagerPart = fnParts.find(p => p.toolRequest.id === 'eagerTool');
            const deferredPart = fnParts.find(p => p.toolRequest.id === 'deferredTool');

            expect(eagerPart?.deferred).to.be.false;
            expect(deferredPart?.deferred).to.be.true;
            expect(result.deferredToolIds?.size).to.equal(1);
            expect(result.deferredToolIds?.has('deferredTool')).to.be.true;
            expect(result.deferredToolIds?.has('eagerTool')).to.be.false;
        });
    });

    describe('slash command disambiguation', () => {
        const parse = (text: string) => parser.parseChatRequest({ text }, ChatAgentLocation.Panel, { variables: [] });

        /** Every character of the input has to be covered by exactly one part, in order and without gaps. */
        const expectFullCoverage = (parts: ReadonlyArray<{ range: { start: number, endExclusive: number } }>, text: string): void => {
            let expectedStart = 0;
            for (const part of parts) {
                expect(part.range.start, `part starting at ${part.range.start} does not continue at ${expectedStart}`).to.equal(expectedStart);
                expectedStart = part.range.endExclusive;
            }
            expect(expectedStart, 'parts do not cover the whole message').to.equal(text.length);
        };

        const expectPlainText = async (text: string) => {
            const result = await parse(text);
            expect(result.parts.map(p => p.kind), `expected only text parts for ${JSON.stringify(text)}`).to.deep.equal(['text']);
            expect(result.parts[0].text).to.equal(text);
            expectFullCoverage(result.parts, text);
        };

        describe('unknown commands stay plain text', () => {
            const unknownCommandInputs = [
                'please look at /home/user/notes.txt and fix the bug',
                '/home/user/notes.txt is broken',
                'read /usr/local/bin/theia',
                'the file is at /etc/hosts',
                'compare /tmp/a.txt with /tmp/b.txt',
                'run cd /home && ls',
                'see /usr/lib/node/foo.js:12 for the stack trace',
                'what does the option --foo /bar do?',
                '/does-not-exist do something',
                '/tmp',
                'move it to /tmp',
                'the separator is / on Unix'
            ];

            unknownCommandInputs.forEach(text => {
                it(`keeps ${JSON.stringify(text)} as plain text`, () => expectPlainText(text));
            });

            it('does not drop any user text when a path follows a known command name prefix', async () => {
                // `/summarizes` is not a known command, even though `/summarize` is.
                await expectPlainText('/summarizes the file');
            });

            it('does not treat a known command name as a command when it is a path segment', async () => {
                // `summarize` is known, but `/summarize/foo` is a path, not a command invocation.
                await expectPlainText('/summarize/foo is a path');
            });

            it('does not treat a known command name followed by punctuation as a command', async () => {
                await expectPlainText('did you mean /summarize?');
            });
        });

        describe('slashes that are not command leaders', () => {
            const nonLeaderInputs = [
                'A: 10/20 and B: 30/40',
                'use and/or here',
                '2026/08/04 is the date',
                'see https://example.com/foo/bar for details',
                'the regex is a\\/b'
            ];

            nonLeaderInputs.forEach(text => {
                it(`keeps ${JSON.stringify(text)} as plain text`, () => expectPlainText(text));
            });
        });

        describe('known commands still parse', () => {
            it('parses a known command without arguments', async () => {
                const text = '/summarize';
                const result = await parse(text);

                expect(result.parts.length).to.equal(1);
                const command = result.parts[0] as ParsedChatRequestVariablePart;
                expect(command.variableName).to.equal('prompt');
                expect(command.variableArg).to.equal('summarize');
                expectFullCoverage(result.parts, text);
            });

            it('parses a known command with arguments', async () => {
                const text = '/summarize foo bar';
                const result = await parse(text);

                expect(result.parts.length).to.equal(1);
                expect((result.parts[0] as ParsedChatRequestVariablePart).variableArg).to.equal('summarize|foo bar');
                expectFullCoverage(result.parts, text);
            });

            it('keeps a path as the argument of a known command', async () => {
                const text = '/summarize /home/user/notes.txt';
                const result = await parse(text);

                expect(result.parts.length).to.equal(1);
                expect((result.parts[0] as ParsedChatRequestVariablePart).variableArg).to.equal('summarize|/home/user/notes.txt');
                expectFullCoverage(result.parts, text);
            });

            it('keeps an unknown slash token inside the arguments of a known command', async () => {
                const text = '/summarize foo /unknown bar';
                const result = await parse(text);

                expect(result.parts.length).to.equal(1);
                expect((result.parts[0] as ParsedChatRequestVariablePart).variableArg).to.equal('summarize|foo /unknown bar');
                expectFullCoverage(result.parts, text);
            });

            it('parses a known command in the middle of a message', async () => {
                const text = 'please /summarize this';
                const result = await parse(text);

                expect(result.parts.map(p => p.kind)).to.deep.equal(['text', 'var']);
                expect(result.parts[0].text).to.equal('please ');
                expect((result.parts[1] as ParsedChatRequestVariablePart).variableArg).to.equal('summarize|this');
                expectFullCoverage(result.parts, text);
            });

            it('parses two adjacent known commands', async () => {
                const text = '/skill-one /skill-two';
                const result = await parse(text);

                expect(result.parts.map(p => p.kind)).to.deep.equal(['var', 'text', 'var']);
                expect((result.parts[0] as ParsedChatRequestVariablePart).variableArg).to.equal('skill-one');
                expect(result.parts[1].text).to.equal(' ');
                expect((result.parts[2] as ParsedChatRequestVariablePart).variableArg).to.equal('skill-two');
                expectFullCoverage(result.parts, text);
            });

            it('separates two argument-taking known commands', async () => {
                const text = '/summarize foo /explain bar';
                const result = await parse(text);

                expect(result.parts.map(p => p.kind)).to.deep.equal(['var', 'text', 'var']);
                expect((result.parts[0] as ParsedChatRequestVariablePart).variableArg).to.equal('summarize|foo');
                expect(result.parts[1].text).to.equal(' ');
                expect((result.parts[2] as ParsedChatRequestVariablePart).variableArg).to.equal('explain|bar');
                expectFullCoverage(result.parts, text);
            });

            it('parses the same command twice, each with its own argument', async () => {
                const text = '/hello Klaus /hello Maria';
                const result = await parse(text);

                expect(result.parts.map(p => p.kind)).to.deep.equal(['var', 'text', 'var']);
                expect((result.parts[0] as ParsedChatRequestVariablePart).variableArg).to.equal('hello|Klaus');
                expect(result.parts[1].text).to.equal(' ');
                expect((result.parts[2] as ParsedChatRequestVariablePart).variableArg).to.equal('hello|Maria');
                expectFullCoverage(result.parts, text);
            });

            it('parses the same command three times', async () => {
                const text = '/hello Klaus /hello Maria /hello Bob';
                const result = await parse(text);

                expect(result.parts.map(p => p.kind)).to.deep.equal(['var', 'text', 'var', 'text', 'var']);
                expect((result.parts[0] as ParsedChatRequestVariablePart).variableArg).to.equal('hello|Klaus');
                expect((result.parts[2] as ParsedChatRequestVariablePart).variableArg).to.equal('hello|Maria');
                expect((result.parts[4] as ParsedChatRequestVariablePart).variableArg).to.equal('hello|Bob');
                expectFullCoverage(result.parts, text);
            });

            it('parses the same command twice without arguments', async () => {
                const text = '/hello /hello';
                const result = await parse(text);

                expect(result.parts.map(p => p.kind)).to.deep.equal(['var', 'text', 'var']);
                expect((result.parts[0] as ParsedChatRequestVariablePart).variableArg).to.equal('hello');
                expect(result.parts[1].text).to.equal(' ');
                expect((result.parts[2] as ParsedChatRequestVariablePart).variableArg).to.equal('hello');
                expectFullCoverage(result.parts, text);
            });

            it('accepts a prompt fragment id as a command', async () => {
                const text = '/coder-system';
                const result = await parse(text);

                expect(result.parts.length).to.equal(1);
                expect((result.parts[0] as ParsedChatRequestVariablePart).variableArg).to.equal('coder-system');
            });

            it('keeps trailing whitespace out of the command part', async () => {
                const text = '/summarize   ';
                const result = await parse(text);

                expect(result.parts.map(p => p.kind)).to.deep.equal(['var', 'text']);
                expect((result.parts[0] as ParsedChatRequestVariablePart).variableArg).to.equal('summarize');
                expect(result.parts[1].text).to.equal('   ');
                expectFullCoverage(result.parts, text);
            });

            it('trims whitespace around arguments', async () => {
                const text = '/summarize \t foo \t ';
                const result = await parse(text);

                expect((result.parts[0] as ParsedChatRequestVariablePart).variableArg).to.equal('summarize|foo');
                expectFullCoverage(result.parts, text);
            });
        });

        describe('arguments do not span multiple lines', () => {
            it('does not consume the following line for a command without arguments', async () => {
                const text = '/summarize\nsecond line';
                const result = await parse(text);

                expect(result.parts.map(p => p.kind)).to.deep.equal(['var', 'text']);
                expect((result.parts[0] as ParsedChatRequestVariablePart).variableArg).to.equal('summarize');
                expect(result.parts[1].text).to.equal('\nsecond line');
                expectFullCoverage(result.parts, text);
            });

            it('does not consume the following line for a command with arguments', async () => {
                const text = '/summarize foo\nsecond line\nthird line';
                const result = await parse(text);

                expect(result.parts.map(p => p.kind)).to.deep.equal(['var', 'text']);
                expect((result.parts[0] as ParsedChatRequestVariablePart).variableArg).to.equal('summarize|foo');
                expect(result.parts[1].text).to.equal('\nsecond line\nthird line');
                expectFullCoverage(result.parts, text);
            });

            it('handles CRLF line endings', async () => {
                const text = '/summarize foo\r\nsecond line';
                const result = await parse(text);

                expect(result.parts.map(p => p.kind)).to.deep.equal(['var', 'text']);
                expect((result.parts[0] as ParsedChatRequestVariablePart).variableArg).to.equal('summarize|foo');
                expect(result.parts[1].text).to.equal('\r\nsecond line');
                expectFullCoverage(result.parts, text);
            });

            it('parses a command on a later line', async () => {
                const text = 'first line\n/summarize foo\nthird line';
                const result = await parse(text);

                expect(result.parts.map(p => p.kind)).to.deep.equal(['text', 'var', 'text']);
                expect(result.parts[0].text).to.equal('first line\n');
                expect((result.parts[1] as ParsedChatRequestVariablePart).variableArg).to.equal('summarize|foo');
                expect(result.parts[2].text).to.equal('\nthird line');
                expectFullCoverage(result.parts, text);
            });

            it('parses one command per line', async () => {
                const text = '/summarize foo\n/explain bar';
                const result = await parse(text);

                expect(result.parts.map(p => p.kind)).to.deep.equal(['var', 'text', 'var']);
                expect((result.parts[0] as ParsedChatRequestVariablePart).variableArg).to.equal('summarize|foo');
                expect(result.parts[1].text).to.equal('\n');
                expect((result.parts[2] as ParsedChatRequestVariablePart).variableArg).to.equal('explain|bar');
                expectFullCoverage(result.parts, text);
            });

            it('parses the same command on consecutive lines', async () => {
                const text = '/hello Klaus\n/hello Maria';
                const result = await parse(text);

                expect(result.parts.map(p => p.kind)).to.deep.equal(['var', 'text', 'var']);
                expect((result.parts[0] as ParsedChatRequestVariablePart).variableArg).to.equal('hello|Klaus');
                expect(result.parts[1].text).to.equal('\n');
                expect((result.parts[2] as ParsedChatRequestVariablePart).variableArg).to.equal('hello|Maria');
                expectFullCoverage(result.parts, text);
            });

            it('keeps a blank line between two commands', async () => {
                const text = '/hello Klaus\n\n/hello Maria';
                const result = await parse(text);

                expect(result.parts.map(p => p.kind)).to.deep.equal(['var', 'text', 'var']);
                expect((result.parts[0] as ParsedChatRequestVariablePart).variableArg).to.equal('hello|Klaus');
                expect(result.parts[1].text).to.equal('\n\n');
                expect((result.parts[2] as ParsedChatRequestVariablePart).variableArg).to.equal('hello|Maria');
                expectFullCoverage(result.parts, text);
            });

            it('mixes a line break with a repetition on the same line', async () => {
                const text = '/hello Klaus\n/hello Maria and /hello Bob';
                const result = await parse(text);

                expect(result.parts.map(p => p.kind)).to.deep.equal(['var', 'text', 'var', 'text', 'var']);
                expect((result.parts[0] as ParsedChatRequestVariablePart).variableArg).to.equal('hello|Klaus');
                expect(result.parts[1].text).to.equal('\n');
                expect((result.parts[2] as ParsedChatRequestVariablePart).variableArg).to.equal('hello|Maria and');
                expect(result.parts[3].text).to.equal(' ');
                expect((result.parts[4] as ParsedChatRequestVariablePart).variableArg).to.equal('hello|Bob');
                expectFullCoverage(result.parts, text);
            });

            it('keeps a multi-line paste containing paths untouched', async () => {
                await expectPlainText('here is a stack trace:\n at /usr/lib/node/foo.js:12\nplease fix it');
            });
        });

        describe('interaction with other request parts', () => {
            it('does not turn a path into a command when it follows a variable', async () => {
                const text = '#file:/path/to/file.ext look at /home/user/notes.txt';
                const result = await parse(text);

                expect(result.parts.map(p => p.kind)).to.deep.equal(['var', 'text']);
                expect((result.parts[0] as ParsedChatRequestVariablePart).variableName).to.equal('file');
                expect(result.parts[1].text).to.equal(' look at /home/user/notes.txt');
                expectFullCoverage(result.parts, text);
            });

            it('does not turn a path into a command when it follows an agent mention', async () => {
                chatAgentService.getAgents.returns([{
                    id: 'agentA',
                    name: 'agentA',
                    description: '',
                    tags: [],
                    variables: [],
                    prompts: [],
                    agentSpecificVariables: [],
                    functions: [],
                    languageModelRequirements: [],
                    locations: [ChatAgentLocation.Panel],
                    invoke: async () => undefined
                } as ChatAgent]);
                const text = '@agentA please read /etc/hosts';
                const result = await parse(text);

                expect(result.parts.map(p => p.kind)).to.deep.equal(['agent', 'text']);
                expect(result.parts[1].text).to.equal(' please read /etc/hosts');
                expectFullCoverage(result.parts, text);
            });
        });
    });

    describe('parsed chat request part kind assignments', () => {
        it('ParsedChatRequestTextPart has kind assigned at runtime', () => {
            const part = new ParsedChatRequestTextPart({ start: 0, endExclusive: 5 }, 'hello');
            expect(part.kind).to.equal('text');
        });

        it('ParsedChatRequestVariablePart has kind assigned at runtime', () => {
            const part = new ParsedChatRequestVariablePart({ start: 0, endExclusive: 5 }, 'varName', undefined);
            expect(part.kind).to.equal('var');
        });

        it('ParsedChatRequestFunctionPart has kind assigned at runtime', () => {
            const toolRequest: ToolRequest = {
                id: 'testTool',
                name: 'Test Tool',
                handler: async () => undefined,
                parameters: { type: 'object', properties: {} }
            };
            const part = new ParsedChatRequestFunctionPart({ start: 0, endExclusive: 5 }, toolRequest);
            expect(part.kind).to.equal('function');
        });

        it('ParsedChatRequestAgentPart has kind assigned at runtime', () => {
            const part = new ParsedChatRequestAgentPart({ start: 0, endExclusive: 5 }, 'agentId', 'agentName');
            expect(part.kind).to.equal('agent');
        });
    });
});
