/**
 * @license
 * Copyright 2026 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { describe, it, expect, vi, beforeEach, afterEach, } from 'vitest';
import { EventEmitter } from 'node:events';
import { awaitConfirmation, resolveConfirmation } from './confirmation.js';
import { MessageBusType, } from '../confirmation-bus/types.js';
import { ToolConfirmationOutcome, } from '../tools/tools.js';
import { randomUUID } from 'node:crypto';
import { fireToolNotificationHook } from '../core/coreToolHookTriggers.js';
// Mock Dependencies
vi.mock('node:crypto', () => ({
    randomUUID: vi.fn(),
}));
vi.mock('../core/coreToolHookTriggers.js', () => ({
    fireToolNotificationHook: vi.fn(),
}));
describe('confirmation.ts', () => {
    let mockMessageBus;
    beforeEach(() => {
        mockMessageBus = new EventEmitter();
        mockMessageBus.publish = vi.fn().mockResolvedValue(undefined);
        vi.spyOn(mockMessageBus, 'on');
        vi.spyOn(mockMessageBus, 'removeListener');
        vi.mocked(randomUUID).mockReturnValue('123e4567-e89b-12d3-a456-426614174000');
    });
    afterEach(() => {
        vi.clearAllMocks();
    });
    const emitResponse = (response) => {
        mockMessageBus.emit(MessageBusType.TOOL_CONFIRMATION_RESPONSE, response);
    };
    /**
     * Helper to wait for a listener to be attached to the bus.
     * This is more robust than setTimeout for synchronizing with the async iterator.
     */
    const waitForListener = (eventName) => new Promise((resolve) => {
        const handler = (event) => {
            if (event === eventName) {
                mockMessageBus.off('newListener', handler);
                resolve();
            }
        };
        mockMessageBus.on('newListener', handler);
    });
    describe('awaitConfirmation', () => {
        it('should resolve when confirmed response matches correlationId', async () => {
            const correlationId = 'test-correlation-id';
            const abortController = new AbortController();
            const promise = awaitConfirmation(mockMessageBus, correlationId, abortController.signal);
            emitResponse({
                type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
                correlationId,
                confirmed: true,
            });
            const result = await promise;
            expect(result).toEqual({
                outcome: ToolConfirmationOutcome.ProceedOnce,
                payload: undefined,
            });
        });
        it('should reject when abort signal is triggered', async () => {
            const correlationId = 'abort-id';
            const abortController = new AbortController();
            const promise = awaitConfirmation(mockMessageBus, correlationId, abortController.signal);
            abortController.abort();
            await expect(promise).rejects.toThrow('Operation cancelled');
        });
    });
    describe('resolveConfirmation', () => {
        let mockState;
        let mockModifier;
        let mockConfig;
        let getPreferredEditor;
        let signal;
        let toolCall;
        let invocationMock;
        let toolMock;
        beforeEach(() => {
            signal = new AbortController().signal;
            mockState = {
                getToolCall: vi.fn(),
                updateStatus: vi.fn(),
                updateArgs: vi.fn(),
            };
            // Mock accessors via defineProperty
            Object.defineProperty(mockState, 'firstActiveCall', {
                get: vi.fn(),
                configurable: true,
            });
            mockModifier = {
                handleModifyWithEditor: vi.fn(),
                applyInlineModify: vi.fn(),
            };
            mockConfig = {
                getEnableHooks: vi.fn().mockReturnValue(true),
            };
            getPreferredEditor = vi.fn().mockReturnValue('vim');
            invocationMock = {
                shouldConfirmExecute: vi.fn(),
            };
            toolMock = {
                build: vi.fn(),
            };
            toolCall = {
                status: 'validating',
                request: {
                    callId: 'call-1',
                    name: 'tool',
                    args: {},
                    isClientInitiated: false,
                    prompt_id: 'prompt-1',
                },
                invocation: invocationMock,
                tool: toolMock,
            };
            // Default: state returns the current call
            mockState.getToolCall.mockReturnValue(toolCall);
            // Default: define firstActiveCall for modifiers
            vi.spyOn(mockState, 'firstActiveCall', 'get').mockReturnValue(toolCall);
        });
        it('should return ProceedOnce immediately if no confirmation needed', async () => {
            invocationMock.shouldConfirmExecute.mockResolvedValue(false);
            const result = await resolveConfirmation(toolCall, signal, {
                config: mockConfig,
                messageBus: mockMessageBus,
                state: mockState,
                modifier: mockModifier,
                getPreferredEditor,
            });
            expect(result.outcome).toBe(ToolConfirmationOutcome.ProceedOnce);
            expect(mockState.updateStatus).not.toHaveBeenCalledWith(expect.anything(), 'awaiting_approval', expect.anything());
        });
        it('should return ProceedOnce after successful user confirmation', async () => {
            const details = {
                type: 'info',
                prompt: 'Confirm?',
                title: 'Title',
                onConfirm: vi.fn(),
            };
            invocationMock.shouldConfirmExecute.mockResolvedValue(details);
            // Wait for listener to attach
            const listenerPromise = waitForListener(MessageBusType.TOOL_CONFIRMATION_RESPONSE);
            const promise = resolveConfirmation(toolCall, signal, {
                config: mockConfig,
                messageBus: mockMessageBus,
                state: mockState,
                modifier: mockModifier,
                getPreferredEditor,
            });
            await listenerPromise;
            emitResponse({
                type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
                correlationId: '123e4567-e89b-12d3-a456-426614174000',
                confirmed: true,
            });
            const result = await promise;
            expect(result.outcome).toBe(ToolConfirmationOutcome.ProceedOnce);
            expect(mockState.updateStatus).toHaveBeenCalledWith('call-1', 'awaiting_approval', expect.objectContaining({
                correlationId: '123e4567-e89b-12d3-a456-426614174000',
            }));
        });
        it('should fire hooks if enabled', async () => {
            const details = {
                type: 'info',
                prompt: 'Confirm?',
                title: 'Title',
                onConfirm: vi.fn(),
            };
            invocationMock.shouldConfirmExecute.mockResolvedValue(details);
            const promise = resolveConfirmation(toolCall, signal, {
                config: mockConfig,
                messageBus: mockMessageBus,
                state: mockState,
                modifier: mockModifier,
                getPreferredEditor,
            });
            await waitForListener(MessageBusType.TOOL_CONFIRMATION_RESPONSE);
            emitResponse({
                type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
                correlationId: '123e4567-e89b-12d3-a456-426614174000',
                confirmed: true,
            });
            await promise;
            expect(fireToolNotificationHook).toHaveBeenCalledWith(mockMessageBus, expect.objectContaining({
                type: details.type,
                prompt: details.prompt,
                title: details.title,
            }));
        });
        it('should handle ModifyWithEditor loop', async () => {
            const details = {
                type: 'info',
                prompt: 'Confirm?',
                title: 'Title',
                onConfirm: vi.fn(),
            };
            invocationMock.shouldConfirmExecute.mockResolvedValue(details);
            // 1. User says Modify
            // 2. User says Proceed
            const listenerPromise1 = waitForListener(MessageBusType.TOOL_CONFIRMATION_RESPONSE);
            const promise = resolveConfirmation(toolCall, signal, {
                config: mockConfig,
                messageBus: mockMessageBus,
                state: mockState,
                modifier: mockModifier,
                getPreferredEditor,
            });
            await listenerPromise1;
            // First response: Modify
            emitResponse({
                type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
                correlationId: '123e4567-e89b-12d3-a456-426614174000',
                confirmed: true,
                outcome: ToolConfirmationOutcome.ModifyWithEditor,
            });
            // Mock the modifier action
            mockModifier.handleModifyWithEditor.mockResolvedValue({
                updatedParams: { foo: 'bar' },
            });
            toolMock.build.mockReturnValue({});
            // Wait for loop to cycle and re-subscribe
            const listenerPromise2 = waitForListener(MessageBusType.TOOL_CONFIRMATION_RESPONSE);
            await listenerPromise2;
            // Expect state update
            expect(mockState.updateArgs).toHaveBeenCalled();
            // Second response: Proceed
            emitResponse({
                type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
                correlationId: '123e4567-e89b-12d3-a456-426614174000',
                confirmed: true,
                outcome: ToolConfirmationOutcome.ProceedOnce,
            });
            const result = await promise;
            expect(result.outcome).toBe(ToolConfirmationOutcome.ProceedOnce);
            expect(mockModifier.handleModifyWithEditor).toHaveBeenCalled();
        });
        it('should handle inline modification (payload)', async () => {
            const details = {
                type: 'info',
                prompt: 'Confirm?',
                title: 'Title',
                onConfirm: vi.fn(),
            };
            invocationMock.shouldConfirmExecute.mockResolvedValue(details);
            const listenerPromise = waitForListener(MessageBusType.TOOL_CONFIRMATION_RESPONSE);
            const promise = resolveConfirmation(toolCall, signal, {
                config: mockConfig,
                messageBus: mockMessageBus,
                state: mockState,
                modifier: mockModifier,
                getPreferredEditor,
            });
            await listenerPromise;
            // Response with payload
            emitResponse({
                type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
                correlationId: '123e4567-e89b-12d3-a456-426614174000',
                confirmed: true,
                outcome: ToolConfirmationOutcome.ProceedOnce, // Ignored if payload present
                payload: { newContent: 'inline' },
            });
            mockModifier.applyInlineModify.mockResolvedValue({
                updatedParams: { inline: 'true' },
            });
            toolMock.build.mockReturnValue({});
            const result = await promise;
            expect(result.outcome).toBe(ToolConfirmationOutcome.ProceedOnce);
            expect(mockModifier.applyInlineModify).toHaveBeenCalled();
            expect(mockState.updateArgs).toHaveBeenCalled();
        });
        it('should resolve immediately if IDE confirmation resolves first', async () => {
            const idePromise = Promise.resolve({
                status: 'accepted',
                content: 'ide-content',
            });
            const details = {
                type: 'info',
                prompt: 'Confirm?',
                title: 'Title',
                onConfirm: vi.fn(),
                ideConfirmation: idePromise,
            };
            invocationMock.shouldConfirmExecute.mockResolvedValue(details);
            // We don't strictly need to wait for the listener because the race might finish instantly
            const promise = resolveConfirmation(toolCall, signal, {
                config: mockConfig,
                messageBus: mockMessageBus,
                state: mockState,
                modifier: mockModifier,
                getPreferredEditor,
            });
            const result = await promise;
            expect(result.outcome).toBe(ToolConfirmationOutcome.ProceedOnce);
        });
        it('should throw if tool call is lost from state during loop', async () => {
            invocationMock.shouldConfirmExecute.mockResolvedValue({
                type: 'info',
                title: 'Title',
                onConfirm: vi.fn(),
                prompt: 'Prompt',
            });
            // Simulate state losing the call (undefined)
            mockState.getToolCall.mockReturnValue(undefined);
            await expect(resolveConfirmation(toolCall, signal, {
                config: mockConfig,
                messageBus: mockMessageBus,
                state: mockState,
                modifier: mockModifier,
                getPreferredEditor,
            })).rejects.toThrow(/lost during confirmation loop/);
        });
    });
});
//# sourceMappingURL=confirmation.test.js.map