/**
 * @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, persistentStateMock } from '../test-utils/render.js';
import { waitFor } from '../test-utils/async.js';
import { cleanup } from 'ink-testing-library';
import { act, useContext, type ReactElement } 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,
  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),
  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 { mergeSettings, type LoadedSettings } from '../config/settings.js';
import type { InitializationResult } from '../core/initializer.js';
import { useQuotaAndFallback } from './hooks/useQuotaAndFallback.js';
import { StreamingState } 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';

// 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(),
  };
});

// Helper component will read the context values provided by AppContainer
// so we can assert against them in our tests.
let capturedUIState: UIState;
let capturedUIActions: UIActions;
function TestContextConsumer() {
  capturedUIState = useContext(UIStateContext)!;
  capturedUIActions = useContext(UIActionsContext)!;
  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('./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('./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,
  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 { useEditorSettings } from './hooks/useEditorSettings.js';
import { useSettingsCommand } from './hooks/useSettingsCommand.js';
import { useModelCommand } from './hooks/useModelCommand.js';
import { useSlashCommandProcessor } from './hooks/slashCommandProcessor.js';
import { useConsoleMessages } 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 { 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 { measureElement } from 'ink';
import { useTerminalSize } from './hooks/useTerminalSize.js';
import {
  ShellExecutionService,
  writeToStdout,
  enableMouseEvents,
  disableMouseEvents,
} from '@google/gemini-cli-core';
import { type ExtensionManager } from '../config/extension-manager.js';
import { WARNING_PROMPT_DURATION_MS } from './constants.js';

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

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

  // Helper to render the AppContainer
  const renderAppContainer = (props?: Parameters<typeof getAppContainer>[0]) =>
    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 = useConsoleMessages 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 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,
    backgroundShellCount: 0,
    isBackgroundShellVisible: false,
    toggleBackgroundShell: vi.fn(),
    backgroundCurrentShell: vi.fn(),
    backgroundShells: new Map(),
    registerBackgroundShell: vi.fn(),
    dismissBackgroundShell: vi.fn(),
  };

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

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

    // Initialize mock stdout for terminal title tests

    mocks.mockStdout.write.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({
      consoleMessages: [],
      handleNewMessage: vi.fn(),
      clearConsoleMessages: 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,
    });

    // Mock Config
    mockConfig = makeFakeConfig();

    // 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
    const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
    mockSettings = {
      merged: {
        ...defaultMergedSettings,
        hideBanner: false,
        hideFooter: false,
        hideTips: false,
        showMemoryUsage: false,
        theme: 'default',
        ui: {
          ...defaultMergedSettings.ui,
          showStatusInTitle: false,
          hideWindowTitle: false,
          useAlternateBuffer: false,
        },
      },
    } as unknown as LoadedSettings;

    // 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 () => {
      let unmount: () => void;
      await act(async () => {
        const result = renderAppContainer();
        unmount = result.unmount;
      });
      await waitFor(() => expect(capturedUIState).toBeTruthy());
      unmount!();
    });

    it('renders with startup warnings', async () => {
      const startupWarnings = ['Warning 1', 'Warning 2'];

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

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

      await waitFor(() => {
        expect(capturedUIState.cleanUiDetailsVisible).toBe(true);
      });
      unmount!();
    });

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

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

      await waitFor(() => {
        expect(capturedUIState.cleanUiDetailsVisible).toBe(false);
      });
      expect(persistentStateMock.get).toHaveBeenCalledWith('focusUiEnabled');
      unmount!();
    });
  });

  describe('State Initialization', () => {
    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'],
                },
              },
            ],
          },
        ],
      });

      let unmount: (() => void) | undefined;
      await act(async () => {
        const rendered = renderAppContainer();
        unmount = rendered.unmount;
      });

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

      await act(async () => {
        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'],
                },
              },
            ],
          },
        ],
      });

      let unmount: (() => void) | undefined;
      await act(async () => {
        const rendered = renderAppContainer();
        unmount = rendered.unmount;
      });

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

      await act(async () => {
        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'],
                },
              },
            ],
          },
        ],
      });

      let unmount: (() => void) | undefined;
      await act(async () => {
        const rendered = renderAppContainer();
        unmount = rendered.unmount;
      });

      await waitFor(() =>
        expect(terminalNotificationsMocks.notifyViaTerminal).toHaveBeenCalled(),
      );

      await act(async () => {
        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,
      }));

      let unmount: (() => void) | undefined;
      let rerender: ((tree: ReactElement) => void) | undefined;

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

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

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

      await act(async () => {
        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,
      }));

      let unmount: (() => void) | undefined;
      let rerender: ((tree: ReactElement) => void) | undefined;

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

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

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

      await act(async () => {
        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,
      }));

      let unmount: (() => void) | undefined;
      let rerender: ((tree: ReactElement) => void) | undefined;

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

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

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

      await act(async () => {
        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,
      }));

      let unmount: (() => void) | undefined;
      let rerender: ((tree: ReactElement) => void) | undefined;

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

      await waitFor(() =>
        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());
      });

      await waitFor(() =>
        expect(
          terminalNotificationsMocks.notifyViaTerminal,
        ).toHaveBeenCalledTimes(2),
      );

      await act(async () => {
        unmount?.();
      });
    });

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

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

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

      expect(() => {
        renderAppContainer({ config: debugConfig });
      }).not.toThrow();
    });
  });

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

      // Should render and unmount cleanly
      expect(() => unmount!()).not.toThrow();
    });

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

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

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

  describe('Settings Integration', () => {
    it('handles settings with all display options disabled', async () => {
      const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
      const settingsAllHidden = {
        merged: {
          ...defaultMergedSettings,
          hideBanner: true,
          hideFooter: true,
          hideTips: true,
          showMemoryUsage: false,
        },
      } as unknown as LoadedSettings;

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

    it('handles settings with memory usage enabled', async () => {
      const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
      const settingsWithMemory = {
        merged: {
          ...defaultMergedSettings,
          hideBanner: false,
          hideFooter: false,
          hideTips: false,
          showMemoryUsage: true,
        },
      } as unknown as LoadedSettings;

      let unmount: () => void;
      await act(async () => {
        const result = renderAppContainer({ settings: settingsWithMemory });
        unmount = result.unmount;
      });
      await waitFor(() => 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) => {
        let unmount: () => void;
        await act(async () => {
          const result = renderAppContainer({ version });
          unmount = result.unmount;
        });
        await waitFor(() => 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 } = renderAppContainer({ config: errorConfig });
      unmount();
    });

    it('handles undefined settings gracefully', async () => {
      const undefinedSettings = {
        merged: mergeSettings({}, {}, {}, {}, true),
      } as LoadedSettings;

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

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

      expect(() => unmount()).not.toThrow();
    });
  });

  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',
      };

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

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

    it('initializes chat recording service when config has it', () => {
      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']>,
      );

      expect(() => {
        renderAppContainer({
          config: configWithRecording,
          settings: mockSettings,
          version: '1.0.0',
          initResult: mockInitResult,
        });
      }).not.toThrow();
    });
  });
  describe('Session Recording Integration', () => {
    it('provides chat recording service configuration', () => {
      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',
      );

      expect(() => {
        renderAppContainer({
          config: configWithRecording,
          settings: mockSettings,
          version: '1.0.0',
          initResult: mockInitResult,
        });
      }).not.toThrow();

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

    it('handles session recording when messages are added', () => {
      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']>,
      );

      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();
    });
  });

  describe('Session Resume Flow', () => {
    it('accepts resumed session data', () => {
      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',
      };

      expect(() => {
        renderAppContainer({
          config: configWithClient,
          settings: mockSettings,
          version: '1.0.0',
          initResult: mockInitResult,
          resumedSessionData: resumedData,
        });
      }).not.toThrow();

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

    it('does not attempt resume when client is not initialized', () => {
      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',
      };

      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();
    });
  });

  describe('Token Counting from Session Stats', () => {
    it('tracks token counts from session messages', () => {
      // 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']>,
      );

      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();
    });
  });

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

    it('passes a valid proQuotaRequest to UIStateContext 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
      let unmount: () => void;
      await act(async () => {
        const result = renderAppContainer();
        unmount = result.unmount;
      });
      await waitFor(() => {
        // Assert: The mock request is correctly passed through the context
        expect(capturedUIState.quota.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
      let unmount: () => void;
      await act(async () => {
        const result = renderAppContainer();
        unmount = result.unmount;
      });
      await waitFor(() => {
        // 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();
    });

    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', () => {
      // Arrange: Set up mock settings with showStatusInTitle disabled
      const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
      const mockSettingsWithShowStatusFalse = {
        ...mockSettings,
        merged: {
          ...defaultMergedSettings,
          ui: {
            ...defaultMergedSettings.ui,
            showStatusInTitle: false,
            hideWindowTitle: false,
          },
        },
      } as unknown as LoadedSettings;

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

      // Act: Render the container
      const { unmount } = 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', () => {
      // Arrange: Set up mock settings with dynamicWindowTitle disabled
      const mockSettingsWithDynamicTitleFalse = {
        ...mockSettings,
        merged: {
          ...mockSettings.merged,
          ui: {
            ...mockSettings.merged.ui,
            dynamicWindowTitle: false,
            hideWindowTitle: false,
          },
        },
      } as unknown as LoadedSettings;

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

      // Act: Render the container
      const { unmount } = 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', () => {
      // Arrange: Set up mock settings with hideWindowTitle enabled
      const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
      const mockSettingsWithHideTitleTrue = {
        ...mockSettings,
        merged: {
          ...defaultMergedSettings,
          ui: {
            ...defaultMergedSettings.ui,
            showStatusInTitle: true,
            hideWindowTitle: true,
          },
        },
      } as unknown as LoadedSettings;

      // Act: Render the container
      const { unmount } = 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', () => {
      // Arrange: Set up mock settings with showStatusInTitle enabled
      const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
      const mockSettingsWithTitleEnabled = {
        ...mockSettings,
        merged: {
          ...defaultMergedSettings,
          ui: {
            ...defaultMergedSettings.ui,
            showStatusInTitle: true,
            hideWindowTitle: false,
          },
        },
      } as unknown as LoadedSettings;

      // 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 } = 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', () => {
      // Arrange: Set up mock settings with showStatusInTitle enabled
      const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
      const mockSettingsWithTitleEnabled = {
        ...mockSettings,
        merged: {
          ...defaultMergedSettings,
          ui: {
            ...defaultMergedSettings.ui,
            showStatusInTitle: true,
            hideWindowTitle: false,
          },
        },
      } as unknown as LoadedSettings;

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

      // Act: Render the container
      const { unmount } = 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 defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
      const mockSettingsWithTitleEnabled = {
        ...mockSettings,
        merged: {
          ...defaultMergedSettings,
          ui: {
            ...defaultMergedSettings.ui,
            showStatusInTitle: true,
            hideWindowTitle: false,
          },
        },
      } as unknown as LoadedSettings;

      // 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
      let unmount: () => void;
      await act(async () => {
        const result = renderAppContainer({
          settings: mockSettingsWithTitleEnabled,
        });
        unmount = result.unmount;
      });

      // 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 = {
          ...mockSettings,
          merged: {
            ...mockSettings.merged,
            ui: {
              ...mockSettings.merged.ui,
              showStatusInTitle: true,
              hideWindowTitle: false,
            },
          },
        } as unknown as LoadedSettings;

        // 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 } = 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 = {
          ...mockSettings,
          merged: {
            ...mockSettings.merged,
            ui: {
              ...mockSettings.merged.ui,
              showStatusInTitle: true,
              hideWindowTitle: false,
            },
          },
        } as unknown as LoadedSettings;

        // 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 } = 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 = {
          ...mockSettings,
          merged: {
            ...mockSettings.merged,
            ui: {
              ...mockSettings.merged.ui,
              showStatusInTitle: true,
              hideWindowTitle: false,
            },
          },
        } as unknown as LoadedSettings;

        // 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 } = 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 = {
          ...mockSettings,
          merged: {
            ...mockSettings.merged,
            ui: {
              ...mockSettings.merged.ui,
              showStatusInTitle: true,
              hideWindowTitle: false,
            },
          },
        } as unknown as LoadedSettings;

        // 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 } = 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', () => {
      // Arrange: Set up mock settings with showStatusInTitle enabled
      const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
      const mockSettingsWithTitleEnabled = {
        ...mockSettings,
        merged: {
          ...defaultMergedSettings,
          ui: {
            ...defaultMergedSettings.ui,
            showStatusInTitle: true,
            hideWindowTitle: false,
          },
        },
      } as unknown as LoadedSettings;

      // 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 } = 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', () => {
      // Arrange: Set up mock settings with showStatusInTitle enabled
      const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
      const mockSettingsWithTitleEnabled = {
        ...mockSettings,
        merged: {
          ...defaultMergedSettings,
          ui: {
            ...defaultMergedSettings.ui,
            showStatusInTitle: true,
            hideWindowTitle: false,
          },
        },
      } as unknown as LoadedSettings;

      // 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 } = 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', () => {
      // Arrange: Set up mock settings with showStatusInTitle disabled (so it shows suffix)
      const mockSettingsWithTitleDisabled = {
        ...mockSettings,
        merged: {
          ...mockSettings.merged,
          ui: {
            ...mockSettings.merged.ui,
            showStatusInTitle: false,
            hideWindowTitle: false,
          },
        },
      } as unknown as LoadedSettings;

      // 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 } = 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 } = 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 } = 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('Terminal Height Calculation', () => {
    const mockedMeasureElement = measureElement as Mock;
    const mockedUseTerminalSize = useTerminalSize as Mock;

    it('should prevent terminal height from being less than 1', async () => {
      const resizePtySpy = vi.spyOn(ShellExecutionService, 'resizePty');
      // Arrange: Simulate a small terminal and a large footer
      mockedUseTerminalSize.mockReturnValue({ columns: 80, rows: 5 });
      mockedMeasureElement.mockReturnValue({ width: 80, height: 10 }); // Footer is taller than the screen

      mockedUseGeminiStream.mockReturnValue({
        ...DEFAULT_GEMINI_STREAM_MOCK,
        activePtyId: 'some-id',
      });

      let unmount: () => void;
      await act(async () => {
        const result = renderAppContainer();
        unmount = result.unmount;
      });
      await waitFor(() => expect(resizePtySpy).toHaveBeenCalled());
      const lastCall =
        resizePtySpy.mock.calls[resizePtySpy.mock.calls.length - 1];
      // Check the height argument specifically
      expect(lastCall[2]).toBe(1);
      unmount!();
    });
  });

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

    // Helper function to reduce boilerplate in tests
    const setupKeypressTest = async () => {
      const renderResult = 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
        // Now count is 2, it should quit.
        expect(mockHandleSlashCommand).toHaveBeenCalledWith(
          '/quit',
          undefined,
          undefined,
          false,
        );
        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 = 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 mockToggleBackgroundShell = vi.fn();
        mockedUseGeminiStream.mockReturnValue({
          ...DEFAULT_GEMINI_STREAM_MOCK,
          activePtyId: null,
          isBackgroundShellVisible: true,
          backgroundShells: new Map([[123, { pid: 123, status: 'running' }]]),
          toggleBackgroundShell: mockToggleBackgroundShell,
        });

        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(mockToggleBackgroundShell).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 mockToggleBackgroundShell = vi.fn();
        mockedUseGeminiStream.mockReturnValue({
          ...DEFAULT_GEMINI_STREAM_MOCK,
          activePtyId: null,
          isBackgroundShellVisible: true,
          backgroundShells: new Map([[123, { pid: 123, status: 'running' }]]),
          toggleBackgroundShell: mockToggleBackgroundShell,
        });

        await setupKeypressTest();

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

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

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

        unmount();
      });

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

        await setupKeypressTest();

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

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

        // Should have toggled (shown) the shell
        expect(mockToggleBackgroundShell).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 = 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();
      });
      await waitFor(() => {
        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();
      });
      await waitFor(() => {
        expect(capturedUIState.shortcutsHelpVisible).toBe(false);
      });

      unmount();
    });
  });

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

    const setupCopyModeTest = async (
      isAlternateMode = false,
      childHandler?: Mock,
    ) => {
      // Update settings for this test run
      const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
      const testSettings = {
        ...mockSettings,
        merged: {
          ...defaultMergedSettings,
          ui: {
            ...defaultMergedSettings.ui,
            useAlternateBuffer: isAlternateMode,
          },
        },
      } as unknown as LoadedSettings;

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

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

      const renderResult = 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();
      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 Ctrl+S is pressed`, async () => {
        await setupCopyModeTest(isAlternateMode);
        mocks.mockStdout.write.mockClear(); // Clear initial enable call

        act(() => {
          stdin.write('\x13'); // Ctrl+S
        });
        rerender();

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

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

          // Turn it on (disable mouse)
          act(() => {
            stdin.write('\x13'); // Ctrl+S
          });
          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 any key press', async () => {
          await setupCopyModeTest(isAlternateMode);

          // Enter copy mode
          act(() => {
            stdin.write('\x13'); // Ctrl+S
          });
          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 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('\x13'); // Ctrl+S
          });
          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(),
      });

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

      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,
      });

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

      // 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 () => {
      let unmount: () => void;
      await act(async () => {
        const result = renderAppContainer();
        unmount = result.unmount;
      });
      await waitFor(() => expect(capturedUIState).toBeTruthy());

      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 () => {
      let unmount: () => void;
      await act(async () => {
        const result = renderAppContainer();
        unmount = result.unmount;
      });
      await waitFor(() => expect(capturedUIState).toBeTruthy());

      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 () => {
      let unmount: () => void;
      await act(async () => {
        const result = renderAppContainer();
        unmount = result.unmount;
      });
      await waitFor(() => expect(capturedUIState).toBeTruthy());

      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 () => {
      let unmount: () => void;
      await act(async () => {
        const result = renderAppContainer();
        unmount = result.unmount;
      });
      await waitFor(() => expect(capturedUIState).toBeTruthy());

      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 = 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 = 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 = 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 = 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 = 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 = renderAppContainer();
        unmount = result.unmount;
      });
      await waitFor(() => expect(capturedUIState).toBeTruthy());

      unmount!();

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

  describe('Shell Interaction', () => {
    it('should not crash if resizing the pty fails', async () => {
      const resizePtySpy = vi
        .spyOn(ShellExecutionService, 'resizePty')
        .mockImplementation(() => {
          throw new Error('Cannot resize a pty that has already exited');
        });

      mockedUseGeminiStream.mockReturnValue({
        ...DEFAULT_GEMINI_STREAM_MOCK,
        activePtyId: 'some-pty-id', // Make sure activePtyId is set
      });

      // The main assertion is that the render does not throw.
      let unmount: () => void;
      await act(async () => {
        const result = renderAppContainer();
        unmount = result.unmount;
      });

      await waitFor(() => expect(resizePtySpy).toHaveBeenCalled());
      unmount!();
    });
  });
  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 = 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('clears the prompt when onCancelSubmit is called with shouldRestorePrompt=false', async () => {
      let unmount: () => void;
      await act(async () => {
        const result = renderAppContainer();
        unmount = result.unmount;
      });
      await waitFor(() => expect(capturedUIState).toBeTruthy());

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

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

      expect(mockSetText).toHaveBeenCalledWith('');

      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(),
      });

      let unmount: () => void;
      await act(async () => {
        const result = renderAppContainer();
        unmount = result.unmount;
      });
      await waitFor(() =>
        expect(capturedUIState.userMessages).toContain('previous message'),
      );

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

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

      await waitFor(() => {
        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(),
      });

      let rerender: (tree: ReactElement) => void;
      let unmount;
      await act(async () => {
        const result = renderAppContainer();
        rerender = result.rerender;
        unmount = result.unmount;
      });

      // Verify userMessages is populated from inputHistory
      await waitFor(() =>
        expect(capturedUIState.userMessages).toContain('first prompt'),
      );
      expect(capturedUIState.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(capturedUIState.userMessages).toContain('first prompt');
      expect(capturedUIState.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();

      let compUnmount: () => void = () => {};
      await act(async () => {
        const { unmount } = renderAppContainer();
        compUnmount = unmount;
      });

      // Allow async effects to run
      await waitFor(() => 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);
      compUnmount();
    });
  });

  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']);

      let unmount: () => void;
      await act(async () => (unmount = renderAppContainer().unmount));

      await waitFor(() => expect(capturedUIActions).toBeTruthy());

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

      expect(capturedUIState.permissionConfirmationRequest).not.toBeNull();
      expect(capturedUIState.permissionConfirmationRequest?.files).toEqual([
        '/test/file.txt',
      ]);
      await act(async () => 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();

        let unmount: () => void;
        await act(async () => (unmount = renderAppContainer().unmount));

        await waitFor(() => 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();
        await act(async () => 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: [],
      });

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

      await waitFor(() => {
        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: [],
      });

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

      await waitFor(() => {
        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: [],
      });

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

      await waitFor(() => {
        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,
              },
            ],
          },
        ],
      });

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

      await waitFor(() => {
        expect(capturedUIState).toBeTruthy();
        expect(capturedUIState.allowPlanMode).toBe(false);
      });
      unmount!();
    });
  });
});
