// @vitest-environment jsdom
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { renderHook, act } from '@testing-library/react'
import { useTerminalSetup, type TerminalConfig } from './useTerminalSetup'
import { useSessionStore, type StatusChip } from '../../../store/sessions'
import { useErrorStore } from '../../../store/errors'
import { allowConsoleError } from '../../../../test/console-guard'
import { PANEL_IDS, DEFAULT_TOOLBAR_PANELS } from '../../../panels/system/types'
import { terminalBufferRegistry } from '../../../shared/utils/terminalBufferRegistry'

// Mock xterm and addons
const mockTerminalWrite = vi.fn((_data: string, cb?: () => void) => { cb?.() })
const mockTerminalOpen = vi.fn()
const mockTerminalDispose = vi.fn()
const mockTerminalFocus = vi.fn()
const mockTerminalScrollToBottom = vi.fn()
const mockTerminalScrollToLine = vi.fn()
const mockTerminalLoadAddon = vi.fn()
const mockTerminalOnData = vi.fn().mockReturnValue({ dispose: vi.fn() })
const mockTerminalOnRender = vi.fn().mockReturnValue({ dispose: vi.fn() })
const mockTerminalOnScroll = vi.fn().mockReturnValue({ dispose: vi.fn() })
const mockTerminalOnResize = vi.fn().mockReturnValue({ dispose: vi.fn() })
const mockTerminalAttachCustomKeyEventHandler = vi.fn()
const mockTerminalResize = vi.fn()

const mockRegisterCsiHandler = vi.fn().mockReturnValue({ dispose: vi.fn() })

vi.mock('@xterm/xterm', () => {
  return {
    Terminal: class MockTerminal {
      write = mockTerminalWrite
      open = mockTerminalOpen
      dispose = mockTerminalDispose
      focus = mockTerminalFocus
      scrollToBottom = mockTerminalScrollToBottom
      scrollToLine = mockTerminalScrollToLine
      loadAddon = mockTerminalLoadAddon
      onData = mockTerminalOnData
      onRender = mockTerminalOnRender
      onScroll = mockTerminalOnScroll
      onResize = mockTerminalOnResize
      attachCustomKeyEventHandler = mockTerminalAttachCustomKeyEventHandler
      resize = mockTerminalResize
      cols = 80
      rows = 24
      buffer = { active: { viewportY: 0, baseY: 0 } }
      parser = { registerCsiHandler: mockRegisterCsiHandler }
    },
  }
})

const mockFitAddonFit = vi.fn()
vi.mock('@xterm/addon-fit', () => {
  return {
    FitAddon: class MockFitAddon {
      fit = mockFitAddonFit
    },
  }
})

const mockSerializeAddonSerialize = vi.fn().mockReturnValue('')
vi.mock('@xterm/addon-serialize', () => {
  return {
    SerializeAddon: class MockSerializeAddon {
      serialize = mockSerializeAddonSerialize
    },
  }
})

// Mock the sub-hooks
vi.mock('./useTerminalKeyboard', () => ({
  useTerminalKeyboard: vi.fn().mockReturnValue(vi.fn().mockReturnValue(true)),
}))

vi.mock('./usePlanDetection', () => ({
  usePlanDetection: vi.fn().mockReturnValue(vi.fn()),
}))

vi.mock('../../../shared/utils/terminalBufferRegistry', () => ({
  terminalBufferRegistry: {
    register: vi.fn(),
    unregister: vi.fn(),
    getBuffer: vi.fn(),
    getLastLines: vi.fn(),
    getSessionIds: vi.fn().mockReturnValue([]),
  },
}))

vi.mock('../utils/terminalActivityDetector', () => ({
  evaluateActivity: vi.fn().mockReturnValue({ status: null, scheduleIdle: false }),
}))

// Mock ResizeObserver - track instances for assertions
const mockResizeObserverInstances: { observe: ReturnType<typeof vi.fn>; disconnect: ReturnType<typeof vi.fn> }[] = []
class MockResizeObserver {
  observe = vi.fn()
  unobserve = vi.fn()
  disconnect = vi.fn()
  constructor() {
    mockResizeObserverInstances.push(this)
  }
}
vi.stubGlobal('ResizeObserver', MockResizeObserver)

// Mock requestAnimationFrame
vi.stubGlobal('requestAnimationFrame', (cb: () => void) => { cb(); return 1 })
vi.stubGlobal('cancelAnimationFrame', vi.fn())

function makeConfig(overrides: Partial<TerminalConfig> = {}): TerminalConfig {
  return {
    sessionId: 'session-1',
    cwd: '/test/dir',
    command: undefined,
    env: undefined,
    isAgentTerminal: false,
    restartKey: 0,
    storeSessionId: 'session-1',
    tabId: '__agent__',
    ...overrides,
  }
}

function makeContainerRef(): React.RefObject<HTMLDivElement | null> {
  const div = document.createElement('div')
  // Give it non-zero dimensions
  Object.defineProperty(div, 'offsetWidth', { value: 800, configurable: true })
  Object.defineProperty(div, 'offsetHeight', { value: 600, configurable: true })
  return { current: div }
}

describe('useTerminalSetup', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mockResizeObserverInstances.length = 0

    // Reset stores
    useSessionStore.setState({
      sessions: [],
      activeSessionId: null,
      isLoading: true,
      showSidebar: true,
      showSettings: false,
      sidebarWidth: 224,
      toolbarPanels: [...DEFAULT_TOOLBAR_PANELS],
      globalPanelVisibility: {
        [PANEL_IDS.SIDEBAR]: true,
        [PANEL_IDS.SETTINGS]: false,
      },
    })
    useErrorStore.setState({
      detailError: null,
    })

    // Reset PTY mocks
    vi.mocked(window.pty.create).mockResolvedValue({ id: 'mock-pty-id' })
    vi.mocked(window.pty.write).mockResolvedValue(undefined)
    vi.mocked(window.pty.resize).mockResolvedValue(undefined)
    vi.mocked(window.pty.kill).mockResolvedValue(undefined)
    vi.mocked(window.pty.onData).mockReturnValue(() => {})
    vi.mocked(window.pty.onExit).mockReturnValue(() => {})
  })

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

  it('returns terminal setup result with expected shape', () => {
    const config = makeConfig()
    const containerRef = makeContainerRef()

    const { result } = renderHook(() => useTerminalSetup(config, containerRef))

    expect(result.current).toHaveProperty('terminalRef')
    expect(result.current).toHaveProperty('ptyIdRef')
    expect(result.current).toHaveProperty('showScrollButton')
    expect(result.current).toHaveProperty('handleScrollToBottom')
    expect(typeof result.current.handleScrollToBottom).toBe('function')
    expect(result.current.showScrollButton).toBe(false)
  })

  it('does not set up terminal when sessionId is undefined', () => {
    const config = makeConfig({ sessionId: undefined })
    const containerRef = makeContainerRef()

    renderHook(() => useTerminalSetup(config, containerRef))

    expect(window.pty.create).not.toHaveBeenCalled()
    expect(mockTerminalOpen).not.toHaveBeenCalled()
  })

  it('does not set up terminal when containerRef is null', () => {
    const config = makeConfig()
    const containerRef = { current: null }

    renderHook(() => useTerminalSetup(config, containerRef))

    expect(window.pty.create).not.toHaveBeenCalled()
  })

  it('creates a PTY on mount with correct parameters', async () => {
    const config = makeConfig({
      sessionId: 'my-session',
      cwd: '/my/cwd',
      command: 'claude-code',
      env: { TERM: 'xterm' },
    })
    const containerRef = makeContainerRef()

    renderHook(() => useTerminalSetup(config, containerRef))

    await act(async () => { await new Promise(r => setTimeout(r, 0)) })

    expect(window.pty.create).toHaveBeenCalledWith(
      expect.objectContaining({
        cwd: '/my/cwd',
        command: 'claude-code',
        sessionId: 'my-session',
        env: { TERM: 'xterm' },
      }),
    )
  })

  it('recreates PTY when command changes (e.g. auto-approve flag appended after repos load)', async () => {
    const containerRef = makeContainerRef()
    // Initial render: agent command without the skip-approval flag (repos not loaded yet)
    const initialConfig = makeConfig({
      sessionId: 'my-session',
      cwd: '/my/cwd',
      command: 'claude',
      isAgentTerminal: true,
    })

    const { rerender } = renderHook(
      ({ config }) => useTerminalSetup(config, containerRef),
      { initialProps: { config: initialConfig } },
    )

    await act(async () => { await new Promise(r => setTimeout(r, 0)) })
    expect(window.pty.create).toHaveBeenCalledTimes(1)
    expect(window.pty.create).toHaveBeenCalledWith(
      expect.objectContaining({ command: 'claude' }),
    )

    // Repos finish loading — command now includes the auto-approve flag
    const updatedConfig = makeConfig({
      sessionId: 'my-session',
      cwd: '/my/cwd',
      command: 'claude --dangerously-skip-permissions',
      isAgentTerminal: true,
    })

    rerender({ config: updatedConfig })
    await act(async () => { await new Promise(r => setTimeout(r, 0)) })

    // The old PTY should be killed and a new one created with the updated command
    expect(window.pty.kill).toHaveBeenCalled()
    expect(window.pty.create).toHaveBeenCalledTimes(2)
    expect(window.pty.create).toHaveBeenLastCalledWith(
      expect.objectContaining({ command: 'claude --dangerously-skip-permissions' }),
    )
  })

  it('does not recreate PTY when command stays the same', async () => {
    const containerRef = makeContainerRef()
    const config = makeConfig({
      sessionId: 'my-session',
      cwd: '/my/cwd',
      command: 'claude',
      isAgentTerminal: true,
    })

    const { rerender } = renderHook(
      ({ config: c }) => useTerminalSetup(c, containerRef),
      { initialProps: { config } },
    )

    await act(async () => { await new Promise(r => setTimeout(r, 0)) })
    expect(window.pty.create).toHaveBeenCalledTimes(1)

    // Re-render with same command — should NOT recreate
    rerender({ config: { ...config } })
    await act(async () => { await new Promise(r => setTimeout(r, 0)) })
    expect(window.pty.create).toHaveBeenCalledTimes(1)
  })

  it('opens terminal in the container element', () => {
    const config = makeConfig()
    const containerRef = makeContainerRef()

    renderHook(() => useTerminalSetup(config, containerRef))

    expect(mockTerminalOpen).toHaveBeenCalledWith(containerRef.current)
  })

  it('registers terminal buffer for agent terminals', () => {
    const config = makeConfig({ isAgentTerminal: true, command: 'claude-code' })
    const containerRef = makeContainerRef()

    renderHook(() => useTerminalSetup(config, containerRef))

    // eslint-disable-next-line @typescript-eslint/unbound-method
    expect(terminalBufferRegistry.register).toHaveBeenCalledWith('session-1', expect.any(Function))
  })

  it('registers terminal buffer for non-agent terminals with user suffix', () => {
    const config = makeConfig({ isAgentTerminal: false })
    const containerRef = makeContainerRef()

    renderHook(() => useTerminalSetup(config, containerRef))

    // eslint-disable-next-line @typescript-eslint/unbound-method
    expect(terminalBufferRegistry.register).toHaveBeenCalledWith('session-1-user', expect.any(Function))
  })

  it('attaches custom key event handler', () => {
    const config = makeConfig()
    const containerRef = makeContainerRef()

    renderHook(() => useTerminalSetup(config, containerRef))

    expect(mockTerminalAttachCustomKeyEventHandler).toHaveBeenCalledWith(expect.any(Function))
  })

  it('sets up resize observer on the container', () => {
    const config = makeConfig()
    const containerRef = makeContainerRef()

    renderHook(() => useTerminalSetup(config, containerRef))

    // ResizeObserver should have been created and observe called
    expect(mockResizeObserverInstances.length).toBeGreaterThan(0)
    expect(mockResizeObserverInstances[0].observe).toHaveBeenCalledWith(containerRef.current)
  })

  it('kills PTY and disposes terminal on unmount', async () => {
    const config = makeConfig()
    const containerRef = makeContainerRef()

    const { unmount } = renderHook(() => useTerminalSetup(config, containerRef))

    await act(async () => { await new Promise(r => setTimeout(r, 0)) })

    unmount()

    expect(window.pty.kill).toHaveBeenCalled()
    expect(mockTerminalDispose).toHaveBeenCalled()
  })

  it('unregisters terminal buffer for agent terminals on unmount', async () => {
    const config = makeConfig({ isAgentTerminal: true, command: 'claude-code' })
    const containerRef = makeContainerRef()

    const { unmount } = renderHook(() => useTerminalSetup(config, containerRef))

    await act(async () => { await new Promise(r => setTimeout(r, 0)) })

    unmount()

    // eslint-disable-next-line @typescript-eslint/unbound-method
    expect(terminalBufferRegistry.unregister).toHaveBeenCalledWith('session-1')
  })

  it('disconnects resize observer on unmount', async () => {
    const config = makeConfig()
    const containerRef = makeContainerRef()

    const { unmount } = renderHook(() => useTerminalSetup(config, containerRef))

    await act(async () => { await new Promise(r => setTimeout(r, 0)) })

    unmount()

    expect(mockResizeObserverInstances.length).toBeGreaterThan(0)
    expect(mockResizeObserverInstances[0].disconnect).toHaveBeenCalled()
  })

  it('handles PTY creation error gracefully', async () => {
    allowConsoleError()
    vi.mocked(window.pty.create).mockRejectedValue(new Error('PTY failed'))

    const config = makeConfig()
    const containerRef = makeContainerRef()

    renderHook(() => useTerminalSetup(config, containerRef))

    await act(async () => { await new Promise(r => setTimeout(r, 0)) })

    // Should write error message to terminal (no callback in error path)
    expect(mockTerminalWrite).toHaveBeenCalledWith(
      expect.stringContaining('Error: Failed to start terminal'),
    )
  })

  it('sets up onData and onExit listeners after PTY creation', async () => {
    const config = makeConfig()
    const containerRef = makeContainerRef()

    renderHook(() => useTerminalSetup(config, containerRef))

    await act(async () => { await new Promise(r => setTimeout(r, 0)) })

    expect(window.pty.onData).toHaveBeenCalled()
    expect(window.pty.onExit).toHaveBeenCalled()
  })

  describe('handleScrollToBottom', () => {
    it('scrolls terminal to bottom and hides scroll button', () => {
      const config = makeConfig()
      const containerRef = makeContainerRef()

      const { result } = renderHook(() => useTerminalSetup(config, containerRef))

      act(() => { result.current.handleScrollToBottom() })

      expect(mockTerminalScrollToBottom).toHaveBeenCalled()
      expect(result.current.showScrollButton).toBe(false)
    })
  })

  describe('active state handling', () => {
    it('fits and focuses terminal when becoming active via store', () => {
      // Set up a session in the store with the correct terminal tabs
      useSessionStore.setState({
        activeSessionId: 'other-session',
        sessions: [{
          id: 'session-1', name: 'test', directory: '/test', branch: 'main',
          status: 'idle', agentId: null, panelVisibility: {}, showExplorer: false,
          showFileViewer: false, showDiff: false, selectedFilePath: null,
          planFilePath: null, fileViewerPosition: 'top' as const,
          layoutSizes: { explorerWidth: 256, fileViewerSize: 300, userTerminalHeight: 192, diffPanelWidth: 320, tutorialPanelWidth: 320 },
          explorerFilter: 'files' as const, lastMessage: null, lastMessageTime: null,
          isUnread: false, workingStartTime: null, recentFiles: [], searchHistory: [],
          terminalTabs: { tabs: [], activeTabId: '__agent__' },
          branchStatus: 'in-progress' as const, hasFeedback: false, checksStatus: 'none' as const, statusChip: 'in-progress' as StatusChip, isArchived: false, stage: 'planning', isRestored: false,
        }],
      })
      const config = makeConfig()
      const containerRef = makeContainerRef()

      renderHook(() => useTerminalSetup(config, containerRef))

      mockFitAddonFit.mockClear()
      mockTerminalFocus.mockClear()

      // Activate this session via the store
      act(() => { useSessionStore.setState({ activeSessionId: 'session-1' }) })

      // requestAnimationFrame is mocked to call immediately
      // fit() is no longer called on activation — invisible preserves layout dimensions
      expect(mockTerminalFocus).toHaveBeenCalled()
    })
  })

  describe('window focus/blur tracking', () => {
    it('sets up focus and blur listeners', () => {
      const addSpy = vi.spyOn(window, 'addEventListener')
      const config = makeConfig()
      const containerRef = makeContainerRef()

      renderHook(() => useTerminalSetup(config, containerRef))

      expect(addSpy).toHaveBeenCalledWith('focus', expect.any(Function))
      expect(addSpy).toHaveBeenCalledWith('blur', expect.any(Function))
      addSpy.mockRestore()
    })

    it('removes focus and blur listeners on unmount', () => {
      const removeSpy = vi.spyOn(window, 'removeEventListener')
      const config = makeConfig()
      const containerRef = makeContainerRef()

      const { unmount } = renderHook(() => useTerminalSetup(config, containerRef))
      unmount()

      expect(removeSpy).toHaveBeenCalledWith('focus', expect.any(Function))
      expect(removeSpy).toHaveBeenCalledWith('blur', expect.any(Function))
      removeSpy.mockRestore()
    })
  })

  describe('agent PTY ID tracking', () => {
    it('sets agent PTY ID in session store for agent terminals', async () => {
      const setAgentPtyIdSpy = vi.fn()
      useSessionStore.setState({ setAgentPtyId: setAgentPtyIdSpy } as never)

      const config = makeConfig({ isAgentTerminal: true, command: 'claude-code' })
      const containerRef = makeContainerRef()

      renderHook(() => useTerminalSetup(config, containerRef))

      await act(async () => { await new Promise(r => setTimeout(r, 0)) })

      expect(setAgentPtyIdSpy).toHaveBeenCalledWith('session-1', expect.any(String))
    })
  })

  describe('agent idle on unmount', () => {
    it('sets agent to idle on unmount when agent was still working', async () => {
      const { evaluateActivity } = await import('../utils/terminalActivityDetector')
      vi.mocked(evaluateActivity).mockReturnValue({ status: 'working', scheduleIdle: true })

      const config = makeConfig({ isAgentTerminal: true, command: 'claude-code' })
      const containerRef = makeContainerRef()

      // Capture the onData callback
      let onDataCb: ((data: string) => void) | null = null
      vi.mocked(window.pty.onData).mockImplementation((_id, cb) => {
        onDataCb = cb as (data: string) => void
        return () => {}
      })

      const { unmount } = renderHook(() => useTerminalSetup(config, containerRef))
      await act(async () => { await new Promise(r => setTimeout(r, 0)) })

      // Simulate some terminal data to set status to working
      if (onDataCb) act(() => { onDataCb!('some output') })

      unmount()

      // Should have called updateAgentMonitor with idle on unmount
      // The store action was called - verify PTY was killed
      expect(window.pty.kill).toHaveBeenCalled()
    })
  })

  describe('PTY data and exit handling', () => {
    it('writes exit message to terminal on PTY exit', async () => {
      let onExitCb: ((exitCode: number) => void) | null = null
      vi.mocked(window.pty.onExit).mockImplementation((_id, cb) => {
        onExitCb = cb as (exitCode: number) => void
        return () => {}
      })

      const config = makeConfig()
      const containerRef = makeContainerRef()

      renderHook(() => useTerminalSetup(config, containerRef))
      await act(async () => { await new Promise(r => setTimeout(r, 0)) })

      if (onExitCb) act(() => { onExitCb!(0) })

      expect(mockTerminalWrite).toHaveBeenCalledWith(
        expect.stringContaining('Process exited with code 0'),
      )
    })

    it('sets exitInfo with Docker OOM detail for exit code 137 on isolated session', async () => {
      let onExitCb: ((exitCode: number) => void) | null = null
      vi.mocked(window.pty.onExit).mockImplementation((_id, cb) => {
        onExitCb = cb as (exitCode: number) => void
        return () => {}
      })

      const config = makeConfig({ isolated: true })
      const containerRef = makeContainerRef()

      const { result } = renderHook(() => useTerminalSetup(config, containerRef))
      await act(async () => { await new Promise(r => setTimeout(r, 0)) })

      if (onExitCb) act(() => { onExitCb!(137) })

      expect(mockTerminalWrite).toHaveBeenCalledWith(
        expect.stringContaining('Process exited with code 137'),
      )
      expect(mockTerminalWrite).toHaveBeenCalledWith(
        expect.stringContaining('out-of-memory killer'),
      )
      expect(result.current.exitInfo).toEqual(expect.objectContaining({
        code: 137,
        message: expect.stringContaining('out-of-memory'),
        detail: expect.stringContaining('Increase Docker Desktop'),
      }))
    })

    it('sets exitInfo without detail for exit code 137 on non-isolated session', async () => {
      let onExitCb: ((exitCode: number) => void) | null = null
      vi.mocked(window.pty.onExit).mockImplementation((_id, cb) => {
        onExitCb = cb as (exitCode: number) => void
        return () => {}
      })

      const config = makeConfig({ isolated: false })
      const containerRef = makeContainerRef()

      const { result } = renderHook(() => useTerminalSetup(config, containerRef))
      await act(async () => { await new Promise(r => setTimeout(r, 0)) })

      if (onExitCb) act(() => { onExitCb!(137) })

      expect(mockTerminalWrite).toHaveBeenCalledWith(
        expect.stringContaining('killed (SIGKILL)'),
      )
      expect(result.current.exitInfo).toEqual(expect.objectContaining({
        code: 137,
        message: expect.stringContaining('SIGKILL'),
      }))
      expect(result.current.exitInfo?.detail).toBeUndefined()
    })

    it('does not set exitInfo for normal exit codes', async () => {
      let onExitCb: ((exitCode: number) => void) | null = null
      vi.mocked(window.pty.onExit).mockImplementation((_id, cb) => {
        onExitCb = cb as (exitCode: number) => void
        return () => {}
      })

      const config = makeConfig()
      const containerRef = makeContainerRef()

      const { result } = renderHook(() => useTerminalSetup(config, containerRef))
      await act(async () => { await new Promise(r => setTimeout(r, 0)) })

      if (onExitCb) act(() => { onExitCb!(0) })

      expect(result.current.exitInfo).toBeNull()
    })

    it('forwards user input to PTY write', async () => {
      let terminalOnDataCb: ((data: string) => void) | null = null
      mockTerminalOnData.mockImplementation((cb: (data: string) => void) => {
        terminalOnDataCb = cb
        return { dispose: vi.fn() }
      })

      const config = makeConfig()
      const containerRef = makeContainerRef()

      renderHook(() => useTerminalSetup(config, containerRef))
      await act(async () => { await new Promise(r => setTimeout(r, 0)) })

      if (terminalOnDataCb) {
        act(() => { terminalOnDataCb!('hello') })
        expect(window.pty.write).toHaveBeenCalled()
      }
    })

    it('marks session as read on user input', async () => {
      let terminalOnDataCb: ((data: string) => void) | null = null
      mockTerminalOnData.mockImplementation((cb: (data: string) => void) => {
        terminalOnDataCb = cb
        return { dispose: vi.fn() }
      })

      const config = makeConfig()
      const containerRef = makeContainerRef()

      renderHook(() => useTerminalSetup(config, containerRef))
      await act(async () => { await new Promise(r => setTimeout(r, 0)) })

      if (terminalOnDataCb) {
        act(() => { terminalOnDataCb!('a') })
        expect(window.pty.write).toHaveBeenCalled()
      }
    })

    it('does not treat cursor position reports as user input', async () => {
      let terminalOnDataCb: ((data: string) => void) | null = null
      mockTerminalOnData.mockImplementation((cb: (data: string) => void) => {
        terminalOnDataCb = cb
        return { dispose: vi.fn() }
      })

      const markSessionRead = vi.fn()
      useSessionStore.setState({
        activeSessionId: 'session-1',
        markSessionRead,
        sessions: [{
          id: 'session-1', name: 'test', directory: '/test', branch: 'main',
          status: 'idle', agentId: null, panelVisibility: {}, showExplorer: false,
          showFileViewer: false, showDiff: false, selectedFilePath: null,
          planFilePath: null, fileViewerPosition: 'top' as const,
          layoutSizes: { explorerWidth: 256, fileViewerSize: 300, userTerminalHeight: 192, diffPanelWidth: 320, tutorialPanelWidth: 320 },
          explorerFilter: 'files' as const, lastMessage: null, lastMessageTime: null,
          isUnread: false, workingStartTime: null, recentFiles: [], searchHistory: [],
          terminalTabs: { tabs: [], activeTabId: '__agent__' },
          branchStatus: 'in-progress' as const, hasFeedback: false, checksStatus: 'none' as const, statusChip: 'in-progress' as StatusChip, isArchived: false, stage: 'planning', isRestored: false,
        }],
      } as never)

      const config = makeConfig()
      const containerRef = makeContainerRef()

      renderHook(() => useTerminalSetup(config, containerRef))
      await act(async () => { await new Promise(r => setTimeout(r, 0)) })

      if (terminalOnDataCb) {
        // Cursor position report (xterm auto-response to DSR query)
        act(() => { terminalOnDataCb!('\x1b[24;80R') })
        // Should still forward to PTY
        expect(window.pty.write).toHaveBeenCalledWith(expect.any(String), '\x1b[24;80R')
        // Should NOT mark session as read (not real user input)
        expect(markSessionRead).not.toHaveBeenCalled()
      }
    })

    it('processes agent activity detection on PTY data for agent terminals', async () => {
      const { evaluateActivity } = await import('../utils/terminalActivityDetector')
      vi.mocked(evaluateActivity).mockReturnValue({ status: 'working', scheduleIdle: true })

      let onDataCb: ((data: string) => void) | null = null
      vi.mocked(window.pty.onData).mockImplementation((_id, cb) => {
        onDataCb = cb as (data: string) => void
        return () => {}
      })

      const config = makeConfig({ isAgentTerminal: true, command: 'claude-code' })
      const containerRef = makeContainerRef()

      renderHook(() => useTerminalSetup(config, containerRef))
      await act(async () => { await new Promise(r => setTimeout(r, 0)) })

      if (onDataCb) {
        act(() => { onDataCb!('some data') })
        expect(evaluateActivity).toHaveBeenCalled()
      }
    })
  })

  describe('resize observer', () => {
    it('calls fitAddon.fit when container resizes', () => {
      let resizeCallback: ((entries: ResizeObserverEntry[]) => void) | null = null
      class TrackableResizeObserver {
        observe = vi.fn()
        unobserve = vi.fn()
        disconnect = vi.fn()
        constructor(cb: (entries: ResizeObserverEntry[]) => void) {
          resizeCallback = cb
          mockResizeObserverInstances.push(this)
        }
      }
      vi.stubGlobal('ResizeObserver', TrackableResizeObserver)

      const config = makeConfig()
      const containerRef = makeContainerRef()
      mockFitAddonFit.mockClear()

      renderHook(() => useTerminalSetup(config, containerRef))

      if (resizeCallback) {
        act(() => {
          resizeCallback!([{ contentRect: { width: 800, height: 600 } } as ResizeObserverEntry])
        })
        expect(mockFitAddonFit).toHaveBeenCalled()
      }
    })
  })

  describe('scroll tracking via wheel events', () => {
    it('disengages following on upward wheel scroll', async () => {
      const config = makeConfig()
      const containerRef = makeContainerRef()

      renderHook(() => useTerminalSetup(config, containerRef))
      await act(async () => { await new Promise(r => setTimeout(r, 0)) })

      // Simulate a wheel event with negative deltaY (scrolling up)
      const wheelEvent = new WheelEvent('wheel', { deltaY: -100, bubbles: true })
      act(() => {
        containerRef.current!.dispatchEvent(wheelEvent)
      })

      // The scroll button should appear when not following and there's scrollback
      // (depends on buffer state, but the listener should have processed)
      expect(true).toBe(true) // Smoke test - no crash
    })

    it('processes downward wheel scroll', async () => {
      const config = makeConfig()
      const containerRef = makeContainerRef()

      renderHook(() => useTerminalSetup(config, containerRef))
      await act(async () => { await new Promise(r => setTimeout(r, 0)) })

      const wheelEvent = new WheelEvent('wheel', { deltaY: 100, bubbles: true })
      act(() => {
        containerRef.current!.dispatchEvent(wheelEvent)
      })
      // No crash
    })

    it('processes keyboard scroll events (PageUp)', async () => {
      const config = makeConfig()
      const containerRef = makeContainerRef()

      renderHook(() => useTerminalSetup(config, containerRef))
      await act(async () => { await new Promise(r => setTimeout(r, 0)) })

      const keyEvent = new KeyboardEvent('keydown', { key: 'PageUp', bubbles: true })
      act(() => {
        containerRef.current!.dispatchEvent(keyEvent)
      })
      // No crash
    })

    it('processes keyboard scroll events (PageDown)', async () => {
      const config = makeConfig()
      const containerRef = makeContainerRef()

      renderHook(() => useTerminalSetup(config, containerRef))
      await act(async () => { await new Promise(r => setTimeout(r, 0)) })

      const keyEvent = new KeyboardEvent('keydown', { key: 'PageDown', bubbles: true })
      act(() => {
        containerRef.current!.dispatchEvent(keyEvent)
      })
      // No crash
    })

    it('processes Shift+ArrowUp scroll', async () => {
      const config = makeConfig()
      const containerRef = makeContainerRef()

      renderHook(() => useTerminalSetup(config, containerRef))
      await act(async () => { await new Promise(r => setTimeout(r, 0)) })

      const keyEvent = new KeyboardEvent('keydown', { key: 'ArrowUp', shiftKey: true, bubbles: true })
      act(() => {
        containerRef.current!.dispatchEvent(keyEvent)
      })
      // No crash
    })

    it('processes Shift+ArrowDown scroll', async () => {
      const config = makeConfig()
      const containerRef = makeContainerRef()

      renderHook(() => useTerminalSetup(config, containerRef))
      await act(async () => { await new Promise(r => setTimeout(r, 0)) })

      const keyEvent = new KeyboardEvent('keydown', { key: 'ArrowDown', shiftKey: true, bubbles: true })
      act(() => {
        containerRef.current!.dispatchEvent(keyEvent)
      })
      // No crash
    })
  })

  describe('scroll event listeners', () => {
    it('attaches wheel and touchmove listeners to the container', () => {
      const config = makeConfig()
      const containerRef = makeContainerRef()
      const addEventSpy = vi.spyOn(containerRef.current!, 'addEventListener')

      renderHook(() => useTerminalSetup(config, containerRef))

      expect(addEventSpy).toHaveBeenCalledWith('wheel', expect.any(Function), { capture: true, passive: true })
      expect(addEventSpy).toHaveBeenCalledWith('touchmove', expect.any(Function), { capture: true, passive: true })
      expect(addEventSpy).toHaveBeenCalledWith('keydown', expect.any(Function), { capture: true })
      addEventSpy.mockRestore()
    })

    it('removes scroll listeners on unmount', () => {
      const config = makeConfig()
      const containerRef = makeContainerRef()
      const removeEventSpy = vi.spyOn(containerRef.current!, 'removeEventListener')

      const { unmount } = renderHook(() => useTerminalSetup(config, containerRef))
      unmount()

      expect(removeEventSpy).toHaveBeenCalledWith('wheel', expect.any(Function), { capture: true })
      expect(removeEventSpy).toHaveBeenCalledWith('touchmove', expect.any(Function), { capture: true })
      expect(removeEventSpy).toHaveBeenCalledWith('keydown', expect.any(Function), { capture: true })
      removeEventSpy.mockRestore()
    })
  })
})
