/**
 * @license
 * Copyright 2026 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import {
  describe,
  it,
  expect,
  vi,
  beforeEach,
  afterEach,
  type Mock,
  type MockedObject,
} from 'vitest';
import { render, cleanup, persistentStateMock } from '../test-utils/render.js';
import { waitFor } from '../test-utils/async.js';
import { act, useContext } from 'react';
import { AppContainer } from './AppContainer.js';
import { SettingsContext } from './contexts/SettingsContext.js';
import { type TrackedToolCall } from './hooks/useToolScheduler.js';
import {
  type Config,
  makeFakeConfig,
  CoreEvent,
  type UserFeedbackPayload,
  type ResumedSessionData,
  type StartupWarning,
  WarningPriority,
  AuthType,
  type AgentDefinition,
  CoreToolCallStatus,
} from '@google/gemini-cli-core';

// Mock coreEvents
const mockCoreEvents = vi.hoisted(() => ({
  on: vi.fn(),
  off: vi.fn(),
  drainBacklogs: vi.fn(),
  emit: vi.fn(),
}));

// Mock IdeClient
const mockIdeClient = vi.hoisted(() => ({
  getInstance: vi.fn().mockReturnValue(new Promise(() => {})),
}));

// Mock stdout
const mocks = vi.hoisted(() => ({
  mockStdout: { write: vi.fn() },
}));
const terminalNotificationsMocks = vi.hoisted(() => ({
  notifyViaTerminal: vi.fn().mockResolvedValue(true),
  isNotificationsEnabled: vi.fn(() => true),
  getNotificationMethod: vi.fn(() => 'auto'),
  buildRunEventNotificationContent: vi.fn((event) => ({
    title: 'Mock Notification',
    subtitle: 'Mock Subtitle',
    body: JSON.stringify(event),
  })),
}));

vi.mock('@google/gemini-cli-core', async (importOriginal) => {
  const actual =
    await importOriginal<typeof import('@google/gemini-cli-core')>();
  return {
    ...actual,
    coreEvents: mockCoreEvents,
    IdeClient: mockIdeClient,
    writeToStdout: vi.fn((...args) =>
      process.stdout.write(
        ...(args as Parameters<typeof process.stdout.write>),
      ),
    ),
    writeToStderr: vi.fn((...args) =>
      process.stderr.write(
        ...(args as Parameters<typeof process.stderr.write>),
      ),
    ),
    patchStdio: vi.fn(() => () => {}),
    createWorkingStdio: vi.fn(() => ({
      stdout: process.stdout,
      stderr: process.stderr,
    })),
    enableMouseEvents: vi.fn(),
    disableMouseEvents: vi.fn(),
    FileDiscoveryService: vi.fn().mockImplementation(() => ({
      initialize: vi.fn(),
    })),
    startupProfiler: {
      flush: vi.fn(),
      start: vi.fn(),
      end: vi.fn(),
    },
  };
});
import ansiEscapes from 'ansi-escapes';
import { type LoadedSettings } from '../config/settings.js';
import { createMockSettings } from '../test-utils/settings.js';
import type { InitializationResult } from '../core/initializer.js';
import { useQuotaAndFallback } from './hooks/useQuotaAndFallback.js';
import { StreamingState, MessageType } from './types.js';
import { UIStateContext, type UIState } from './contexts/UIStateContext.js';
import {
  UIActionsContext,
  type UIActions,
} from './contexts/UIActionsContext.js';
import { KeypressProvider } from './contexts/KeypressContext.js';
import { OverflowProvider } from './contexts/OverflowContext.js';
import {
  useOverflowActions,
  type OverflowActions,
} from './contexts/OverflowContext.js';

// Mock useStdout to capture terminal title writes
vi.mock('ink', async (importOriginal) => {
  const actual = await importOriginal<typeof import('ink')>();
  return {
    ...actual,
    useStdout: () => ({ stdout: mocks.mockStdout }),
    measureElement: vi.fn(),
  };
});

import { InputContext, type InputState } from './contexts/InputContext.js';
import { QuotaContext, type QuotaState } from './contexts/QuotaContext.js';

// Helper component will read the context values provided by AppContainer
// so we can assert against them in our tests.
let capturedUIState: UIState;
let capturedInputState: InputState;
let capturedQuotaState: QuotaState;
let capturedUIActions: UIActions;
let capturedOverflowActions: OverflowActions;
function TestContextConsumer() {
  capturedUIState = useContext(UIStateContext)!;
  capturedInputState = useContext(InputContext)!;
  capturedQuotaState = useContext(QuotaContext)!;
  capturedUIActions = useContext(UIActionsContext)!;
  capturedOverflowActions = useOverflowActions()!;
  return null;
}

vi.mock('./App.js', () => ({
  App: TestContextConsumer,
}));

vi.mock('./hooks/useQuotaAndFallback.js');
vi.mock('./hooks/useHistoryManager.js');
vi.mock('./hooks/useThemeCommand.js');
vi.mock('./auth/useAuth.js');
vi.mock('../config/auth.js', () => ({
  validateAuthMethod: vi.fn().mockResolvedValue(null),
}));
vi.mock('./hooks/useEditorSettings.js');
vi.mock('./hooks/useSettingsCommand.js');
vi.mock('./hooks/useModelCommand.js');
vi.mock('./hooks/slashCommandProcessor.js');
vi.mock('./hooks/useConsoleMessages.js');
vi.mock('./hooks/useTerminalSize.js', () => ({
  useTerminalSize: vi.fn(() => ({ columns: 80, rows: 24 })),
}));
vi.mock('./hooks/useGeminiStream.js');
vi.mock('./hooks/vim.js');
vi.mock('./hooks/useFocus.js');
vi.mock('./hooks/useBracketedPaste.js');
vi.mock('./hooks/useLoadingIndicator.js');
vi.mock('./hooks/useSuspend.js');
vi.mock('./hooks/useFolderTrust.js');
vi.mock('./hooks/useIdeTrustListener.js');
vi.mock('./hooks/useMessageQueue.js');
vi.mock('./hooks/useApprovalModeIndicator.js');
vi.mock('./hooks/useGitBranchName.js');
vi.mock('./hooks/useExtensionUpdates.js');
vi.mock('./contexts/VimModeContext.js');
vi.mock('./contexts/SessionContext.js');
vi.mock('./components/shared/text-buffer.js');
vi.mock('./hooks/useLogger.js');
vi.mock('./hooks/useInputHistoryStore.js');
vi.mock('./hooks/atCommandProcessor.js');
vi.mock('./hooks/useHookDisplayState.js');
vi.mock('./hooks/useBanner.js', () => ({
  useBanner: vi.fn((bannerData) => ({
    bannerText: (
      bannerData.warningText ||
      bannerData.defaultText ||
      ''
    ).replace(/\\n/g, '\n'),
  })),
}));
vi.mock('./hooks/useShellInactivityStatus.js', () => ({
  useShellInactivityStatus: vi.fn(() => ({
    shouldShowFocusHint: false,
    inactivityStatus: 'none',
  })),
}));
vi.mock('../utils/terminalNotifications.js', () => ({
  notifyViaTerminal: terminalNotificationsMocks.notifyViaTerminal,
  isNotificationsEnabled: terminalNotificationsMocks.isNotificationsEnabled,
  getNotificationMethod: terminalNotificationsMocks.getNotificationMethod,
  buildRunEventNotificationContent:
    terminalNotificationsMocks.buildRunEventNotificationContent,
}));
vi.mock('./hooks/useTerminalTheme.js', () => ({
  useTerminalTheme: vi.fn(),
}));

import { useHookDisplayState } from './hooks/useHookDisplayState.js';
import { useTerminalTheme } from './hooks/useTerminalTheme.js';
import { useShellInactivityStatus } from './hooks/useShellInactivityStatus.js';
import { useFocus } from './hooks/useFocus.js';

// Mock external utilities
vi.mock('../utils/events.js');
vi.mock('../utils/handleAutoUpdate.js');
vi.mock('./utils/ConsolePatcher.js');
vi.mock('../utils/cleanup.js');

import { useHistory } from './hooks/useHistoryManager.js';
import { useThemeCommand } from './hooks/useThemeCommand.js';
import { useAuthCommand } from './auth/useAuth.js';
import { validateAuthMethod } from '../config/auth.js';
import { useEditorSettings } from './hooks/useEditorSettings.js';
import { useSettingsCommand } from './hooks/useSettingsCommand.js';
import { useModelCommand } from './hooks/useModelCommand.js';
import { useSlashCommandProcessor } from './hooks/slashCommandProcessor.js';
import { useErrorCount } from './hooks/useConsoleMessages.js';
import { useGeminiStream } from './hooks/useGeminiStream.js';
import { useVim } from './hooks/vim.js';
import { useFolderTrust } from './hooks/useFolderTrust.js';
import { useIdeTrustListener } from './hooks/useIdeTrustListener.js';
import { useMessageQueue } from './hooks/useMessageQueue.js';
import { useApprovalModeIndicator } from './hooks/useApprovalModeIndicator.js';
import { useGitBranchName } from './hooks/useGitBranchName.js';
import {
  useConfirmUpdateRequests,
  useExtensionUpdates,
} from './hooks/useExtensionUpdates.js';
import { useVimMode } from './contexts/VimModeContext.js';
import { useSessionStats } from './contexts/SessionContext.js';
import { useTextBuffer } from './components/shared/text-buffer.js';
import { useLogger } from './hooks/useLogger.js';
import { useLoadingIndicator } from './hooks/useLoadingIndicator.js';
import { useInputHistoryStore } from './hooks/useInputHistoryStore.js';
import { useKeypress, type Key } from './hooks/useKeypress.js';
import * as useKeypressModule from './hooks/useKeypress.js';
import { useSuspend } from './hooks/useSuspend.js';
import {
  writeToStdout,
  enableMouseEvents,
  disableMouseEvents,
} from '@google/gemini-cli-core';
import { type ExtensionManager } from '../config/extension-manager.js';
import {
  WARNING_PROMPT_DURATION_MS,
  EXPAND_HINT_DURATION_MS,
} from './constants.js';

describe('AppContainer State Management', () => {
  let mockConfig: Config;
  let mockSettings: LoadedSettings;
  let mockInitResult: InitializationResult;
  let mockExtensionManager: MockedObject<ExtensionManager>;

  type AppContainerProps = {
    settings?: LoadedSettings;
    config?: Config;
    version?: string;
    initResult?: InitializationResult;
    startupWarnings?: StartupWarning[];
    resumedSessionData?: ResumedSessionData;
  };

  // Helper to generate the AppContainer JSX for render and rerender
  const getAppContainer = ({
    settings = mockSettings,
    config = mockConfig,
    version = '1.0.0',
    initResult = mockInitResult,
    startupWarnings,
    resumedSessionData,
  }: AppContainerProps = {}) => (
    <SettingsContext.Provider value={settings}>
      <KeypressProvider config={config}>
        <OverflowProvider>
          <AppContainer
            config={config}
            version={version}
            initializationResult={initResult}
            startupWarnings={startupWarnings}
            resumedSessionData={resumedSessionData}
          />
        </OverflowProvider>
      </KeypressProvider>
    </SettingsContext.Provider>
  );

  // Helper to render the AppContainer
  const renderAppContainer = async (props?: AppContainerProps) =>
    render(getAppContainer(props));

  // Create typed mocks for all hooks
  const mockedUseQuotaAndFallback = useQuotaAndFallback as Mock;
  const mockedUseHistory = useHistory as Mock;
  const mockedUseThemeCommand = useThemeCommand as Mock;
  const mockedUseAuthCommand = useAuthCommand as Mock;
  const mockedUseEditorSettings = useEditorSettings as Mock;
  const mockedUseSettingsCommand = useSettingsCommand as Mock;
  const mockedUseModelCommand = useModelCommand as Mock;
  const mockedUseSlashCommandProcessor = useSlashCommandProcessor as Mock;
  const mockedUseConsoleMessages = useErrorCount as Mock;
  const mockedUseGeminiStream = useGeminiStream as Mock;
  const mockedUseVim = useVim as Mock;
  const mockedUseFolderTrust = useFolderTrust as Mock;
  const mockedUseIdeTrustListener = useIdeTrustListener as Mock;
  const mockedUseMessageQueue = useMessageQueue as Mock;
  const mockedUseApprovalModeIndicator = useApprovalModeIndicator as Mock;
  const mockedUseGitBranchName = useGitBranchName as Mock;
  const mockedUseConfirmUpdateRequests = useConfirmUpdateRequests as Mock;
  const mockedUseExtensionUpdates = useExtensionUpdates as Mock;
  const mockedUseVimMode = useVimMode as Mock;
  const mockedUseSessionStats = useSessionStats as Mock;
  const mockedUseTextBuffer = useTextBuffer as Mock;
  const mockedUseLogger = useLogger as Mock;
  const mockedUseLoadingIndicator = useLoadingIndicator as Mock;
  const mockedUseSuspend = useSuspend as Mock;
  const mockedUseInputHistoryStore = useInputHistoryStore as Mock;
  const mockedUseHookDisplayState = useHookDisplayState as Mock;
  const mockedUseTerminalTheme = useTerminalTheme as Mock;
  const mockedUseShellInactivityStatus = useShellInactivityStatus as Mock;
  const mockedUseFocusState = useFocus as Mock;

  const DEFAULT_GEMINI_STREAM_MOCK = {
    streamingState: 'idle',
    submitQuery: vi.fn(),
    initError: null,
    pendingHistoryItems: [],
    thought: null,
    cancelOngoingRequest: vi.fn(),
    handleApprovalModeChange: vi.fn(),
    activePtyId: null,
    loopDetectionConfirmationRequest: null,
    backgroundTaskCount: 0,
    isBackgroundTaskVisible: false,
    toggleBackgroundTasks: vi.fn(),
    backgroundCurrentExecution: vi.fn(),
    backgroundTasks: new Map(),
    registerBackgroundTask: vi.fn(),
    dismissBackgroundTask: vi.fn(),
  };

  beforeEach(() => {
    persistentStateMock.reset();
    vi.clearAllMocks();

    mockIdeClient.getInstance.mockReturnValue(new Promise(() => {}));

    // Initialize mock stdout for terminal title tests

    mocks.mockStdout.write.mockClear();
    (disableMouseEvents as import('vitest').Mock).mockClear();

    capturedUIState = null!;

    // **Provide a default return value for EVERY mocked hook.**
    mockedUseQuotaAndFallback.mockReturnValue({
      proQuotaRequest: null,
      handleProQuotaChoice: vi.fn(),
    });
    mockedUseHistory.mockReturnValue({
      history: [],
      addItem: vi.fn(),
      updateItem: vi.fn(),
      clearItems: vi.fn(),
      loadHistory: vi.fn(),
    });
    mockedUseThemeCommand.mockReturnValue({
      isThemeDialogOpen: false,
      openThemeDialog: vi.fn(),
      handleThemeSelect: vi.fn(),
      handleThemeHighlight: vi.fn(),
    });
    mockedUseAuthCommand.mockReturnValue({
      authState: 'authenticated',
      setAuthState: vi.fn(),
      authError: null,
      onAuthError: vi.fn(),
    });
    mockedUseEditorSettings.mockReturnValue({
      isEditorDialogOpen: false,
      openEditorDialog: vi.fn(),
      handleEditorSelect: vi.fn(),
      exitEditorDialog: vi.fn(),
    });
    mockedUseSettingsCommand.mockReturnValue({
      isSettingsDialogOpen: false,
      openSettingsDialog: vi.fn(),
      closeSettingsDialog: vi.fn(),
    });
    mockedUseModelCommand.mockReturnValue({
      isModelDialogOpen: false,
      openModelDialog: vi.fn(),
      closeModelDialog: vi.fn(),
    });
    mockedUseSlashCommandProcessor.mockReturnValue({
      handleSlashCommand: vi.fn(),
      slashCommands: [],
      pendingHistoryItems: [],
      commandContext: {},
      shellConfirmationRequest: null,
      confirmationRequest: null,
    });
    mockedUseConsoleMessages.mockReturnValue({
      errorCount: 0,
      handleNewMessage: vi.fn(),
      clearErrorCount: vi.fn(),
    });
    mockedUseGeminiStream.mockReturnValue(DEFAULT_GEMINI_STREAM_MOCK);
    mockedUseVim.mockReturnValue({ handleInput: vi.fn() });
    mockedUseFolderTrust.mockReturnValue({
      isFolderTrustDialogOpen: false,
      handleFolderTrustSelect: vi.fn(),
      isRestarting: false,
    });
    mockedUseIdeTrustListener.mockReturnValue({
      needsRestart: false,
      restartReason: 'NONE',
    });
    mockedUseMessageQueue.mockReturnValue({
      messageQueue: [],
      addMessage: vi.fn(),
      clearQueue: vi.fn(),
      getQueuedMessagesText: vi.fn().mockReturnValue(''),
    });
    mockedUseApprovalModeIndicator.mockReturnValue(false);
    mockedUseGitBranchName.mockReturnValue('main');
    mockedUseVimMode.mockReturnValue({
      isVimEnabled: false,
      toggleVimEnabled: vi.fn(),
    });
    mockedUseSessionStats.mockReturnValue({ stats: {} });
    mockedUseTextBuffer.mockReturnValue({
      text: '',
      setText: vi.fn(),
      lines: [''],
      cursor: [0, 0],
      handleInput: vi.fn().mockReturnValue(false),
    });
    mockedUseLogger.mockReturnValue({
      getPreviousUserMessages: vi.fn().mockResolvedValue([]),
    });
    mockedUseInputHistoryStore.mockReturnValue({
      inputHistory: [],
      addInput: vi.fn(),
      initializeFromLogger: vi.fn(),
    });
    mockedUseLoadingIndicator.mockReturnValue({
      elapsedTime: '0.0s',
      currentLoadingPhrase: '',
    });
    mockedUseSuspend.mockReturnValue({
      handleSuspend: vi.fn(),
    });
    mockedUseHookDisplayState.mockReturnValue([]);
    mockedUseTerminalTheme.mockReturnValue(undefined);
    mockedUseShellInactivityStatus.mockReturnValue({
      shouldShowFocusHint: false,
      inactivityStatus: 'none',
    });
    mockedUseFocusState.mockReturnValue({
      isFocused: true,
      hasReceivedFocusEvent: true,
    });
    mockedUseConfirmUpdateRequests.mockReturnValue({
      addConfirmUpdateExtensionRequest: vi.fn(),
      confirmUpdateExtensionRequests: [],
    });
    mockedUseExtensionUpdates.mockReturnValue({
      extensionsUpdateState: new Map(),
      extensionsUpdateStateInternal: new Map(),
      dispatchExtensionStateUpdate: vi.fn(),
    });

    // Mock Config
    mockConfig = makeFakeConfig();
    vi.spyOn(mockConfig, 'getUseRenderProcess').mockReturnValue(false);

    // Mock config's getTargetDir to return consistent workspace directory
    vi.spyOn(mockConfig, 'getTargetDir').mockReturnValue('/test/workspace');
    vi.spyOn(mockConfig, 'initialize').mockResolvedValue(undefined);
    vi.spyOn(mockConfig, 'getDebugMode').mockReturnValue(false);

    mockExtensionManager = vi.mockObject({
      getExtensions: vi.fn().mockReturnValue([]),
      setRequestConsent: vi.fn(),
      setRequestSetting: vi.fn(),
      start: vi.fn(),
    } as unknown as ExtensionManager);
    vi.spyOn(mockConfig, 'getExtensionLoader').mockReturnValue(
      mockExtensionManager,
    );

    // Mock LoadedSettings
    mockSettings = createMockSettings({
      hideBanner: false,
      hideTips: false,
      hideFooter: false,
      showMemoryUsage: false,
      theme: 'default',
      ui: {
        showStatusInTitle: false,
        hideWindowTitle: false,
        useAlternateBuffer: false,
      },
    });

    // Mock InitializationResult
    mockInitResult = {
      themeError: null,
      authError: null,
      shouldOpenAuthDialog: false,
      geminiMdFileCount: 0,
    } as InitializationResult;
  });

  afterEach(() => {
    cleanup();
    vi.restoreAllMocks();
  });

  describe('Basic Rendering', () => {
    it('renders without crashing with minimal props', async () => {
      const { unmount } = await act(async () => renderAppContainer());
      expect(capturedUIState).toBeTruthy();
      unmount();
    });

    it('renders with startup warnings', async () => {
      const startupWarnings: StartupWarning[] = [
        {
          id: 'w1',
          message: 'Warning 1',
          priority: WarningPriority.High,
        },
        {
          id: 'w2',
          message: 'Warning 2',
          priority: WarningPriority.High,
        },
      ];

      const { unmount } = await act(async () =>
        renderAppContainer({ startupWarnings }),
      );
      expect(capturedUIState).toBeTruthy();
      unmount();
    });

    it('shows full UI details by default', async () => {
      const { unmount } = await act(async () => renderAppContainer());

      expect(capturedUIState.cleanUiDetailsVisible).toBe(true);
      unmount();
    });

    it('starts in minimal UI mode when Focus UI preference is persisted', async () => {
      persistentStateMock.get.mockReturnValueOnce(true);

      const { unmount } = await act(async () =>
        renderAppContainer({
          settings: mockSettings,
        }),
      );

      expect(capturedUIState.cleanUiDetailsVisible).toBe(false);
      expect(persistentStateMock.get).toHaveBeenCalledWith('focusUiEnabled');
      unmount();
    });
  });

  describe('State Initialization', () => {
    it('calls validateAuthMethod and onAuthError if validation fails', async () => {
      const mockOnAuthError = vi.fn();
      mockedUseAuthCommand.mockReturnValue({
        authState: 'authenticated',
        setAuthState: vi.fn(),
        authError: null,
        onAuthError: mockOnAuthError,
      });
      vi.mocked(validateAuthMethod).mockResolvedValueOnce('Validation Failed');

      const { unmount } = await act(async () =>
        renderAppContainer({
          settings: createMockSettings({
            merged: {
              security: {
                auth: { selectedType: 'oauth-personal', useExternal: false },
              },
            },
          }),
        }),
      );

      await waitFor(() => {
        expect(validateAuthMethod).toHaveBeenCalledWith('oauth-personal');
        expect(mockOnAuthError).toHaveBeenCalledWith('Validation Failed');
      });

      unmount();
    });

    it('sends a macOS notification when confirmation is pending and terminal is unfocused', async () => {
      mockedUseFocusState.mockReturnValue({
        isFocused: false,
        hasReceivedFocusEvent: true,
      });
      mockedUseGeminiStream.mockReturnValue({
        ...DEFAULT_GEMINI_STREAM_MOCK,
        pendingHistoryItems: [
          {
            type: 'tool_group',
            tools: [
              {
                callId: 'call-1',
                name: 'run_shell_command',
                description: 'Run command',
                resultDisplay: undefined,
                status: CoreToolCallStatus.AwaitingApproval,
                confirmationDetails: {
                  type: 'exec',
                  title: 'Run shell command',
                  command: 'ls',
                  rootCommand: 'ls',
                  rootCommands: ['ls'],
                },
              },
            ],
          },
        ],
      });

      const { unmount } = await act(async () => renderAppContainer());

      expect(terminalNotificationsMocks.notifyViaTerminal).toHaveBeenCalled();
      expect(
        terminalNotificationsMocks.buildRunEventNotificationContent,
      ).toHaveBeenCalledWith(
        expect.objectContaining({
          type: 'attention',
        }),
      );

      unmount();
    });

    it('does not send attention notification when terminal is focused', async () => {
      mockedUseFocusState.mockReturnValue({
        isFocused: true,
        hasReceivedFocusEvent: true,
      });
      mockedUseGeminiStream.mockReturnValue({
        ...DEFAULT_GEMINI_STREAM_MOCK,
        pendingHistoryItems: [
          {
            type: 'tool_group',
            tools: [
              {
                callId: 'call-2',
                name: 'run_shell_command',
                description: 'Run command',
                resultDisplay: undefined,
                status: CoreToolCallStatus.AwaitingApproval,
                confirmationDetails: {
                  type: 'exec',
                  title: 'Run shell command',
                  command: 'ls',
                  rootCommand: 'ls',
                  rootCommands: ['ls'],
                },
              },
            ],
          },
        ],
      });

      const { unmount } = await act(async () => renderAppContainer());

      expect(
        terminalNotificationsMocks.notifyViaTerminal,
      ).not.toHaveBeenCalled();

      unmount();
    });

    it('sends attention notification when focus reporting is unavailable', async () => {
      mockedUseFocusState.mockReturnValue({
        isFocused: true,
        hasReceivedFocusEvent: false,
      });
      mockedUseGeminiStream.mockReturnValue({
        ...DEFAULT_GEMINI_STREAM_MOCK,
        pendingHistoryItems: [
          {
            type: 'tool_group',
            tools: [
              {
                callId: 'call-focus-unknown',
                name: 'run_shell_command',
                description: 'Run command',
                resultDisplay: undefined,
                status: CoreToolCallStatus.AwaitingApproval,
                confirmationDetails: {
                  type: 'exec',
                  title: 'Run shell command',
                  command: 'ls',
                  rootCommand: 'ls',
                  rootCommands: ['ls'],
                },
              },
            ],
          },
        ],
      });

      const { unmount } = await act(async () => renderAppContainer());

      expect(terminalNotificationsMocks.notifyViaTerminal).toHaveBeenCalled();

      unmount();
    });

    it('sends a macOS notification when a response completes while unfocused', async () => {
      mockedUseFocusState.mockReturnValue({
        isFocused: false,
        hasReceivedFocusEvent: true,
      });
      let currentStreamingState: 'idle' | 'responding' = 'responding';
      mockedUseGeminiStream.mockImplementation(() => ({
        ...DEFAULT_GEMINI_STREAM_MOCK,
        streamingState: currentStreamingState,
      }));

      const { unmount, rerender } = await act(async () => renderAppContainer());

      currentStreamingState = 'idle';
      await act(async () => {
        rerender(getAppContainer());
      });

      expect(
        terminalNotificationsMocks.buildRunEventNotificationContent,
      ).toHaveBeenCalledWith(
        expect.objectContaining({
          type: 'session_complete',
          detail: 'Gemini CLI finished responding.',
        }),
      );
      expect(terminalNotificationsMocks.notifyViaTerminal).toHaveBeenCalled();

      unmount();
    });

    it('sends completion notification when focus reporting is unavailable', async () => {
      mockedUseFocusState.mockReturnValue({
        isFocused: true,
        hasReceivedFocusEvent: false,
      });
      let currentStreamingState: 'idle' | 'responding' = 'responding';
      mockedUseGeminiStream.mockImplementation(() => ({
        ...DEFAULT_GEMINI_STREAM_MOCK,
        streamingState: currentStreamingState,
      }));

      const { unmount, rerender } = await act(async () => renderAppContainer());

      currentStreamingState = 'idle';
      await act(async () => {
        rerender(getAppContainer());
      });

      expect(
        terminalNotificationsMocks.buildRunEventNotificationContent,
      ).toHaveBeenCalledWith(
        expect.objectContaining({
          type: 'session_complete',
          detail: 'Gemini CLI finished responding.',
        }),
      );

      unmount();
    });

    it('does not send completion notification when another action-required dialog is pending', async () => {
      mockedUseFocusState.mockReturnValue({
        isFocused: false,
        hasReceivedFocusEvent: true,
      });
      mockedUseQuotaAndFallback.mockReturnValue({
        proQuotaRequest: { kind: 'upgrade' },
        handleProQuotaChoice: vi.fn(),
      });
      let currentStreamingState: 'idle' | 'responding' = 'responding';
      mockedUseGeminiStream.mockImplementation(() => ({
        ...DEFAULT_GEMINI_STREAM_MOCK,
        streamingState: currentStreamingState,
      }));

      const { unmount, rerender } = await act(async () => renderAppContainer());

      currentStreamingState = 'idle';
      await act(async () => {
        rerender(getAppContainer());
      });

      expect(
        terminalNotificationsMocks.notifyViaTerminal,
      ).not.toHaveBeenCalled();

      unmount();
    });

    it('can send repeated attention notifications for the same key after pending state clears', async () => {
      mockedUseFocusState.mockReturnValue({
        isFocused: false,
        hasReceivedFocusEvent: true,
      });

      let pendingHistoryItems = [
        {
          type: 'tool_group',
          tools: [
            {
              callId: 'repeat-key-call',
              name: 'run_shell_command',
              description: 'Run command',
              resultDisplay: undefined,
              status: CoreToolCallStatus.AwaitingApproval,
              confirmationDetails: {
                type: 'exec',
                title: 'Run shell command',
                command: 'ls',
                rootCommand: 'ls',
                rootCommands: ['ls'],
              },
            },
          ],
        },
      ];

      mockedUseGeminiStream.mockImplementation(() => ({
        ...DEFAULT_GEMINI_STREAM_MOCK,
        pendingHistoryItems,
      }));

      const { unmount, rerender } = await act(async () => renderAppContainer());

      expect(
        terminalNotificationsMocks.notifyViaTerminal,
      ).toHaveBeenCalledTimes(1);

      pendingHistoryItems = [];
      await act(async () => {
        rerender(getAppContainer());
      });

      pendingHistoryItems = [
        {
          type: 'tool_group',
          tools: [
            {
              callId: 'repeat-key-call',
              name: 'run_shell_command',
              description: 'Run command',
              resultDisplay: undefined,
              status: CoreToolCallStatus.AwaitingApproval,
              confirmationDetails: {
                type: 'exec',
                title: 'Run shell command',
                command: 'ls',
                rootCommand: 'ls',
                rootCommands: ['ls'],
              },
            },
          ],
        },
      ];
      await act(async () => {
        rerender(getAppContainer());
      });

      expect(
        terminalNotificationsMocks.notifyViaTerminal,
      ).toHaveBeenCalledTimes(2);

      unmount();
    });

    it('initializes with theme error from initialization result', async () => {
      const initResultWithError = {
        ...mockInitResult,
        themeError: 'Failed to load theme',
      };

      const { unmount } = await act(async () =>
        renderAppContainer({
          initResult: initResultWithError,
        }),
      );
      expect(capturedUIState).toBeTruthy();
      unmount();
    });

    it('handles debug mode state', async () => {
      const debugConfig = makeFakeConfig();
      vi.spyOn(debugConfig, 'getDebugMode').mockReturnValue(true);

      const { unmount } = await act(async () =>
        renderAppContainer({ config: debugConfig }),
      );
      unmount();
    });
  });

  describe('Context Providers', () => {
    it('provides AppContext with correct values', async () => {
      const { unmount } = await act(async () =>
        renderAppContainer({ version: '2.0.0' }),
      );
      expect(capturedUIState).toBeTruthy();

      // Should render and unmount cleanly
      unmount();
    });

    it('provides UIStateContext with state management', async () => {
      const { unmount } = await act(async () => renderAppContainer());
      expect(capturedUIState).toBeTruthy();
      unmount();
    });

    it('provides UIActionsContext with action handlers', async () => {
      const { unmount } = await act(async () => renderAppContainer());
      expect(capturedUIState).toBeTruthy();
      unmount();
    });

    it('provides ConfigContext with config object', async () => {
      const { unmount } = await act(async () => renderAppContainer());
      expect(capturedUIState).toBeTruthy();
      unmount();
    });
  });

  describe('Settings Integration', () => {
    it('handles settings with all display options disabled', async () => {
      const settingsAllHidden = createMockSettings({
        hideBanner: true,
        hideTips: true,
        hideFooter: true,
        showMemoryUsage: false,
      });

      const { unmount } = await act(async () =>
        renderAppContainer({ settings: settingsAllHidden }),
      );
      expect(capturedUIState).toBeTruthy();
      unmount();
    });

    it('handles settings with memory usage enabled', async () => {
      const settingsWithMemory = createMockSettings({
        showMemoryUsage: true,
      });

      const { unmount } = await act(async () =>
        renderAppContainer({ settings: settingsWithMemory }),
      );
      expect(capturedUIState).toBeTruthy();
      unmount();
    });
  });

  describe('Version Handling', () => {
    it.each(['1.0.0', '2.1.3-beta', '3.0.0-nightly'])(
      'handles version format: %s',
      async (version) => {
        const { unmount } = await act(async () =>
          renderAppContainer({ version }),
        );
        expect(capturedUIState).toBeTruthy();
        unmount();
      },
    );
  });

  describe('Error Handling', () => {
    it('handles config methods that might throw', async () => {
      const errorConfig = makeFakeConfig();
      vi.spyOn(errorConfig, 'getModel').mockImplementation(() => {
        throw new Error('Config error');
      });

      // Should still render without crashing - errors should be handled internally
      const { unmount } = await act(async () =>
        renderAppContainer({ config: errorConfig }),
      );
      unmount();
    });

    it('handles undefined settings gracefully', async () => {
      const undefinedSettings = createMockSettings();

      const { unmount } = await act(async () =>
        renderAppContainer({ settings: undefinedSettings }),
      );
      expect(capturedUIState).toBeTruthy();
      unmount();
    });
  });

  describe('Provider Hierarchy', () => {
    it('establishes correct provider nesting order', async () => {
      // This tests that all the context providers are properly nested
      // and that the component tree can be built without circular dependencies
      const { unmount } = await act(async () => renderAppContainer());

      unmount();
    });
  });

  describe('Session Resumption', () => {
    it('handles resumed session data correctly', async () => {
      const mockResumedSessionData = {
        conversation: {
          sessionId: 'test-session-123',
          projectHash: 'test-project-hash',
          startTime: '2024-01-01T00:00:00Z',
          lastUpdated: '2024-01-01T00:00:01Z',
          messages: [
            {
              id: 'msg-1',
              type: 'user' as const,
              content: 'Hello',
              timestamp: '2024-01-01T00:00:00Z',
            },
            {
              id: 'msg-2',
              type: 'gemini' as const,
              content: 'Hi there!',
              role: 'model' as const,
              parts: [{ text: 'Hi there!' }],
              timestamp: '2024-01-01T00:00:01Z',
            },
          ],
        },
        filePath: '/tmp/test-session.json',
      };

      const { unmount } = await act(async () =>
        renderAppContainer({
          config: mockConfig,
          settings: mockSettings,
          version: '1.0.0',
          initResult: mockInitResult,
          resumedSessionData: mockResumedSessionData,
        }),
      );
      unmount();
    });

    it('renders without resumed session data', async () => {
      const { unmount } = await act(async () =>
        renderAppContainer({
          config: mockConfig,
          settings: mockSettings,
          version: '1.0.0',
          initResult: mockInitResult,
          resumedSessionData: undefined,
        }),
      );
      unmount();
    });

    it('initializes chat recording service when config has it', async () => {
      const mockChatRecordingService = {
        initialize: vi.fn(),
        recordMessage: vi.fn(),
        recordMessageTokens: vi.fn(),
        recordToolCalls: vi.fn(),
      };

      const mockGeminiClient = {
        isInitialized: vi.fn(() => true),
        resumeChat: vi.fn(),
        getUserTier: vi.fn(),
        getChatRecordingService: vi.fn(() => mockChatRecordingService),
      };

      const configWithRecording = makeFakeConfig();
      vi.spyOn(configWithRecording, 'getGeminiClient').mockReturnValue(
        mockGeminiClient as unknown as ReturnType<Config['getGeminiClient']>,
      );

      const { unmount } = await act(async () =>
        renderAppContainer({
          config: configWithRecording,
          settings: mockSettings,
          version: '1.0.0',
          initResult: mockInitResult,
        }),
      );
      unmount();
    });
  });
  describe('Session Recording Integration', () => {
    it('provides chat recording service configuration', async () => {
      const mockChatRecordingService = {
        initialize: vi.fn(),
        recordMessage: vi.fn(),
        recordMessageTokens: vi.fn(),
        recordToolCalls: vi.fn(),
        getSessionId: vi.fn(() => 'test-session-123'),
        getCurrentConversation: vi.fn(),
      };

      const mockGeminiClient = {
        isInitialized: vi.fn(() => true),
        resumeChat: vi.fn(),
        getUserTier: vi.fn(),
        getChatRecordingService: vi.fn(() => mockChatRecordingService),
        setHistory: vi.fn(),
      };

      const configWithRecording = makeFakeConfig();
      vi.spyOn(configWithRecording, 'getGeminiClient').mockReturnValue(
        mockGeminiClient as unknown as ReturnType<Config['getGeminiClient']>,
      );
      vi.spyOn(configWithRecording, 'getSessionId').mockReturnValue(
        'test-session-123',
      );

      const { unmount } = await act(async () =>
        renderAppContainer({
          config: configWithRecording,
          settings: mockSettings,
          version: '1.0.0',
          initResult: mockInitResult,
        }),
      );

      // Verify the recording service structure is correct
      expect(configWithRecording.getGeminiClient).toBeDefined();
      expect(mockGeminiClient.getChatRecordingService).toBeDefined();
      expect(mockChatRecordingService.initialize).toBeDefined();
      expect(mockChatRecordingService.recordMessage).toBeDefined();
      unmount();
    });

    it('handles session recording when messages are added', async () => {
      const mockRecordMessage = vi.fn();
      const mockRecordMessageTokens = vi.fn();

      const mockChatRecordingService = {
        initialize: vi.fn(),
        recordMessage: mockRecordMessage,
        recordMessageTokens: mockRecordMessageTokens,
        recordToolCalls: vi.fn(),
        getSessionId: vi.fn(() => 'test-session-123'),
      };

      const mockGeminiClient = {
        isInitialized: vi.fn(() => true),
        getChatRecordingService: vi.fn(() => mockChatRecordingService),
        getUserTier: vi.fn(),
      };

      const configWithRecording = makeFakeConfig();
      vi.spyOn(configWithRecording, 'getGeminiClient').mockReturnValue(
        mockGeminiClient as unknown as ReturnType<Config['getGeminiClient']>,
      );

      const { unmount } = await act(async () =>
        renderAppContainer({
          config: configWithRecording,
          settings: mockSettings,
          version: '1.0.0',
          initResult: mockInitResult,
        }),
      );

      // The actual recording happens through the useHistory hook
      // which would be triggered by user interactions
      expect(mockChatRecordingService.initialize).toBeDefined();
      expect(mockChatRecordingService.recordMessage).toBeDefined();
      unmount();
    });
  });

  describe('Session Resume Flow', () => {
    it('accepts resumed session data', async () => {
      const mockResumeChat = vi.fn();
      const mockGeminiClient = {
        isInitialized: vi.fn(() => true),
        resumeChat: mockResumeChat,
        getUserTier: vi.fn(),
        getChatRecordingService: vi.fn(() => ({
          initialize: vi.fn(),
          recordMessage: vi.fn(),
          recordMessageTokens: vi.fn(),
          recordToolCalls: vi.fn(),
        })),
      };

      const configWithClient = makeFakeConfig();
      vi.spyOn(configWithClient, 'getGeminiClient').mockReturnValue(
        mockGeminiClient as unknown as ReturnType<Config['getGeminiClient']>,
      );

      const resumedData = {
        conversation: {
          sessionId: 'resumed-session-456',
          projectHash: 'project-hash',
          startTime: '2024-01-01T00:00:00Z',
          lastUpdated: '2024-01-01T00:01:00Z',
          messages: [
            {
              id: 'msg-1',
              type: 'user' as const,
              content: 'Previous question',
              timestamp: '2024-01-01T00:00:00Z',
            },
            {
              id: 'msg-2',
              type: 'gemini' as const,
              content: 'Previous answer',
              role: 'model' as const,
              parts: [{ text: 'Previous answer' }],
              timestamp: '2024-01-01T00:00:30Z',
              tokenCount: { input: 10, output: 20 },
            },
          ],
        },
        filePath: '/tmp/resumed-session.json',
      };

      const { unmount } = await act(async () =>
        renderAppContainer({
          config: configWithClient,
          settings: mockSettings,
          version: '1.0.0',
          initResult: mockInitResult,
          resumedSessionData: resumedData,
        }),
      );

      // Verify the resume functionality structure is in place
      expect(mockGeminiClient.resumeChat).toBeDefined();
      expect(resumedData.conversation.messages).toHaveLength(2);
      unmount();
    });

    it('does not attempt resume when client is not initialized', async () => {
      const mockResumeChat = vi.fn();
      const mockGeminiClient = {
        isInitialized: vi.fn(() => false), // Not initialized
        resumeChat: mockResumeChat,
        getUserTier: vi.fn(),
        getChatRecordingService: vi.fn(),
      };

      const configWithClient = makeFakeConfig();
      vi.spyOn(configWithClient, 'getGeminiClient').mockReturnValue(
        mockGeminiClient as unknown as ReturnType<Config['getGeminiClient']>,
      );

      const resumedData = {
        conversation: {
          sessionId: 'test-session',
          projectHash: 'project-hash',
          startTime: '2024-01-01T00:00:00Z',
          lastUpdated: '2024-01-01T00:01:00Z',
          messages: [],
        },
        filePath: '/tmp/session.json',
      };

      const { unmount } = await act(async () =>
        renderAppContainer({
          config: configWithClient,
          settings: mockSettings,
          version: '1.0.0',
          initResult: mockInitResult,
          resumedSessionData: resumedData,
        }),
      );

      // Should not call resumeChat when client is not initialized
      expect(mockResumeChat).not.toHaveBeenCalled();
      unmount();
    });
  });

  describe('SessionStart Hook Rendering', () => {
    it('does not render systemMessage directly (avoids duplicate with HookSystemMessage event)', async () => {
      const mockAddItem = vi.fn();
      mockedUseHistory.mockReturnValue({
        history: [],
        addItem: mockAddItem,
        updateItem: vi.fn(),
        clearItems: vi.fn(),
        loadHistory: vi.fn(),
      });

      const fireSessionStartEvent = vi.fn().mockResolvedValue({
        systemMessage: 'Hello from SessionStart hook',
        getAdditionalContext: vi.fn(() => undefined),
      });
      vi.spyOn(mockConfig, 'getHookSystem').mockReturnValue({
        fireSessionEndEvent: vi.fn().mockResolvedValue(undefined),
        fireSessionStartEvent,
      } as unknown as ReturnType<Config['getHookSystem']>);

      const { unmount } = await act(async () => renderAppContainer());
      await waitFor(() => expect(fireSessionStartEvent).toHaveBeenCalled());

      // The direct-render path (the bug) would call addItem with the
      // systemMessage text and no `source` field. The HookSystemMessage
      // event-listener path (the correct one) always sets `source`.
      const directRenderCall = mockAddItem.mock.calls.find(
        ([item]) =>
          item?.text === 'Hello from SessionStart hook' && !item?.source,
      );
      expect(directRenderCall).toBeUndefined();

      unmount();
    });
  });

  describe('Token Counting from Session Stats', () => {
    it('tracks token counts from session messages', async () => {
      // Session stats are provided through the SessionStatsProvider context
      // in the real app, not through the config directly
      const mockChatRecordingService = {
        initialize: vi.fn(),
        recordMessage: vi.fn(),
        recordMessageTokens: vi.fn(),
        recordToolCalls: vi.fn(),
        getSessionId: vi.fn(() => 'test-session-123'),
        getCurrentConversation: vi.fn(() => ({
          sessionId: 'test-session-123',
          messages: [],
          totalInputTokens: 150,
          totalOutputTokens: 350,
        })),
      };

      const mockGeminiClient = {
        isInitialized: vi.fn(() => true),
        getChatRecordingService: vi.fn(() => mockChatRecordingService),
        getUserTier: vi.fn(),
      };

      const configWithRecording = makeFakeConfig();
      vi.spyOn(configWithRecording, 'getGeminiClient').mockReturnValue(
        mockGeminiClient as unknown as ReturnType<Config['getGeminiClient']>,
      );

      const { unmount } = await act(async () =>
        renderAppContainer({
          config: configWithRecording,
          settings: mockSettings,
          version: '1.0.0',
          initResult: mockInitResult,
        }),
      );

      // In the actual app, these stats would be displayed in components
      // and updated as messages are processed through the recording service
      expect(mockChatRecordingService.recordMessageTokens).toBeDefined();
      expect(mockChatRecordingService.getCurrentConversation).toBeDefined();
      unmount();
    });
  });

  describe('Quota and Fallback Integration', () => {
    it('passes a null proQuotaRequest to QuotaContext by default', async () => {
      // The default mock from beforeEach already sets proQuotaRequest to null
      const { unmount } = await act(async () => renderAppContainer());
      // Assert that the context value is as expected
      expect(capturedQuotaState.proQuotaRequest).toBeNull();
      unmount();
    });

    it('passes a valid proQuotaRequest to QuotaContext when provided by the hook', async () => {
      // Arrange: Create a mock request object that a UI dialog would receive
      const mockRequest = {
        failedModel: 'gemini-pro',
        fallbackModel: 'gemini-flash',
        resolve: vi.fn(),
      };
      mockedUseQuotaAndFallback.mockReturnValue({
        proQuotaRequest: mockRequest,
        handleProQuotaChoice: vi.fn(),
      });

      // Act: Render the container
      const { unmount } = await act(async () => renderAppContainer());
      // Assert: The mock request is correctly passed through the context
      expect(capturedQuotaState.proQuotaRequest).toEqual(mockRequest);
      unmount();
    });

    it('passes the handleProQuotaChoice function to UIActionsContext', async () => {
      // Arrange: Create a mock handler function
      const mockHandler = vi.fn();
      mockedUseQuotaAndFallback.mockReturnValue({
        proQuotaRequest: null,
        handleProQuotaChoice: mockHandler,
      });

      // Act: Render the container
      const { unmount } = await act(async () => renderAppContainer());
      // Assert: The action in the context is the mock handler we provided
      expect(capturedUIActions.handleProQuotaChoice).toBe(mockHandler);

      // You can even verify that the plumbed function is callable
      act(() => {
        capturedUIActions.handleProQuotaChoice('retry_later');
      });
      expect(mockHandler).toHaveBeenCalledWith('retry_later');
      unmount();
    });
  });

  describe('Terminal Title Update Feature', () => {
    beforeEach(() => {
      // Reset mock stdout for each test
      mocks.mockStdout.write.mockClear();
      (disableMouseEvents as import('vitest').Mock).mockClear();
    });

    it('verifies useStdout is mocked', async () => {
      const { useStdout } = await import('ink');
      const { stdout } = useStdout();
      expect(stdout).toBe(mocks.mockStdout);
    });

    it('should update terminal title with Working… when showStatusInTitle is false', async () => {
      // Arrange: Set up mock settings with showStatusInTitle disabled
      const mockSettingsWithShowStatusFalse = createMockSettings({
        ui: {
          showStatusInTitle: false,
          hideWindowTitle: false,
        },
      });

      // Mock the streaming state as Active
      mockedUseGeminiStream.mockReturnValue({
        ...DEFAULT_GEMINI_STREAM_MOCK,
        streamingState: 'responding',
        thought: { subject: 'Some thought' },
      });

      // Act: Render the container
      const { unmount } = await act(async () =>
        renderAppContainer({
          settings: mockSettingsWithShowStatusFalse,
        }),
      );

      // Assert: Check that title was updated with "Working…"
      const titleWrites = mocks.mockStdout.write.mock.calls.filter((call) =>
        call[0].includes('\x1b]0;'),
      );

      expect(titleWrites).toHaveLength(1);
      expect(titleWrites[0][0]).toBe(
        `\x1b]0;${'✦  Working… (workspace)'.padEnd(80, ' ')}\x07`,
      );
      unmount();
    });

    it('should use legacy terminal title when dynamicWindowTitle is false', async () => {
      // Arrange: Set up mock settings with dynamicWindowTitle disabled
      const mockSettingsWithDynamicTitleFalse = createMockSettings({
        ui: {
          dynamicWindowTitle: false,
          hideWindowTitle: false,
        },
      });

      // Mock the streaming state
      mockedUseGeminiStream.mockReturnValue({
        ...DEFAULT_GEMINI_STREAM_MOCK,
        streamingState: 'responding',
        thought: { subject: 'Some thought' },
      });

      // Act: Render the container
      const { unmount } = await act(async () =>
        renderAppContainer({
          settings: mockSettingsWithDynamicTitleFalse,
        }),
      );

      // Assert: Check that legacy title was used
      const titleWrites = mocks.mockStdout.write.mock.calls.filter((call) =>
        call[0].includes('\x1b]0;'),
      );

      expect(titleWrites).toHaveLength(1);
      expect(titleWrites[0][0]).toBe(
        `\x1b]0;${'Gemini CLI (workspace)'.padEnd(80, ' ')}\x07`,
      );
      unmount();
    });

    it('should not update terminal title when hideWindowTitle is true', async () => {
      // Arrange: Set up mock settings with hideWindowTitle enabled
      const mockSettingsWithHideTitleTrue = createMockSettings({
        ui: {
          showStatusInTitle: true,
          hideWindowTitle: true,
        },
      });

      // Act: Render the container
      const { unmount } = await act(async () =>
        renderAppContainer({
          settings: mockSettingsWithHideTitleTrue,
        }),
      );

      // Assert: Check that no title-related writes occurred
      const titleWrites = mocks.mockStdout.write.mock.calls.filter((call) =>
        call[0].includes('\x1b]0;'),
      );

      expect(titleWrites).toHaveLength(0);
      unmount();
    });

    it('should update terminal title with thought subject when in active state', async () => {
      // Arrange: Set up mock settings with showStatusInTitle enabled
      const mockSettingsWithTitleEnabled = createMockSettings({
        ui: {
          showStatusInTitle: true,
          hideWindowTitle: false,
        },
      });

      // Mock the streaming state and thought
      const thoughtSubject = 'Processing request';
      mockedUseGeminiStream.mockReturnValue({
        ...DEFAULT_GEMINI_STREAM_MOCK,
        streamingState: 'responding',
        thought: { subject: thoughtSubject },
      });

      // Act: Render the container
      const { unmount } = await act(async () =>
        renderAppContainer({
          settings: mockSettingsWithTitleEnabled,
        }),
      );

      // Assert: Check that title was updated with thought subject and suffix
      const titleWrites = mocks.mockStdout.write.mock.calls.filter((call) =>
        call[0].includes('\x1b]0;'),
      );

      expect(titleWrites).toHaveLength(1);
      expect(titleWrites[0][0]).toBe(
        `\x1b]0;${`✦  ${thoughtSubject} (workspace)`.padEnd(80, ' ')}\x07`,
      );
      unmount();
    });

    it('should update terminal title with default text when in Idle state and no thought subject', async () => {
      // Arrange: Set up mock settings with showStatusInTitle enabled
      const mockSettingsWithTitleEnabled = createMockSettings({
        ui: {
          showStatusInTitle: true,
          hideWindowTitle: false,
        },
      });

      // Mock the streaming state as Idle with no thought
      mockedUseGeminiStream.mockReturnValue(DEFAULT_GEMINI_STREAM_MOCK);

      // Act: Render the container
      const { unmount } = await act(async () =>
        renderAppContainer({
          settings: mockSettingsWithTitleEnabled,
        }),
      );

      // Assert: Check that title was updated with default Idle text
      const titleWrites = mocks.mockStdout.write.mock.calls.filter((call) =>
        call[0].includes('\x1b]0;'),
      );

      expect(titleWrites).toHaveLength(1);
      expect(titleWrites[0][0]).toBe(
        `\x1b]0;${'◇  Ready (workspace)'.padEnd(80, ' ')}\x07`,
      );
      unmount();
    });

    it('should update terminal title when in WaitingForConfirmation state with thought subject', async () => {
      // Arrange: Set up mock settings with showStatusInTitle enabled
      const mockSettingsWithTitleEnabled = createMockSettings({
        ui: {
          showStatusInTitle: true,
          hideWindowTitle: false,
        },
      });

      // Mock the streaming state and thought
      const thoughtSubject = 'Confirm tool execution';
      mockedUseGeminiStream.mockReturnValue({
        ...DEFAULT_GEMINI_STREAM_MOCK,
        streamingState: 'waiting_for_confirmation',
        thought: { subject: thoughtSubject },
      });

      // Act: Render the container
      const { unmount } = await act(async () =>
        renderAppContainer({
          settings: mockSettingsWithTitleEnabled,
        }),
      );

      // Assert: Check that title was updated with confirmation text
      const titleWrites = mocks.mockStdout.write.mock.calls.filter((call) =>
        call[0].includes('\x1b]0;'),
      );

      expect(titleWrites).toHaveLength(1);
      expect(titleWrites[0][0]).toBe(
        `\x1b]0;${'✋  Action Required (workspace)'.padEnd(80, ' ')}\x07`,
      );
      unmount();
    });

    describe('Shell Focus Action Required', () => {
      beforeEach(async () => {
        vi.useFakeTimers();
        // Use real implementation for these tests to verify title updates
        const actual = await vi.importActual<
          typeof import('./hooks/useShellInactivityStatus.js')
        >('./hooks/useShellInactivityStatus.js');
        mockedUseShellInactivityStatus.mockImplementation(
          actual.useShellInactivityStatus,
        );
      });

      afterEach(() => {
        vi.useRealTimers();
      });

      it('should show Action Required in title after a delay when shell is awaiting focus', async () => {
        const startTime = 1000000;
        vi.setSystemTime(startTime);

        // Arrange: Set up mock settings with showStatusInTitle enabled
        const mockSettingsWithTitleEnabled = createMockSettings({
          ui: {
            showStatusInTitle: true,
            hideWindowTitle: false,
          },
        });

        // Mock an active shell pty but not focused
        mockedUseGeminiStream.mockReturnValue({
          ...DEFAULT_GEMINI_STREAM_MOCK,
          streamingState: 'responding',
          thought: { subject: 'Executing shell command' },
          pendingToolCalls: [],
          activePtyId: 'pty-1',
          lastOutputTime: startTime + 100, // Trigger aggressive delay
          retryStatus: null,
        });

        vi.spyOn(mockConfig, 'isInteractive').mockReturnValue(true);
        vi.spyOn(mockConfig, 'isInteractiveShellEnabled').mockReturnValue(true);

        // Act: Render the container (embeddedShellFocused is false by default in state)
        const { unmount } = await act(async () =>
          renderAppContainer({
            settings: mockSettingsWithTitleEnabled,
          }),
        );

        // Initially it should show the working status
        const titleWrites = mocks.mockStdout.write.mock.calls.filter((call) =>
          call[0].includes('\x1b]0;'),
        );
        expect(titleWrites[titleWrites.length - 1][0]).toContain(
          '✦  Executing shell command',
        );

        // Fast-forward time by 40 seconds
        await act(async () => {
          await vi.advanceTimersByTimeAsync(40000);
        });

        // Now it should show Action Required
        const titleWritesDelayed = mocks.mockStdout.write.mock.calls.filter(
          (call) => call[0].includes('\x1b]0;'),
        );
        const lastTitle = titleWritesDelayed[titleWritesDelayed.length - 1][0];
        expect(lastTitle).toContain('✋  Action Required');

        unmount();
      });

      it('should show Working… in title for redirected commands after 2 mins', async () => {
        const startTime = 1000000;
        vi.setSystemTime(startTime);

        // Arrange: Set up mock settings with showStatusInTitle enabled
        const mockSettingsWithTitleEnabled = createMockSettings({
          ui: {
            showStatusInTitle: true,
            hideWindowTitle: false,
          },
        });

        // Mock an active shell pty with redirection active
        mockedUseGeminiStream.mockReturnValue({
          ...DEFAULT_GEMINI_STREAM_MOCK,
          streamingState: 'responding',
          thought: { subject: 'Executing shell command' },
          pendingToolCalls: [
            {
              request: {
                name: 'run_shell_command',
                args: { command: 'ls > out' },
              },
              status: CoreToolCallStatus.Executing,
            } as unknown as TrackedToolCall,
          ],
          activePtyId: 'pty-1',
          lastOutputTime: startTime,
          retryStatus: null,
        });

        vi.spyOn(mockConfig, 'isInteractive').mockReturnValue(true);
        vi.spyOn(mockConfig, 'isInteractiveShellEnabled').mockReturnValue(true);

        const { unmount } = await act(async () =>
          renderAppContainer({
            settings: mockSettingsWithTitleEnabled,
          }),
        );

        // Fast-forward time by 65 seconds - should still NOT be Action Required
        await act(async () => {
          await vi.advanceTimersByTimeAsync(65000);
        });

        const titleWritesMid = mocks.mockStdout.write.mock.calls.filter(
          (call) => call[0].includes('\x1b]0;'),
        );
        expect(titleWritesMid[titleWritesMid.length - 1][0]).not.toContain(
          '✋  Action Required',
        );

        // Fast-forward to 2 minutes (120000ms)
        await act(async () => {
          await vi.advanceTimersByTimeAsync(60000);
        });

        const titleWritesEnd = mocks.mockStdout.write.mock.calls.filter(
          (call) => call[0].includes('\x1b]0;'),
        );
        expect(titleWritesEnd[titleWritesEnd.length - 1][0]).toContain(
          '⏲  Working…',
        );

        unmount();
      });

      it('should show Working… in title for silent non-redirected commands after 1 min', async () => {
        const startTime = 1000000;
        vi.setSystemTime(startTime);

        // Arrange: Set up mock settings with showStatusInTitle enabled
        const mockSettingsWithTitleEnabled = createMockSettings({
          ui: {
            showStatusInTitle: true,
            hideWindowTitle: false,
          },
        });

        // Mock an active shell pty with NO output since operation started (silent)
        mockedUseGeminiStream.mockReturnValue({
          ...DEFAULT_GEMINI_STREAM_MOCK,
          streamingState: 'responding',
          thought: { subject: 'Executing shell command' },
          pendingToolCalls: [],
          activePtyId: 'pty-1',
          lastOutputTime: startTime, // lastOutputTime <= operationStartTime
          retryStatus: null,
        });

        vi.spyOn(mockConfig, 'isInteractive').mockReturnValue(true);
        vi.spyOn(mockConfig, 'isInteractiveShellEnabled').mockReturnValue(true);

        const { unmount } = await act(async () =>
          renderAppContainer({
            settings: mockSettingsWithTitleEnabled,
          }),
        );

        // Fast-forward time by 65 seconds
        await act(async () => {
          await vi.advanceTimersByTimeAsync(65000);
        });

        const titleWrites = mocks.mockStdout.write.mock.calls.filter((call) =>
          call[0].includes('\x1b]0;'),
        );
        const lastTitle = titleWrites[titleWrites.length - 1][0];
        // Should show Working… (⏲) instead of Action Required (✋)
        expect(lastTitle).toContain('⏲  Working…');

        unmount();
      });

      it('should NOT show Action Required in title if shell is streaming output', async () => {
        const startTime = 1000000;
        vi.setSystemTime(startTime);

        // Arrange: Set up mock settings with showStatusInTitle enabled
        const mockSettingsWithTitleEnabled = createMockSettings({
          ui: {
            showStatusInTitle: true,
            hideWindowTitle: false,
          },
        });

        // Mock an active shell pty but not focused
        let lastOutputTime = startTime + 1000;
        mockedUseGeminiStream.mockImplementation(() => ({
          ...DEFAULT_GEMINI_STREAM_MOCK,
          streamingState: 'responding',
          thought: { subject: 'Executing shell command' },
          activePtyId: 'pty-1',
          lastOutputTime,
        }));

        vi.spyOn(mockConfig, 'isInteractive').mockReturnValue(true);
        vi.spyOn(mockConfig, 'isInteractiveShellEnabled').mockReturnValue(true);

        // Act: Render the container
        const { unmount, rerender } = await act(async () =>
          renderAppContainer({
            settings: mockSettingsWithTitleEnabled,
          }),
        );

        // Fast-forward time by 20 seconds
        await act(async () => {
          await vi.advanceTimersByTimeAsync(20000);
        });

        // Update lastOutputTime to simulate new output
        lastOutputTime = startTime + 21000;
        mockedUseGeminiStream.mockImplementation(() => ({
          ...DEFAULT_GEMINI_STREAM_MOCK,
          streamingState: 'responding',
          thought: { subject: 'Executing shell command' },
          activePtyId: 'pty-1',
          lastOutputTime,
        }));

        // Rerender to propagate the new lastOutputTime
        await act(async () => {
          rerender(getAppContainer({ settings: mockSettingsWithTitleEnabled }));
        });

        // Fast-forward time by another 20 seconds
        // Total time elapsed: 40s.
        // Time since last output: 20s.
        // It should NOT show Action Required yet.
        await act(async () => {
          await vi.advanceTimersByTimeAsync(20000);
        });

        const titleWritesAfterOutput = mocks.mockStdout.write.mock.calls.filter(
          (call) => call[0].includes('\x1b]0;'),
        );
        const lastTitle =
          titleWritesAfterOutput[titleWritesAfterOutput.length - 1][0];
        expect(lastTitle).not.toContain('✋  Action Required');
        expect(lastTitle).toContain('✦  Executing shell command');

        // Fast-forward another 40 seconds (Total 60s since last output)
        await act(async () => {
          await vi.advanceTimersByTimeAsync(40000);
        });

        // Now it SHOULD show Action Required
        const titleWrites = mocks.mockStdout.write.mock.calls.filter((call) =>
          call[0].includes('\x1b]0;'),
        );
        const lastTitleFinal = titleWrites[titleWrites.length - 1][0];
        expect(lastTitleFinal).toContain('✋  Action Required');

        unmount();
      });
    });

    it('should pad title to exactly 80 characters', async () => {
      // Arrange: Set up mock settings with showStatusInTitle enabled
      const mockSettingsWithTitleEnabled = createMockSettings({
        ui: {
          showStatusInTitle: true,
          hideWindowTitle: false,
        },
      });

      // Mock the streaming state and thought with a short subject
      const shortTitle = 'Short';
      mockedUseGeminiStream.mockReturnValue({
        ...DEFAULT_GEMINI_STREAM_MOCK,
        streamingState: 'responding',
        thought: { subject: shortTitle },
      });

      // Act: Render the container
      const { unmount } = await act(async () =>
        renderAppContainer({
          settings: mockSettingsWithTitleEnabled,
        }),
      );

      // Assert: Check that title is padded to exactly 80 characters
      const titleWrites = mocks.mockStdout.write.mock.calls.filter((call) =>
        call[0].includes('\x1b]0;'),
      );

      expect(titleWrites).toHaveLength(1);
      const calledWith = titleWrites[0][0];
      const expectedTitle = `✦  ${shortTitle} (workspace)`.padEnd(80, ' ');
      const expectedEscapeSequence = `\x1b]0;${expectedTitle}\x07`;
      expect(calledWith).toBe(expectedEscapeSequence);
      unmount();
    });

    it('should use correct ANSI escape code format', async () => {
      // Arrange: Set up mock settings with showStatusInTitle enabled
      const mockSettingsWithTitleEnabled = createMockSettings({
        ui: {
          showStatusInTitle: true,
          hideWindowTitle: false,
        },
      });

      // Mock the streaming state and thought
      const title = 'Test Title';
      mockedUseGeminiStream.mockReturnValue({
        ...DEFAULT_GEMINI_STREAM_MOCK,
        streamingState: 'responding',
        thought: { subject: title },
      });

      // Act: Render the container
      const { unmount } = await act(async () =>
        renderAppContainer({
          settings: mockSettingsWithTitleEnabled,
        }),
      );

      // Assert: Check that the correct ANSI escape sequence is used
      const titleWrites = mocks.mockStdout.write.mock.calls.filter((call) =>
        call[0].includes('\x1b]0;'),
      );

      expect(titleWrites).toHaveLength(1);
      const expectedEscapeSequence = `\x1b]0;${`✦  ${title} (workspace)`.padEnd(80, ' ')}\x07`;
      expect(titleWrites[0][0]).toBe(expectedEscapeSequence);
      unmount();
    });

    it('should use CLI_TITLE environment variable when set', async () => {
      // Arrange: Set up mock settings with showStatusInTitle disabled (so it shows suffix)
      const mockSettingsWithTitleDisabled = createMockSettings({
        ui: {
          showStatusInTitle: false,
          hideWindowTitle: false,
        },
      });

      // Mock CLI_TITLE environment variable
      vi.stubEnv('CLI_TITLE', 'Custom Gemini Title');

      // Mock the streaming state
      mockedUseGeminiStream.mockReturnValue({
        ...DEFAULT_GEMINI_STREAM_MOCK,
        streamingState: 'responding',
      });

      // Act: Render the container
      const { unmount } = await act(async () =>
        renderAppContainer({
          settings: mockSettingsWithTitleDisabled,
        }),
      );

      // Assert: Check that title was updated with CLI_TITLE value
      const titleWrites = mocks.mockStdout.write.mock.calls.filter((call) =>
        call[0].includes('\x1b]0;'),
      );

      expect(titleWrites).toHaveLength(1);
      expect(titleWrites[0][0]).toBe(
        `\x1b]0;${'✦  Working… (Custom Gemini Title)'.padEnd(80, ' ')}\x07`,
      );
      unmount();
    });
  });

  describe('Queue Error Message', () => {
    beforeEach(() => {
      vi.useFakeTimers();
    });

    afterEach(() => {
      vi.useRealTimers();
      vi.restoreAllMocks();
    });

    it('should set and clear the queue error message after a timeout', async () => {
      const { rerender, unmount } = await act(async () => renderAppContainer());
      await act(async () => {
        vi.advanceTimersByTime(0);
      });

      expect(capturedUIState.queueErrorMessage).toBeNull();

      act(() => {
        capturedUIActions.setQueueErrorMessage('Test error');
      });
      rerender(getAppContainer());
      expect(capturedUIState.queueErrorMessage).toBe('Test error');

      act(() => {
        vi.advanceTimersByTime(3000);
      });
      rerender(getAppContainer());
      expect(capturedUIState.queueErrorMessage).toBeNull();
      unmount();
    });

    it('should reset the timer if a new error message is set', async () => {
      const { rerender, unmount } = await act(async () => renderAppContainer());
      await act(async () => {
        vi.advanceTimersByTime(0);
      });

      act(() => {
        capturedUIActions.setQueueErrorMessage('First error');
      });
      rerender(getAppContainer());
      expect(capturedUIState.queueErrorMessage).toBe('First error');

      act(() => {
        vi.advanceTimersByTime(1500);
      });

      act(() => {
        capturedUIActions.setQueueErrorMessage('Second error');
      });
      rerender(getAppContainer());
      expect(capturedUIState.queueErrorMessage).toBe('Second error');

      act(() => {
        vi.advanceTimersByTime(2000);
      });
      rerender(getAppContainer());
      expect(capturedUIState.queueErrorMessage).toBe('Second error');

      // 5. Advance time past the 3 second timeout from the second message
      act(() => {
        vi.advanceTimersByTime(1000);
      });
      rerender(getAppContainer());
      expect(capturedUIState.queueErrorMessage).toBeNull();
      unmount();
    });
  });

  describe('Keyboard Input Handling (CTRL+C / CTRL+D)', () => {
    let mockHandleSlashCommand: Mock;
    let mockCancelOngoingRequest: Mock;
    let rerender: () => void;
    let unmount: () => void;
    let stdin: Awaited<ReturnType<typeof render>>['stdin'];

    // Helper function to reduce boilerplate in tests
    const setupKeypressTest = async () => {
      const renderResult = await act(async () => renderAppContainer());
      stdin = renderResult.stdin;
      await act(async () => {
        vi.advanceTimersByTime(0);
      });

      rerender = () => {
        renderResult.rerender(getAppContainer());
      };
      unmount = renderResult.unmount;
    };

    const pressKey = (sequence: string, times = 1) => {
      for (let i = 0; i < times; i++) {
        act(() => {
          stdin.write(sequence);
        });
        rerender();
      }
    };

    beforeEach(() => {
      // Mock slash command handler
      mockHandleSlashCommand = vi.fn();
      mockedUseSlashCommandProcessor.mockReturnValue({
        handleSlashCommand: mockHandleSlashCommand,
        slashCommands: [],
        pendingHistoryItems: [],
        commandContext: {},
        shellConfirmationRequest: null,
        confirmationRequest: null,
      });

      // Mock request cancellation
      mockCancelOngoingRequest = vi.fn();
      mockedUseGeminiStream.mockReturnValue({
        ...DEFAULT_GEMINI_STREAM_MOCK,
        cancelOngoingRequest: mockCancelOngoingRequest,
      });

      // Default empty text buffer
      mockedUseTextBuffer.mockReturnValue({
        text: '',
        setText: vi.fn(),
        lines: [''],
        cursor: [0, 0],
        handleInput: vi.fn().mockReturnValue(false),
      });

      vi.useFakeTimers();
    });

    afterEach(() => {
      vi.useRealTimers();
      vi.restoreAllMocks();
    });

    describe('CTRL+C', () => {
      it('should cancel ongoing request on first press', async () => {
        mockedUseGeminiStream.mockReturnValue({
          ...DEFAULT_GEMINI_STREAM_MOCK,
          streamingState: 'responding',
          cancelOngoingRequest: mockCancelOngoingRequest,
        });
        await setupKeypressTest();

        pressKey('\x03'); // Ctrl+C

        expect(mockCancelOngoingRequest).toHaveBeenCalledTimes(1);
        expect(mockHandleSlashCommand).not.toHaveBeenCalled();
        unmount();
      });

      it('should quit on second press', async () => {
        await setupKeypressTest();

        pressKey('\x03', 2); // Ctrl+C

        expect(mockCancelOngoingRequest).toHaveBeenCalledTimes(2);
        expect(mockHandleSlashCommand).toHaveBeenCalledWith(
          '/quit',
          undefined,
          undefined,
          false,
        );
        unmount();
      });

      it('should reset press count after a timeout', async () => {
        await setupKeypressTest();

        pressKey('\x03'); // Ctrl+C
        expect(mockHandleSlashCommand).not.toHaveBeenCalled();

        // Advance timer past the reset threshold
        act(() => {
          vi.advanceTimersByTime(WARNING_PROMPT_DURATION_MS + 1);
        });

        pressKey('\x03'); // Ctrl+C
        expect(mockHandleSlashCommand).not.toHaveBeenCalled();
        unmount();
      });
    });

    describe('CTRL+D', () => {
      it('should quit on second press if buffer is empty', async () => {
        await setupKeypressTest();

        pressKey('\x04', 2); // Ctrl+D

        expect(mockHandleSlashCommand).toHaveBeenCalledWith(
          '/quit',
          undefined,
          undefined,
          false,
        );
        unmount();
      });

      it('should NOT quit if buffer is not empty', async () => {
        mockedUseTextBuffer.mockReturnValue({
          text: 'some text',
          setText: vi.fn(),
          lines: ['some text'],
          cursor: [0, 9], // At the end
          handleInput: vi.fn().mockReturnValue(false),
        });
        await setupKeypressTest();

        pressKey('\x04'); // Ctrl+D

        // Should only be called once, so count is 1, not quitting yet.
        expect(mockHandleSlashCommand).not.toHaveBeenCalled();

        pressKey('\x04'); // Ctrl+D
        // It should still not quit because buffer is non-empty.
        expect(mockHandleSlashCommand).not.toHaveBeenCalled();
        unmount();
      });

      it('should reset press count after a timeout', async () => {
        await setupKeypressTest();

        pressKey('\x04'); // Ctrl+D
        expect(mockHandleSlashCommand).not.toHaveBeenCalled();

        // Advance timer past the reset threshold
        act(() => {
          vi.advanceTimersByTime(WARNING_PROMPT_DURATION_MS + 1);
        });

        pressKey('\x04'); // Ctrl+D
        expect(mockHandleSlashCommand).not.toHaveBeenCalled();
        unmount();
      });
    });

    describe('CTRL+Z', () => {
      it('should call handleSuspend', async () => {
        const handleSuspend = vi.fn();
        mockedUseSuspend.mockReturnValue({ handleSuspend });
        await setupKeypressTest();

        pressKey('\x1A'); // Ctrl+Z

        expect(handleSuspend).toHaveBeenCalledTimes(1);
        unmount();
      });
    });

    describe('Focus Handling (Tab / Shift+Tab)', () => {
      beforeEach(() => {
        // Mock activePtyId to enable focus
        mockedUseGeminiStream.mockReturnValue({
          ...DEFAULT_GEMINI_STREAM_MOCK,
          activePtyId: 1,
        });
      });

      it('should focus shell input on Tab', async () => {
        await setupKeypressTest();

        pressKey('\t');

        expect(capturedUIState.embeddedShellFocused).toBe(true);
        unmount();
      });

      it('should unfocus shell input on Shift+Tab', async () => {
        await setupKeypressTest();

        // Focus first
        pressKey('\t');
        expect(capturedUIState.embeddedShellFocused).toBe(true);

        // Unfocus via Shift+Tab
        pressKey('\x1b[Z');
        expect(capturedUIState.embeddedShellFocused).toBe(false);
        unmount();
      });

      it('should auto-unfocus when activePtyId becomes null', async () => {
        // Start with active pty and focused
        mockedUseGeminiStream.mockReturnValue({
          ...DEFAULT_GEMINI_STREAM_MOCK,
          activePtyId: 1,
        });

        const renderResult = await act(async () => render(getAppContainer()));
        await act(async () => {
          vi.advanceTimersByTime(0);
        });

        // Focus it
        act(() => {
          renderResult.stdin.write('\t');
        });
        expect(capturedUIState.embeddedShellFocused).toBe(true);

        // Now mock activePtyId becoming null
        mockedUseGeminiStream.mockReturnValue({
          ...DEFAULT_GEMINI_STREAM_MOCK,
          activePtyId: null,
        });

        // Rerender to trigger useEffect
        await act(async () => {
          renderResult.rerender(getAppContainer());
        });

        expect(capturedUIState.embeddedShellFocused).toBe(false);
        renderResult.unmount();
      });

      it('should focus background shell on Tab when already visible (not toggle it off)', async () => {
        const mockToggleBackgroundTask = vi.fn();
        mockedUseGeminiStream.mockReturnValue({
          ...DEFAULT_GEMINI_STREAM_MOCK,
          activePtyId: null,
          isBackgroundTaskVisible: true,
          backgroundTasks: new Map([[123, { pid: 123, status: 'running' }]]),
          toggleBackgroundTasks: mockToggleBackgroundTask,
        });

        await setupKeypressTest();

        // Initially not focused
        expect(capturedUIState.embeddedShellFocused).toBe(false);

        // Press Tab
        pressKey('\t');

        // Should be focused
        expect(capturedUIState.embeddedShellFocused).toBe(true);
        // Should NOT have toggled (closed) the shell
        expect(mockToggleBackgroundTask).not.toHaveBeenCalled();

        unmount();
      });
    });

    describe('Background Shell Toggling (CTRL+B)', () => {
      it('should toggle background shell on Ctrl+B even if visible but not focused', async () => {
        const mockToggleBackgroundTask = vi.fn();
        mockedUseGeminiStream.mockReturnValue({
          ...DEFAULT_GEMINI_STREAM_MOCK,
          activePtyId: null,
          isBackgroundTaskVisible: true,
          backgroundTasks: new Map([[123, { pid: 123, status: 'running' }]]),
          toggleBackgroundTasks: mockToggleBackgroundTask,
        });

        await setupKeypressTest();

        // Initially not focused, but visible
        expect(capturedUIState.embeddedShellFocused).toBe(false);

        // Press Ctrl+B
        pressKey('\x02');

        // Should have toggled (closed) the shell
        expect(mockToggleBackgroundTask).toHaveBeenCalled();
        // Should be unfocused
        expect(capturedUIState.embeddedShellFocused).toBe(false);

        unmount();
      });

      it('should show and focus background shell on Ctrl+B if hidden', async () => {
        const mockToggleBackgroundTask = vi.fn();
        const geminiStreamMock = {
          ...DEFAULT_GEMINI_STREAM_MOCK,
          activePtyId: null,
          isBackgroundTaskVisible: false,
          backgroundTasks: new Map([[123, { pid: 123, status: 'running' }]]),
          toggleBackgroundTasks: mockToggleBackgroundTask,
        };
        mockedUseGeminiStream.mockReturnValue(geminiStreamMock);

        await setupKeypressTest();

        // Update the mock state when toggled to simulate real behavior
        mockToggleBackgroundTask.mockImplementation(() => {
          geminiStreamMock.isBackgroundTaskVisible = true;
        });

        // Press Ctrl+B
        pressKey('\x02');

        // Should have toggled (shown) the shell
        expect(mockToggleBackgroundTask).toHaveBeenCalled();
        // Should be focused
        expect(capturedUIState.embeddedShellFocused).toBe(true);

        unmount();
      });
    });
  });

  describe('Shortcuts Help Visibility', () => {
    let handleGlobalKeypress: (key: Key) => boolean;
    let mockedUseKeypress: Mock;
    let rerender: () => void;
    let unmount: () => void;

    const setupShortcutsVisibilityTest = async () => {
      const renderResult = await act(async () => renderAppContainer());
      await act(async () => {
        vi.advanceTimersByTime(0);
      });
      rerender = () => renderResult.rerender(getAppContainer());
      unmount = renderResult.unmount;
    };

    const pressKey = (key: Partial<Key>) => {
      act(() => {
        handleGlobalKeypress({
          name: 'r',
          shift: false,
          alt: false,
          ctrl: false,
          cmd: false,
          insertable: false,
          sequence: '',
          ...key,
        } as Key);
      });
      rerender();
    };

    beforeEach(() => {
      mockedUseKeypress = vi.spyOn(useKeypressModule, 'useKeypress') as Mock;
      mockedUseKeypress.mockImplementation(
        (callback: (key: Key) => boolean, options: { isActive: boolean }) => {
          // AppContainer registers multiple keypress handlers; capture only
          // active handlers so inactive copy-mode handler doesn't override.
          if (options?.isActive) {
            handleGlobalKeypress = callback;
          }
        },
      );
      vi.useFakeTimers();
    });

    afterEach(() => {
      mockedUseKeypress.mockRestore();
      vi.useRealTimers();
      vi.restoreAllMocks();
    });

    it('dismisses shortcuts help when a registered hotkey is pressed', async () => {
      await setupShortcutsVisibilityTest();

      act(() => {
        capturedUIActions.setShortcutsHelpVisible(true);
      });
      rerender();
      expect(capturedUIState.shortcutsHelpVisible).toBe(true);

      pressKey({ name: 'r', ctrl: true, sequence: '\x12' }); // Ctrl+R
      expect(capturedUIState.shortcutsHelpVisible).toBe(false);

      unmount();
    });

    it('dismisses shortcuts help when streaming starts', async () => {
      await setupShortcutsVisibilityTest();

      act(() => {
        capturedUIActions.setShortcutsHelpVisible(true);
      });
      rerender();
      expect(capturedUIState.shortcutsHelpVisible).toBe(true);

      mockedUseGeminiStream.mockReturnValue({
        ...DEFAULT_GEMINI_STREAM_MOCK,
        streamingState: 'responding',
      });

      await act(async () => {
        rerender();
      });
      expect(capturedUIState.shortcutsHelpVisible).toBe(false);

      unmount();
    });

    it('dismisses shortcuts help when action-required confirmation appears', async () => {
      await setupShortcutsVisibilityTest();

      act(() => {
        capturedUIActions.setShortcutsHelpVisible(true);
      });
      rerender();
      expect(capturedUIState.shortcutsHelpVisible).toBe(true);

      mockedUseSlashCommandProcessor.mockReturnValue({
        handleSlashCommand: vi.fn(),
        slashCommands: [],
        pendingHistoryItems: [],
        commandContext: {},
        shellConfirmationRequest: null,
        confirmationRequest: {
          prompt: 'Confirm this action?',
          onConfirm: vi.fn(),
        },
      });

      await act(async () => {
        rerender();
      });
      expect(capturedUIState.shortcutsHelpVisible).toBe(false);

      unmount();
    });
  });

  describe('Copy Mode (F9)', () => {
    let rerender: () => void;
    let unmount: () => void;
    let stdin: Awaited<ReturnType<typeof render>>['stdin'];

    const setupCopyModeTest = async (
      isAlternateMode = false,
      childHandler?: Mock,
    ) => {
      vi.spyOn(mockConfig, 'getUseTerminalBuffer').mockReturnValue(false);

      vi.spyOn(mockConfig, 'getUseAlternateBuffer').mockReturnValue(
        isAlternateMode,
      );

      // Update settings for this test run
      const testSettings = createMockSettings({
        ui: { useAlternateBuffer: isAlternateMode },
      });

      function TestChild() {
        useKeypress(childHandler || (() => {}), {
          isActive: !!childHandler,
          priority: true,
        });
        return null;
      }

      const getTree = (settings: LoadedSettings) => (
        <SettingsContext.Provider value={settings}>
          <KeypressProvider config={mockConfig}>
            <OverflowProvider>
              <AppContainer
                config={mockConfig}
                version="1.0.0"
                initializationResult={mockInitResult}
              />
              <TestChild />
            </OverflowProvider>
          </KeypressProvider>
        </SettingsContext.Provider>
      );

      const renderResult = await act(async () => render(getTree(testSettings)));
      stdin = renderResult.stdin;
      await act(async () => {
        vi.advanceTimersByTime(0);
      });

      rerender = () => renderResult.rerender(getTree(testSettings));
      unmount = renderResult.unmount;
    };

    beforeEach(() => {
      mocks.mockStdout.write.mockClear();
      (disableMouseEvents as import('vitest').Mock).mockClear();

      vi.useFakeTimers();
    });

    afterEach(() => {
      vi.useRealTimers();
      vi.restoreAllMocks();
    });

    describe.each([
      {
        isAlternateMode: false,
        shouldEnable: false,
        modeName: 'Normal Mode',
      },
      {
        isAlternateMode: true,
        shouldEnable: true,
        modeName: 'Alternate Buffer Mode',
      },
    ])('$modeName', ({ isAlternateMode, shouldEnable }) => {
      it(`should ${shouldEnable ? 'toggle' : 'NOT toggle'} mouse off when F9 is pressed`, async () => {
        await setupCopyModeTest(isAlternateMode);
        mocks.mockStdout.write.mockClear(); // Clear initial enable call
        (disableMouseEvents as import('vitest').Mock).mockClear();

        act(() => {
          stdin.write('\x1b[20~'); // F9
        });
        rerender();

        if (shouldEnable) {
          expect(disableMouseEvents).toHaveBeenCalled();
        } else {
          expect(disableMouseEvents).not.toHaveBeenCalled();
        }
        unmount();
      });

      if (shouldEnable) {
        it('should toggle mouse back on when F9 is pressed again', async () => {
          await setupCopyModeTest(isAlternateMode);
          (writeToStdout as Mock).mockClear();

          // Turn it on (disable mouse)
          act(() => {
            stdin.write('\x1b[20~'); // F9
          });
          rerender();
          expect(disableMouseEvents).toHaveBeenCalled();

          // Turn it off (enable mouse)
          act(() => {
            stdin.write('a'); // Any key should exit copy mode
          });
          rerender();

          expect(enableMouseEvents).toHaveBeenCalled();
          unmount();
        });

        it('should exit copy mode on non-scroll key press', async () => {
          await setupCopyModeTest(isAlternateMode);

          // Enter copy mode
          act(() => {
            stdin.write('\x1b[20~'); // F9
          });
          rerender();

          (writeToStdout as Mock).mockClear();

          // Press any other key
          act(() => {
            stdin.write('a');
          });
          rerender();

          // Should have re-enabled mouse
          expect(enableMouseEvents).toHaveBeenCalled();
          unmount();
        });

        it('should not exit copy mode on PageDown and should pass it through', async () => {
          const childHandler = vi.fn().mockReturnValue(false);
          await setupCopyModeTest(true, childHandler);

          // Enter copy mode
          act(() => {
            stdin.write('\x13'); // Ctrl+S
          });
          rerender();
          expect(disableMouseEvents).toHaveBeenCalled();

          childHandler.mockClear();
          (enableMouseEvents as Mock).mockClear();

          // PageDown should be passed through to lower-priority handlers.
          act(() => {
            stdin.write('\x1b[6~');
          });
          rerender();

          expect(enableMouseEvents).not.toHaveBeenCalled();
          expect(childHandler).toHaveBeenCalled();
          expect(childHandler).toHaveBeenCalledWith(
            expect.objectContaining({ name: 'pagedown' }),
          );
          unmount();
        });

        it('should not exit copy mode on Shift+Down and should pass it through', async () => {
          const childHandler = vi.fn().mockReturnValue(false);
          await setupCopyModeTest(true, childHandler);

          // Enter copy mode
          act(() => {
            stdin.write('\x13'); // Ctrl+S
          });
          rerender();
          expect(disableMouseEvents).toHaveBeenCalled();

          childHandler.mockClear();
          (enableMouseEvents as Mock).mockClear();

          act(() => {
            stdin.write('\x1b[1;2B'); // Shift+Down
          });
          rerender();

          expect(enableMouseEvents).not.toHaveBeenCalled();
          expect(childHandler).toHaveBeenCalled();
          expect(childHandler).toHaveBeenCalledWith(
            expect.objectContaining({ name: 'down', shift: true }),
          );
          unmount();
        });

        it('should have higher priority than other priority listeners when enabled', async () => {
          // 1. Initial state with a child component's priority listener (already subscribed)
          // It should NOT handle Ctrl+S so we can enter copy mode.
          const childHandler = vi.fn().mockReturnValue(false);
          await setupCopyModeTest(true, childHandler);

          // 2. Enter copy mode
          act(() => {
            stdin.write('\x1b[20~'); // F9
          });
          rerender();

          // 3. Verify we are in copy mode
          expect(disableMouseEvents).toHaveBeenCalled();

          // 4. Press any key
          childHandler.mockClear();
          // Now childHandler should return true for other keys, simulating a greedy listener
          childHandler.mockReturnValue(true);

          act(() => {
            stdin.write('a');
          });
          rerender();

          // 5. Verify that the exit handler took priority and childHandler was NOT called
          expect(childHandler).not.toHaveBeenCalled();
          expect(enableMouseEvents).toHaveBeenCalled();
          unmount();
        });
      }
    });
  });

  describe('Model Dialog Integration', () => {
    it('should provide isModelDialogOpen in the UIStateContext', async () => {
      mockedUseModelCommand.mockReturnValue({
        isModelDialogOpen: true,
        openModelDialog: vi.fn(),
        closeModelDialog: vi.fn(),
      });

      const { unmount } = await act(async () => renderAppContainer());

      expect(capturedUIState.isModelDialogOpen).toBe(true);
      unmount();
    });

    it('should provide model dialog actions in the UIActionsContext', async () => {
      const mockCloseModelDialog = vi.fn();

      mockedUseModelCommand.mockReturnValue({
        isModelDialogOpen: false,
        openModelDialog: vi.fn(),
        closeModelDialog: mockCloseModelDialog,
      });

      const { unmount } = await act(async () => renderAppContainer());

      // Verify that the actions are correctly passed through context
      act(() => {
        capturedUIActions.closeModelDialog();
      });
      expect(mockCloseModelDialog).toHaveBeenCalled();
      unmount();
    });
  });

  describe('Agent Configuration Dialog Integration', () => {
    it('should initialize with dialog closed and no agent selected', async () => {
      const { unmount } = await act(async () => renderAppContainer());
      expect(capturedUIState.isAgentConfigDialogOpen).toBe(false);
      expect(capturedUIState.selectedAgentName).toBeUndefined();
      expect(capturedUIState.selectedAgentDisplayName).toBeUndefined();
      expect(capturedUIState.selectedAgentDefinition).toBeUndefined();
      unmount();
    });

    it('should update state when openAgentConfigDialog is called', async () => {
      const { unmount } = await act(async () => renderAppContainer());

      const agentDefinition = { name: 'test-agent' };
      act(() => {
        capturedUIActions.openAgentConfigDialog(
          'test-agent',
          'Test Agent',
          agentDefinition as unknown as AgentDefinition,
        );
      });

      expect(capturedUIState.isAgentConfigDialogOpen).toBe(true);
      expect(capturedUIState.selectedAgentName).toBe('test-agent');
      expect(capturedUIState.selectedAgentDisplayName).toBe('Test Agent');
      expect(capturedUIState.selectedAgentDefinition).toEqual(agentDefinition);
      unmount();
    });

    it('should clear state when closeAgentConfigDialog is called', async () => {
      const { unmount } = await act(async () => renderAppContainer());

      const agentDefinition = { name: 'test-agent' };
      act(() => {
        capturedUIActions.openAgentConfigDialog(
          'test-agent',
          'Test Agent',
          agentDefinition as unknown as AgentDefinition,
        );
      });

      expect(capturedUIState.isAgentConfigDialogOpen).toBe(true);

      act(() => {
        capturedUIActions.closeAgentConfigDialog();
      });

      expect(capturedUIState.isAgentConfigDialogOpen).toBe(false);
      expect(capturedUIState.selectedAgentName).toBeUndefined();
      expect(capturedUIState.selectedAgentDisplayName).toBeUndefined();
      expect(capturedUIState.selectedAgentDefinition).toBeUndefined();
      unmount();
    });
  });

  describe('CoreEvents Integration', () => {
    it('subscribes to UserFeedback and drains backlog on mount', async () => {
      const { unmount } = await act(async () => renderAppContainer());

      expect(mockCoreEvents.on).toHaveBeenCalledWith(
        CoreEvent.UserFeedback,
        expect.any(Function),
      );
      expect(mockCoreEvents.drainBacklogs).toHaveBeenCalledTimes(1);
      unmount();
    });

    it('unsubscribes from UserFeedback on unmount', async () => {
      let unmount: () => void;
      await act(async () => {
        const result = await renderAppContainer();
        unmount = result.unmount;
      });
      await waitFor(() => expect(capturedUIState).toBeTruthy());

      unmount!();

      expect(mockCoreEvents.off).toHaveBeenCalledWith(
        CoreEvent.UserFeedback,
        expect.any(Function),
      );
    });

    it('adds history item when UserFeedback event is received', async () => {
      let unmount: () => void;
      await act(async () => {
        const result = await renderAppContainer();
        unmount = result.unmount;
      });
      await waitFor(() => expect(capturedUIState).toBeTruthy());

      // Get the registered handler
      const handler = mockCoreEvents.on.mock.calls.find(
        (call: unknown[]) => call[0] === CoreEvent.UserFeedback,
      )?.[1];
      expect(handler).toBeDefined();

      // Simulate an event
      const payload: UserFeedbackPayload = {
        severity: 'error',
        message: 'Test error message',
      };
      act(() => {
        handler(payload);
      });

      expect(mockedUseHistory().addItem).toHaveBeenCalledWith(
        expect.objectContaining({
          type: 'error',
          text: 'Test error message',
        }),
        expect.any(Number),
      );
      unmount!();
    });

    it('updates currentModel when ModelChanged event is received', async () => {
      // Arrange: Mock initial model
      vi.spyOn(mockConfig, 'getModel').mockReturnValue('initial-model');

      let unmount: () => void;
      await act(async () => {
        const result = await renderAppContainer();
        unmount = result.unmount;
      });
      await waitFor(() => {
        expect(capturedUIState?.currentModel).toBe('initial-model');
      });

      // Get the registered handler for ModelChanged
      const handler = mockCoreEvents.on.mock.calls.find(
        (call: unknown[]) => call[0] === CoreEvent.ModelChanged,
      )?.[1];
      expect(handler).toBeDefined();

      // Act: Simulate ModelChanged event
      // Update config mock to return new model since the handler reads from config
      vi.spyOn(mockConfig, 'getModel').mockReturnValue('new-model');
      act(() => {
        handler({ model: 'new-model' });
      });

      // Assert: Verify model is updated
      await waitFor(() => {
        expect(capturedUIState.currentModel).toBe('new-model');
      });
      unmount!();
    });

    it('provides activeHooks from useHookDisplayState', async () => {
      const mockHooks = [{ name: 'hook1', eventName: 'event1' }];
      mockedUseHookDisplayState.mockReturnValue(mockHooks);

      let unmount: () => void;
      await act(async () => {
        const result = await renderAppContainer();
        unmount = result.unmount;
      });
      await waitFor(() => expect(capturedUIState).toBeTruthy());

      expect(capturedUIState.activeHooks).toEqual(mockHooks);
      unmount!();
    });

    it('handles consent request events', async () => {
      let unmount: () => void;
      await act(async () => {
        const result = await renderAppContainer();
        unmount = result.unmount;
      });
      await waitFor(() => expect(capturedUIState).toBeTruthy());

      const handler = mockCoreEvents.on.mock.calls.find(
        (call: unknown[]) => call[0] === CoreEvent.ConsentRequest,
      )?.[1];
      expect(handler).toBeDefined();

      const onConfirm = vi.fn();
      const payload = {
        prompt: 'Do you consent?',
        onConfirm,
      };

      act(() => {
        handler(payload);
      });

      expect(capturedUIState.authConsentRequest).toBeDefined();
      expect(capturedUIState.authConsentRequest?.prompt).toBe(
        'Do you consent?',
      );

      act(() => {
        capturedUIState.authConsentRequest?.onConfirm(true);
      });

      expect(onConfirm).toHaveBeenCalledWith(true);
      expect(capturedUIState.authConsentRequest).toBeNull();
      unmount!();
    });

    it('unsubscribes from ConsentRequest on unmount', async () => {
      let unmount: () => void;
      await act(async () => {
        const result = await renderAppContainer();
        unmount = result.unmount;
      });
      await waitFor(() => expect(capturedUIState).toBeTruthy());

      unmount!();

      expect(mockCoreEvents.off).toHaveBeenCalledWith(
        CoreEvent.ConsentRequest,
        expect.any(Function),
      );
    });
  });

  describe('Banner Text', () => {
    it('should render placeholder banner text for USE_GEMINI auth type', async () => {
      const config = makeFakeConfig();
      vi.spyOn(config, 'getContentGeneratorConfig').mockReturnValue({
        authType: AuthType.USE_GEMINI,
        apiKey: 'fake-key',
      });
      let unmount: () => void;
      await act(async () => {
        const result = await renderAppContainer();
        unmount = result.unmount;
      });
      await waitFor(() => {
        expect(capturedUIState.bannerData.defaultText).toBeDefined();
        unmount!();
      });
    });
  });

  describe('onCancelSubmit Behavior', () => {
    let mockSetText: Mock;

    // Helper to extract arguments from the useGeminiStream hook call
    // This isolates the positional argument dependency to a single location
    const extractUseGeminiStreamArgs = (args: unknown[]) => ({
      onCancelSubmit: args[13] as (shouldRestorePrompt?: boolean) => void,
    });

    beforeEach(() => {
      mockSetText = vi.fn();
      mockedUseTextBuffer.mockReturnValue({
        text: '',
        setText: mockSetText,
      });
    });

    it('preserves buffer when cancelling, even if empty (user is in control)', async () => {
      const { unmount } = await act(async () => renderAppContainer());

      const { onCancelSubmit } = extractUseGeminiStreamArgs(
        mockedUseGeminiStream.mock.lastCall!,
      );

      act(() => {
        onCancelSubmit(false);
      });

      // Should NOT modify buffer when cancelling - user is in control
      expect(mockSetText).not.toHaveBeenCalled();

      unmount();
    });

    it('preserves prompt text when cancelling streaming, even if same as last message (regression test for issue #13387)', async () => {
      // Mock buffer with text that user typed while streaming (same as last message)
      const promptText = 'What is Python?';
      mockedUseTextBuffer.mockReturnValue({
        text: promptText,
        setText: mockSetText,
      });

      // Mock input history with same message
      mockedUseInputHistoryStore.mockReturnValue({
        inputHistory: [promptText],
        addInput: vi.fn(),
        initializeFromLogger: vi.fn(),
      });

      const { unmount } = await act(async () => renderAppContainer());

      const { onCancelSubmit } = extractUseGeminiStreamArgs(
        mockedUseGeminiStream.mock.lastCall!,
      );

      act(() => {
        // Simulate Escape key cancelling streaming (shouldRestorePrompt=false)
        onCancelSubmit(false);
      });

      // Should NOT call setText - prompt should be preserved regardless of content
      expect(mockSetText).not.toHaveBeenCalled();

      unmount();
    });

    it('restores the prompt when onCancelSubmit is called with shouldRestorePrompt=true (or undefined)', async () => {
      // Mock useInputHistoryStore to provide input history
      mockedUseInputHistoryStore.mockReturnValue({
        inputHistory: ['previous message'],
        addInput: vi.fn(),
        initializeFromLogger: vi.fn(),
      });

      const { unmount } = await act(async () => renderAppContainer());
      expect(capturedInputState.userMessages).toContain('previous message');

      const { onCancelSubmit } = extractUseGeminiStreamArgs(
        mockedUseGeminiStream.mock.lastCall!,
      );

      await act(async () => {
        onCancelSubmit(true);
      });

      expect(mockSetText).toHaveBeenCalledWith('previous message');

      unmount();
    });

    it('input history is independent from conversation history (survives /clear)', async () => {
      // This test verifies that input history (used for up-arrow navigation) is maintained
      // separately from conversation history and survives /clear operations.
      const mockAddInput = vi.fn();
      mockedUseInputHistoryStore.mockReturnValue({
        inputHistory: ['first prompt', 'second prompt'],
        addInput: mockAddInput,
        initializeFromLogger: vi.fn(),
      });

      const { rerender, unmount } = await act(async () => renderAppContainer());

      // Verify userMessages is populated from inputHistory
      expect(capturedInputState.userMessages).toContain('first prompt');
      expect(capturedInputState.userMessages).toContain('second prompt');

      // Clear the conversation history (simulating /clear command)
      const mockClearItems = vi.fn();
      mockedUseHistory.mockReturnValue({
        history: [],
        addItem: vi.fn(),
        updateItem: vi.fn(),
        clearItems: mockClearItems,
        loadHistory: vi.fn(),
      });

      await act(async () => {
        // Rerender to apply the new mock.
        rerender(getAppContainer());
      });

      // Verify that userMessages still contains the input history
      // (it should not be affected by clearing conversation history)
      expect(capturedInputState.userMessages).toContain('first prompt');
      expect(capturedInputState.userMessages).toContain('second prompt');

      unmount();
    });
  });

  describe('Regression Tests', () => {
    it('does not refresh static on startup if banner text is empty', async () => {
      // Mock banner text to be empty strings
      vi.spyOn(mockConfig, 'getBannerTextNoCapacityIssues').mockResolvedValue(
        '',
      );
      vi.spyOn(mockConfig, 'getBannerTextCapacityIssues').mockResolvedValue('');

      // Clear previous calls
      mocks.mockStdout.write.mockClear();
      (disableMouseEvents as import('vitest').Mock).mockClear();

      const { unmount } = await act(async () => renderAppContainer());

      // Allow async effects to run
      expect(capturedUIState).toBeTruthy();

      // Wait for fetchBannerTexts to complete
      await act(async () => {
        await new Promise((resolve) => setTimeout(resolve, 100));
      });

      // Check that clearTerminal was NOT written to stdout
      const clearTerminalCalls = mocks.mockStdout.write.mock.calls.filter(
        (call: unknown[]) => call[0] === ansiEscapes.clearTerminal,
      );

      expect(clearTerminalCalls).toHaveLength(0);
      unmount();
    });
  });

  describe('Submission Handling', () => {
    it('resets expansion state on submission when not in alternate buffer', async () => {
      const { checkPermissions } = await import(
        './hooks/atCommandProcessor.js'
      );
      vi.mocked(checkPermissions).mockResolvedValue([]);

      const { unmount } = await act(async () =>
        renderAppContainer({
          settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
        }),
      );

      expect(capturedUIActions).toBeTruthy();

      // Expand first
      act(() => capturedUIActions.setConstrainHeight(false));
      expect(capturedUIState.constrainHeight).toBe(false);

      // Reset mock stdout to clear any initial writes
      mocks.mockStdout.write.mockClear();
      (disableMouseEvents as import('vitest').Mock).mockClear();

      // Submit
      await act(async () => capturedUIActions.handleFinalSubmit('test prompt'));

      // Should be reset
      expect(capturedUIState.constrainHeight).toBe(true);
      unmount();
    });

    it('resets expansion state on submission when in alternate buffer without clearing terminal', async () => {
      const { checkPermissions } = await import(
        './hooks/atCommandProcessor.js'
      );
      vi.mocked(checkPermissions).mockResolvedValue([]);

      vi.spyOn(mockConfig, 'getUseTerminalBuffer').mockReturnValue(false);

      vi.spyOn(mockConfig, 'getUseAlternateBuffer').mockReturnValue(true);

      const { unmount } = await act(async () =>
        renderAppContainer({
          settings: createMockSettings({ ui: { useAlternateBuffer: true } }),
        }),
      );

      expect(capturedUIActions).toBeTruthy();

      // Expand first
      act(() => capturedUIActions.setConstrainHeight(false));
      expect(capturedUIState.constrainHeight).toBe(false);

      // Reset mock stdout
      mocks.mockStdout.write.mockClear();
      (disableMouseEvents as import('vitest').Mock).mockClear();

      // Submit
      await act(async () => capturedUIActions.handleFinalSubmit('test prompt'));

      // Should be reset
      expect(capturedUIState.constrainHeight).toBe(true);
      // Should NOT refresh static's clearTerminal in alternate buffer
      expect(mocks.mockStdout.write).not.toHaveBeenCalledWith(
        ansiEscapes.clearTerminal,
      );
      unmount();
    });
  });

  describe('Overflow Hint Handling', () => {
    beforeEach(() => {
      vi.useFakeTimers();
    });

    afterEach(() => {
      vi.useRealTimers();
    });

    it('should set showIsExpandableHint when overflow occurs in Standard Mode and hides after 10s', async () => {
      const { unmount } = await act(async () => renderAppContainer());
      await waitFor(() => expect(capturedOverflowActions).toBeTruthy());

      // Trigger overflow
      act(() => {
        capturedOverflowActions.addOverflowingId('test-id');
      });

      await waitFor(() => {
        // Should show hint because we are in Standard Mode (default settings) and have overflow
        expect(capturedUIState.showIsExpandableHint).toBe(true);
      });

      // Advance just before the timeout
      act(() => {
        vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS - 100);
      });
      expect(capturedUIState.showIsExpandableHint).toBe(true);

      // Advance to hit the timeout mark
      act(() => {
        vi.advanceTimersByTime(100);
      });
      await waitFor(() => {
        expect(capturedUIState.showIsExpandableHint).toBe(false);
      });

      unmount();
    });

    it('resets the hint timer when a new component overflows (overflowingIdsSize increases)', async () => {
      const { unmount } = await act(async () => renderAppContainer());
      await waitFor(() => expect(capturedOverflowActions).toBeTruthy());

      // 1. Trigger first overflow
      act(() => {
        capturedOverflowActions.addOverflowingId('test-id-1');
      });

      await waitFor(() => {
        expect(capturedUIState.showIsExpandableHint).toBe(true);
      });

      // 2. Advance half the duration
      act(() => {
        vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS / 2);
      });
      expect(capturedUIState.showIsExpandableHint).toBe(true);

      // 3. Trigger second overflow (this should reset the timer)
      act(() => {
        capturedOverflowActions.addOverflowingId('test-id-2');
      });

      // Advance by 1ms to allow the OverflowProvider's 0ms batching timeout to fire
      // and flush the state update to AppContainer, triggering the reset.
      act(() => {
        vi.advanceTimersByTime(1);
      });

      await waitFor(() => {
        expect(capturedUIState.showIsExpandableHint).toBe(true);
      });

      // 4. Advance enough that the ORIGINAL timer would have expired
      // Subtracting 1ms since we advanced it above to flush the state.
      act(() => {
        vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS / 2 + 100 - 1);
      });
      // The hint should STILL be visible because the timer reset at step 3
      expect(capturedUIState.showIsExpandableHint).toBe(true);

      // 5. Advance to the end of the NEW timer
      act(() => {
        vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS / 2 - 100);
      });
      await waitFor(() => {
        expect(capturedUIState.showIsExpandableHint).toBe(false);
      });

      unmount();
    });

    it('toggles expansion state and resets the hint timer when Ctrl+O is pressed in Standard Mode', async () => {
      const { stdin, unmount } = await act(async () => renderAppContainer());
      await waitFor(() => expect(capturedOverflowActions).toBeTruthy());

      // Initial state is constrainHeight = true
      expect(capturedUIState.constrainHeight).toBe(true);

      // Trigger overflow so the hint starts showing
      act(() => {
        capturedOverflowActions.addOverflowingId('test-id');
      });

      await waitFor(() => {
        expect(capturedUIState.showIsExpandableHint).toBe(true);
      });

      // Advance half the duration
      act(() => {
        vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS / 2);
      });
      expect(capturedUIState.showIsExpandableHint).toBe(true);

      // Simulate Ctrl+O
      act(() => {
        stdin.write('\x0f'); // \x0f is Ctrl+O
      });

      // constrainHeight should toggle
      expect(capturedUIState.constrainHeight).toBe(false);

      // Advance enough that the original timer would have expired if it hadn't reset
      act(() => {
        vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS / 2 + 1000);
      });

      // We expect it to still be true because Ctrl+O should have reset the timer
      expect(capturedUIState.showIsExpandableHint).toBe(true);

      // Advance remaining time to reach the new timeout
      act(() => {
        vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS / 2 - 1000);
      });

      await waitFor(() => {
        expect(capturedUIState.showIsExpandableHint).toBe(false);
      });

      unmount();
    });

    it('toggles Ctrl+O multiple times and verifies the hint disappears exactly after the last toggle', async () => {
      const { stdin, unmount } = await act(async () => renderAppContainer());
      await waitFor(() => expect(capturedOverflowActions).toBeTruthy());

      // Initial state is constrainHeight = true
      expect(capturedUIState.constrainHeight).toBe(true);

      // Trigger overflow so the hint starts showing
      act(() => {
        capturedOverflowActions.addOverflowingId('test-id');
      });

      await waitFor(() => {
        expect(capturedUIState.showIsExpandableHint).toBe(true);
      });

      // Advance half the duration
      act(() => {
        vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS / 2);
      });
      expect(capturedUIState.showIsExpandableHint).toBe(true);

      // First toggle 'on' (expanded)
      act(() => {
        stdin.write('\x0f'); // Ctrl+O
      });
      expect(capturedUIState.constrainHeight).toBe(false);

      // Wait 1 second
      act(() => {
        vi.advanceTimersByTime(1000);
      });
      expect(capturedUIState.showIsExpandableHint).toBe(true);

      // Second toggle 'off' (collapsed)
      act(() => {
        stdin.write('\x0f'); // Ctrl+O
      });
      expect(capturedUIState.constrainHeight).toBe(true);

      // Wait 1 second
      act(() => {
        vi.advanceTimersByTime(1000);
      });
      expect(capturedUIState.showIsExpandableHint).toBe(true);

      // Third toggle 'on' (expanded)
      act(() => {
        stdin.write('\x0f'); // Ctrl+O
      });
      expect(capturedUIState.constrainHeight).toBe(false);

      // Now we wait just before the timeout from the LAST toggle.
      // It should still be true.
      act(() => {
        vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS - 100);
      });
      expect(capturedUIState.showIsExpandableHint).toBe(true);

      // Wait 0.1s more to hit exactly the timeout since the last toggle.
      // It should hide now.
      act(() => {
        vi.advanceTimersByTime(100);
      });
      await waitFor(() => {
        expect(capturedUIState.showIsExpandableHint).toBe(false);
      });

      unmount();
    });

    it('DOES set showIsExpandableHint when overflow occurs in Alternate Buffer Mode', async () => {
      const settingsWithAlternateBuffer = createMockSettings({
        ui: { useAlternateBuffer: true },
      });

      vi.spyOn(mockConfig, 'getUseTerminalBuffer').mockReturnValue(false);

      vi.spyOn(mockConfig, 'getUseAlternateBuffer').mockReturnValue(true);

      const { unmount } = await act(async () =>
        renderAppContainer({
          settings: settingsWithAlternateBuffer,
        }),
      );
      await waitFor(() => expect(capturedOverflowActions).toBeTruthy());

      // Trigger overflow
      act(() => {
        capturedOverflowActions.addOverflowingId('test-id');
      });

      // Should NOW show hint because we are in Alternate Buffer Mode
      await waitFor(() => {
        expect(capturedUIState.showIsExpandableHint).toBe(true);
      });

      unmount();
    });
  });

  describe('Permission Handling', () => {
    it('shows permission dialog when checkPermissions returns paths', async () => {
      const { checkPermissions } = await import(
        './hooks/atCommandProcessor.js'
      );
      vi.mocked(checkPermissions).mockResolvedValue(['/test/file.txt']);

      const { unmount } = await act(async () => renderAppContainer());

      expect(capturedUIActions).toBeTruthy();

      await act(async () =>
        capturedUIActions.handleFinalSubmit('read @file.txt'),
      );

      expect(capturedUIState.permissionConfirmationRequest).not.toBeNull();
      expect(capturedUIState.permissionConfirmationRequest?.files).toEqual([
        '/test/file.txt',
      ]);
      unmount();
    });

    it.each([true, false])(
      'handles permissions when allowed is %s',
      async (allowed) => {
        const { checkPermissions } = await import(
          './hooks/atCommandProcessor.js'
        );
        vi.mocked(checkPermissions).mockResolvedValue(['/test/file.txt']);
        const addReadOnlyPathSpy = vi.spyOn(
          mockConfig.getWorkspaceContext(),
          'addReadOnlyPath',
        );
        const { submitQuery } = mockedUseGeminiStream();

        const { unmount } = await act(async () => renderAppContainer());

        expect(capturedUIActions).toBeTruthy();

        await act(async () =>
          capturedUIActions.handleFinalSubmit('read @file.txt'),
        );

        await act(async () =>
          capturedUIState.permissionConfirmationRequest?.onComplete({
            allowed,
          }),
        );

        if (allowed) {
          expect(addReadOnlyPathSpy).toHaveBeenCalledWith('/test/file.txt');
        } else {
          expect(addReadOnlyPathSpy).not.toHaveBeenCalled();
        }
        expect(submitQuery).toHaveBeenCalledWith('read @file.txt');
        expect(capturedUIState.permissionConfirmationRequest).toBeNull();
        unmount();
      },
    );
  });

  describe('Plan Mode Availability', () => {
    it('should allow plan mode when enabled and idle', async () => {
      vi.spyOn(mockConfig, 'isPlanEnabled').mockReturnValue(true);
      mockedUseGeminiStream.mockReturnValue({
        ...DEFAULT_GEMINI_STREAM_MOCK,
        pendingHistoryItems: [],
      });

      const { unmount } = await act(async () => renderAppContainer());

      expect(capturedUIState).toBeTruthy();
      expect(capturedUIState.allowPlanMode).toBe(true);
      unmount();
    });

    it('should NOT allow plan mode when disabled in config', async () => {
      vi.spyOn(mockConfig, 'isPlanEnabled').mockReturnValue(false);
      mockedUseGeminiStream.mockReturnValue({
        ...DEFAULT_GEMINI_STREAM_MOCK,
        pendingHistoryItems: [],
      });

      const { unmount } = await act(async () => renderAppContainer());

      expect(capturedUIState).toBeTruthy();
      expect(capturedUIState.allowPlanMode).toBe(false);
      unmount();
    });

    it('should NOT allow plan mode when streaming', async () => {
      vi.spyOn(mockConfig, 'isPlanEnabled').mockReturnValue(true);
      mockedUseGeminiStream.mockReturnValue({
        ...DEFAULT_GEMINI_STREAM_MOCK,
        streamingState: StreamingState.Responding,
        pendingHistoryItems: [],
      });

      const { unmount } = await act(async () => renderAppContainer());

      expect(capturedUIState).toBeTruthy();
      expect(capturedUIState.allowPlanMode).toBe(false);
      unmount();
    });

    it('should NOT allow plan mode when a tool is awaiting confirmation', async () => {
      vi.spyOn(mockConfig, 'isPlanEnabled').mockReturnValue(true);
      mockedUseGeminiStream.mockReturnValue({
        ...DEFAULT_GEMINI_STREAM_MOCK,
        streamingState: StreamingState.Idle,
        pendingHistoryItems: [
          {
            type: 'tool_group',
            tools: [
              {
                name: 'test_tool',
                status: CoreToolCallStatus.AwaitingApproval,
              },
            ],
          },
        ],
      });

      const { unmount } = await act(async () => renderAppContainer());

      expect(capturedUIState).toBeTruthy();
      expect(capturedUIState.allowPlanMode).toBe(false);
      unmount();
    });
  });

  describe('Compression Queuing', () => {
    beforeEach(async () => {
      const { checkPermissions } = await import(
        './hooks/atCommandProcessor.js'
      );
      vi.mocked(checkPermissions).mockResolvedValue([]);

      vi.spyOn(mockConfig, 'isModelSteeringEnabled').mockReturnValue(true);

      const actual = await vi.importActual('./hooks/useMessageQueue.js');
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      const { useMessageQueue: realUseMessageQueue } = actual as any;
      mockedUseMessageQueue.mockImplementation(realUseMessageQueue);

      // Start compression by mocking pendingHistoryItems to include a pending compression
      mockedUseGeminiStream.mockImplementation(() => ({
        ...DEFAULT_GEMINI_STREAM_MOCK,
        pendingHistoryItems: [
          {
            type: MessageType.COMPRESSION,
            compression: {
              isPending: true,
              originalTokenCount: null,
              newTokenCount: null,
              compressionStatus: null,
            },
          },
        ],
      }));
    });

    it('queues messages during compression instead of handling as steering hints', async () => {
      const { unmount } = await act(async () => renderAppContainer());

      // Verify state isolation
      expect(capturedUIState.streamingState).toBe(StreamingState.Idle);

      // Submit a message
      await act(async () =>
        capturedUIActions.handleFinalSubmit('follow up message'),
      );

      // Verify it was queued, not submitted as steering hint
      expect(capturedUIState.messageQueue).toContain('follow up message');

      unmount();
    });

    it('executes slash commands immediately during compression', async () => {
      const { unmount } = await act(async () => renderAppContainer());

      // Submit a slash command
      await act(async () => capturedUIActions.handleFinalSubmit('/help'));

      // Verify it was NOT queued
      expect(capturedUIState.messageQueue).not.toContain('/help');

      unmount();
    });
  });
});
