import { jsx as _jsx } from "react/jsx-runtime";
/**
 * @license
 * Copyright 2026 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { vi } from 'vitest';
import { act } from 'react';
import stripAnsi from 'strip-ansi';
import os from 'node:os';
import path from 'node:path';
import fs from 'node:fs';
import { AppContainer } from '../ui/AppContainer.js';
import { renderWithProviders } from './render.js';
import { makeFakeConfig, ExtensionLoader, AuthType, ApprovalMode, createPolicyEngineConfig, PolicyDecision, ToolConfirmationOutcome, MessageBusType, coreEvents, ideContextStore, createContentGenerator, IdeClient, debugLogger, CoreToolCallStatus, IntegrityDataStatus, } from '@google/gemini-cli-core';
import { MockShellExecutionService, } from './MockShellExecutionService.js';
import { createMockSettings } from './settings.js';
import { resetSettingsCacheForTesting, } from '../config/settings.js';
import { AuthState, StreamingState } from '../ui/types.js';
import { randomUUID } from 'node:crypto';
// Global state observer for React-based signals
const sessionStateMap = new Map();
const activeRigs = new Map();
// Mock StreamingContext to report state changes back to the observer
vi.mock('../ui/contexts/StreamingContext.js', async (importOriginal) => {
    const original = await importOriginal();
    const { useConfig } = await import('../ui/contexts/ConfigContext.js');
    const React = await import('react');
    return {
        ...original,
        useStreamingContext: () => {
            const state = original.useStreamingContext();
            const config = useConfig();
            const sessionId = config.getSessionId();
            React.useEffect(() => {
                sessionStateMap.set(sessionId, state);
                // If we see activity, we are no longer "awaiting" the start of a response
                if (state !== StreamingState.Idle) {
                    const rig = activeRigs.get(sessionId);
                    if (rig) {
                        rig.awaitingResponse = false;
                    }
                }
            }, [sessionId, state]);
            return state;
        },
    };
});
// Mock core functions globally for tests using AppRig.
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
    const original = await importOriginal();
    const { MockShellExecutionService: MockService } = await import('./MockShellExecutionService.js');
    // Register the real execution logic so MockShellExecutionService can fall back to it
    MockService.setOriginalImplementation(original.ShellExecutionService.execute);
    return {
        ...original,
        ShellExecutionService: MockService,
    };
});
// Mock useAuthCommand to bypass authentication flows in tests
vi.mock('../ui/auth/useAuth.js', () => ({
    useAuthCommand: () => ({
        authState: AuthState.Authenticated,
        setAuthState: vi.fn(),
        authError: null,
        onAuthError: vi.fn(),
        apiKeyDefaultValue: 'test-api-key',
        reloadApiKey: vi.fn().mockResolvedValue('test-api-key'),
        accountSuspensionInfo: null,
        setAccountSuspensionInfo: vi.fn(),
    }),
    validateAuthMethodWithSettings: () => null,
}));
// A minimal mock ExtensionManager to satisfy AppContainer's forceful cast
class MockExtensionManager extends ExtensionLoader {
    getExtensions = vi.fn().mockReturnValue([]);
    setRequestConsent = vi.fn();
    setRequestSetting = vi.fn();
    integrityManager = {
        verifyExtensionIntegrity: vi
            .fn()
            .mockResolvedValue(IntegrityDataStatus.VERIFIED),
        storeExtensionIntegrity: vi.fn().mockResolvedValue(undefined),
    };
}
// Mock GeminiRespondingSpinner to disable animations (avoiding 'act()' warnings) without triggering screen reader mode.
vi.mock('../ui/components/GeminiRespondingSpinner.js', async () => {
    const React = await import('react');
    const { Text } = await import('ink');
    return {
        GeminiSpinner: () => React.createElement(Text, null, '...'),
        GeminiRespondingSpinner: ({ nonRespondingDisplay, }) => React.createElement(Text, null, nonRespondingDisplay || '...'),
    };
});
export class AppRig {
    options;
    renderResult;
    config;
    settings;
    testDir;
    sessionId;
    pendingConfirmations = new Map();
    breakpointTools = new Set();
    lastAwaitedConfirmation;
    /**
     * True if a message was just sent but React hasn't yet reported a non-idle state.
     */
    awaitingResponse = false;
    constructor(options = {}) {
        this.options = options;
        const uniqueId = randomUUID();
        this.testDir = fs.mkdtempSync(path.join(os.tmpdir(), `gemini-app-rig-${uniqueId.slice(0, 8)}-`));
        this.sessionId = `test-session-${uniqueId}`;
        activeRigs.set(this.sessionId, this);
    }
    async initialize() {
        this.setupEnvironment();
        resetSettingsCacheForTesting();
        this.settings = this.createRigSettings();
        const approvalMode = this.options.configOverrides?.approvalMode ?? ApprovalMode.DEFAULT;
        const policyEngineConfig = await createPolicyEngineConfig(this.settings.merged, approvalMode);
        const configParams = {
            sessionId: this.sessionId,
            targetDir: this.testDir,
            cwd: this.testDir,
            debugMode: false,
            model: 'test-model',
            fakeResponses: this.options.fakeResponsesPath,
            interactive: true,
            approvalMode,
            policyEngineConfig,
            enableEventDrivenScheduler: true,
            extensionLoader: new MockExtensionManager(),
            excludeTools: this.options.configOverrides?.excludeTools,
            ...this.options.configOverrides,
        };
        this.config = makeFakeConfig(configParams);
        if (this.options.fakeResponsesPath) {
            this.stubRefreshAuth();
        }
        this.setupMessageBusListeners();
        await act(async () => {
            await this.config.initialize();
            // Since we mocked useAuthCommand, we must manually trigger the first
            // refreshAuth to ensure contentGenerator is initialized.
            await this.config.refreshAuth(AuthType.USE_GEMINI);
        });
    }
    setupEnvironment() {
        // Stub environment variables to avoid interference from developer's machine
        vi.stubEnv('GEMINI_CLI_HOME', this.testDir);
        if (this.options.fakeResponsesPath) {
            vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
            MockShellExecutionService.setPassthrough(false);
        }
        else {
            if (!process.env['GEMINI_API_KEY']) {
                throw new Error('GEMINI_API_KEY must be set in the environment for live model tests.');
            }
            // For live tests, we allow falling through to the real shell service if no mock matches
            MockShellExecutionService.setPassthrough(true);
        }
        vi.stubEnv('GEMINI_DEFAULT_AUTH_TYPE', AuthType.USE_GEMINI);
    }
    createRigSettings() {
        return createMockSettings({
            user: {
                path: path.join(this.testDir, '.gemini', 'user_settings.json'),
                settings: {
                    security: {
                        auth: {
                            selectedType: AuthType.USE_GEMINI,
                            useExternal: true,
                        },
                        folderTrust: {
                            enabled: true,
                        },
                    },
                    ide: {
                        enabled: false,
                        hasSeenNudge: true,
                    },
                },
                originalSettings: {},
            },
            merged: {
                security: {
                    auth: {
                        selectedType: AuthType.USE_GEMINI,
                        useExternal: true,
                    },
                    folderTrust: {
                        enabled: true,
                    },
                },
                ide: {
                    enabled: false,
                    hasSeenNudge: true,
                },
            },
        });
    }
    stubRefreshAuth() {
        // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
        const gcConfig = this.config;
        gcConfig.refreshAuth = async (authMethod) => {
            gcConfig.modelAvailabilityService.reset();
            const newContentGeneratorConfig = {
                authType: authMethod,
                // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
                proxy: gcConfig.getProxy(),
                apiKey: process.env['GEMINI_API_KEY'] || 'test-api-key',
            };
            gcConfig.contentGenerator = await createContentGenerator(newContentGeneratorConfig, this.config, gcConfig.getSessionId());
            gcConfig.contentGeneratorConfig = newContentGeneratorConfig;
            // Initialize BaseLlmClient now that the ContentGenerator is available
            const { BaseLlmClient } = await import('@google/gemini-cli-core');
            gcConfig.baseLlmClient = new BaseLlmClient(gcConfig.contentGenerator, this.config);
        };
    }
    toolCalls = [];
    setupMessageBusListeners() {
        if (!this.config)
            return;
        const messageBus = this.config.getMessageBus();
        messageBus.subscribe(MessageBusType.TOOL_CALLS_UPDATE, (message) => {
            this.toolCalls = message.toolCalls;
            for (const call of message.toolCalls) {
                if (call.status === 'awaiting_approval' && call.correlationId) {
                    const details = call.confirmationDetails;
                    const title = 'title' in details ? details.title : '';
                    const toolDisplayName = call.tool?.displayName || title.replace(/^Confirm:\s*/, '');
                    if (!this.pendingConfirmations.has(call.correlationId)) {
                        this.pendingConfirmations.set(call.correlationId, {
                            toolName: call.request.name,
                            toolDisplayName,
                            correlationId: call.correlationId,
                        });
                    }
                }
                else if (call.status !== 'awaiting_approval') {
                    for (const [correlationId, pending,] of this.pendingConfirmations.entries()) {
                        if (pending.toolName === call.request.name) {
                            this.pendingConfirmations.delete(correlationId);
                            break;
                        }
                    }
                }
            }
        });
    }
    /**
     * Returns true if the agent is currently busy (responding or executing tools).
     */
    isBusy() {
        if (this.awaitingResponse) {
            return true;
        }
        const reactState = sessionStateMap.get(this.sessionId);
        // If we have a React-based state, use it as the definitive signal.
        // 'responding' and 'waiting-for-confirmation' both count as busy for the overall task.
        if (reactState !== undefined) {
            return reactState !== StreamingState.Idle;
        }
        // Fallback to tool tracking if React hasn't reported yet
        const isAnyToolActive = this.toolCalls.some((tc) => {
            if (tc.status === CoreToolCallStatus.Executing ||
                tc.status === CoreToolCallStatus.Scheduled ||
                tc.status === CoreToolCallStatus.Validating) {
                return true;
            }
            if (tc.status === CoreToolCallStatus.Success ||
                tc.status === CoreToolCallStatus.Error ||
                tc.status === CoreToolCallStatus.Cancelled) {
                return !tc
                    .responseSubmittedToGemini;
            }
            return false;
        });
        const isAwaitingConfirmation = this.toolCalls.some((tc) => tc.status === CoreToolCallStatus.AwaitingApproval);
        return isAnyToolActive || isAwaitingConfirmation;
    }
    render() {
        if (!this.config || !this.settings)
            throw new Error('AppRig not initialized');
        act(() => {
            this.renderResult = renderWithProviders(_jsx(AppContainer, { config: this.config, version: "test-version", initializationResult: {
                    authError: null,
                    accountSuspensionInfo: null,
                    themeError: null,
                    shouldOpenAuthDialog: false,
                    geminiMdFileCount: 0,
                } }), {
                config: this.config,
                settings: this.settings,
                width: this.options.terminalWidth ?? 120,
                useAlternateBuffer: false,
                uiState: {
                    terminalHeight: this.options.terminalHeight ?? 40,
                },
            });
        });
    }
    setMockCommands(commands) {
        MockShellExecutionService.setMockCommands(commands);
    }
    setToolPolicy(toolName, decision, priority = 10) {
        if (!this.config)
            throw new Error('AppRig not initialized');
        this.config.getPolicyEngine().addRule({
            toolName,
            decision,
            priority,
            source: 'AppRig Override',
        });
    }
    setBreakpoint(toolName) {
        if (Array.isArray(toolName)) {
            for (const name of toolName) {
                this.setBreakpoint(name);
            }
        }
        else {
            // Use undefined toolName to create a global rule if '*' is provided
            const actualToolName = toolName === '*' ? undefined : toolName;
            this.setToolPolicy(actualToolName, PolicyDecision.ASK_USER, 100);
            this.breakpointTools.add(toolName);
        }
    }
    removeToolPolicy(toolName, source = 'AppRig Override') {
        if (!this.config)
            throw new Error('AppRig not initialized');
        // Map '*' back to undefined for policy removal
        const actualToolName = toolName === '*' ? undefined : toolName;
        this.config
            .getPolicyEngine()
            // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
            .removeRulesForTool(actualToolName, source);
        this.breakpointTools.delete(toolName);
    }
    getTestDir() {
        return this.testDir;
    }
    getPendingConfirmations() {
        return Array.from(this.pendingConfirmations.values());
    }
    async waitUntil(predicate, options = {}) {
        const { timeout = 30000, interval = 100, message = 'Condition timed out', } = options;
        const start = Date.now();
        while (true) {
            if (await predicate())
                return;
            if (Date.now() - start > timeout) {
                throw new Error(message);
            }
            await act(async () => {
                await new Promise((resolve) => setTimeout(resolve, interval));
            });
        }
    }
    async waitForPendingConfirmation(toolNameOrDisplayName, timeout = 30000) {
        const matches = (p) => {
            if (!toolNameOrDisplayName)
                return true;
            if (typeof toolNameOrDisplayName === 'string') {
                return (p.toolName === toolNameOrDisplayName ||
                    p.toolDisplayName === toolNameOrDisplayName);
            }
            if (Array.isArray(toolNameOrDisplayName)) {
                return (toolNameOrDisplayName.includes(p.toolName) ||
                    toolNameOrDisplayName.includes(p.toolDisplayName || ''));
            }
            return (toolNameOrDisplayName.test(p.toolName) ||
                toolNameOrDisplayName.test(p.toolDisplayName || ''));
        };
        let matched;
        await this.waitUntil(() => {
            matched = this.getPendingConfirmations().find(matches);
            return !!matched;
        }, {
            timeout,
            message: `Timed out waiting for pending confirmation: ${toolNameOrDisplayName || 'any'}. Current pending: ${this.getPendingConfirmations()
                .map((p) => p.toolName)
                .join(', ')}`,
        });
        this.lastAwaitedConfirmation = matched;
        return matched;
    }
    /**
     * Waits for either a tool confirmation request OR for the agent to go idle.
     */
    async waitForNextEvent(timeout = 60000) {
        let confirmation;
        let isIdle = false;
        await this.waitUntil(async () => {
            await act(async () => {
                await new Promise((resolve) => setTimeout(resolve, 0));
            });
            confirmation = this.getPendingConfirmations()[0];
            // Now that we have a code-powered signal, this should be perfectly deterministic.
            isIdle = !this.isBusy();
            return !!confirmation || isIdle;
        }, {
            timeout,
            message: 'Timed out waiting for next event (confirmation or idle).',
        });
        if (confirmation) {
            this.lastAwaitedConfirmation = confirmation;
            return { type: 'confirmation', confirmation };
        }
        // Ensure all renders are flushed before returning 'idle'
        await this.renderResult?.waitUntilReady();
        return { type: 'idle' };
    }
    async resolveTool(toolNameOrDisplayName, outcome = ToolConfirmationOutcome.ProceedOnce) {
        if (!this.config)
            throw new Error('AppRig not initialized');
        const messageBus = this.config.getMessageBus();
        let pending;
        if (typeof toolNameOrDisplayName === 'object' &&
            'correlationId' in toolNameOrDisplayName) {
            pending = toolNameOrDisplayName;
        }
        else {
            pending = await this.waitForPendingConfirmation(toolNameOrDisplayName);
        }
        await act(async () => {
            this.pendingConfirmations.delete(pending.correlationId);
            if (this.breakpointTools.has(pending.toolName)) {
                this.removeToolPolicy(pending.toolName);
            }
            // eslint-disable-next-line @typescript-eslint/no-floating-promises
            messageBus.publish({
                type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
                correlationId: pending.correlationId,
                confirmed: outcome !== ToolConfirmationOutcome.Cancel,
                outcome,
            });
        });
        await act(async () => {
            await new Promise((resolve) => setTimeout(resolve, 100));
        });
    }
    async resolveAwaitedTool(outcome = ToolConfirmationOutcome.ProceedOnce) {
        if (!this.lastAwaitedConfirmation) {
            throw new Error('No tool has been awaited yet');
        }
        await this.resolveTool(this.lastAwaitedConfirmation, outcome);
        this.lastAwaitedConfirmation = undefined;
    }
    async addUserHint(hint) {
        if (!this.config)
            throw new Error('AppRig not initialized');
        await act(async () => {
            this.config.injectionService.addInjection(hint, 'user_steering');
        });
    }
    /**
     * Drains all pending tool calls that hit a breakpoint until the agent is idle.
     * Useful for negative tests to ensure no unwanted tools (like generalist) are called.
     *
     * @param onConfirmation Optional callback to inspect each confirmation before resolving.
     *                       Return true to skip the default resolveTool call (e.g. if you handled it).
     */
    async drainBreakpointsUntilIdle(onConfirmation, timeout = 60000) {
        while (true) {
            const event = await this.waitForNextEvent(timeout);
            if (event.type === 'idle') {
                break;
            }
            const confirmation = event.confirmation;
            const handled = onConfirmation?.(confirmation);
            if (!handled) {
                await this.resolveTool(confirmation);
            }
        }
    }
    getConfig() {
        if (!this.config)
            throw new Error('AppRig not initialized');
        return this.config;
    }
    async type(text) {
        if (!this.renderResult)
            throw new Error('AppRig not initialized');
        await act(async () => {
            this.renderResult.stdin.write(text);
        });
        await act(async () => {
            await new Promise((resolve) => setTimeout(resolve, 50));
        });
    }
    async pressEnter() {
        await this.type('\r');
    }
    async pressKey(key) {
        if (!this.renderResult)
            throw new Error('AppRig not initialized');
        await act(async () => {
            this.renderResult.stdin.write(key);
        });
        await act(async () => {
            await new Promise((resolve) => setTimeout(resolve, 50));
        });
    }
    get lastFrame() {
        if (!this.renderResult)
            return '';
        return stripAnsi(this.renderResult.lastFrame({ allowEmpty: true }) || '');
    }
    getStaticOutput() {
        if (!this.renderResult)
            return '';
        return stripAnsi(this.renderResult.stdout.lastFrame() || '');
    }
    async waitForOutput(pattern, timeout = 30000) {
        await this.waitUntil(() => {
            const frame = this.lastFrame;
            return typeof pattern === 'string'
                ? frame.includes(pattern)
                : pattern.test(frame);
        }, {
            timeout,
            message: `Timed out waiting for output: ${pattern}\nLast frame:\n${this.lastFrame}`,
        });
    }
    async waitForIdle(timeout = 20000) {
        await this.waitForOutput('Type your message', timeout);
    }
    async sendMessage(text) {
        this.awaitingResponse = true;
        await this.type(text);
        await this.pressEnter();
    }
    async unmount() {
        // Clean up global state for this session
        sessionStateMap.delete(this.sessionId);
        activeRigs.delete(this.sessionId);
        // Poison the chat recording service to prevent late writes to the test directory
        if (this.config) {
            const recordingService = this.config
                .getGeminiClient()
                ?.getChatRecordingService();
            if (recordingService) {
                // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion
                recordingService.conversationFile = null;
            }
        }
        if (this.renderResult) {
            this.renderResult.unmount();
        }
        await act(async () => {
            await new Promise((resolve) => setTimeout(resolve, 500));
        });
        vi.unstubAllEnvs();
        coreEvents.removeAllListeners();
        coreEvents.drainBacklogs();
        MockShellExecutionService.reset();
        ideContextStore.clear();
        // Forcefully clear IdeClient singleton promise
        // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion
        IdeClient.instancePromise = null;
        vi.clearAllMocks();
        this.config = undefined;
        this.renderResult = undefined;
        if (this.testDir && fs.existsSync(this.testDir)) {
            try {
                fs.rmSync(this.testDir, { recursive: true, force: true });
            }
            catch (e) {
                debugLogger.warn(`Failed to cleanup test directory ${this.testDir}:`, e);
            }
        }
    }
}
//# sourceMappingURL=AppRig.js.map