import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'

import type { ClientSessionState } from '@/app/types'
import { createClientSessionState } from '@/lib/chat-runtime'

import { $activeSessionId, $selectedStoredSessionId, $unreadFinishedSessionIds } from './session'
import {
  $attentionSessionIds,
  $stalledSessionIds,
  $workingSessionIds,
  clearAllSessionStates,
  getRecentlySettledSessionIds,
  publishSessionState,
  SESSION_WATCHDOG_TIMEOUT_MS
} from './session-states'

// Read from the store rather than restated here: these assert what happens on
// either side of the threshold, not what the threshold is.
const WATCHDOG_MS = SESSION_WATCHDOG_TIMEOUT_MS

function state(over: Partial<ClientSessionState> = {}): ClientSessionState {
  return { ...createClientSessionState(null), storedSessionId: 's1', ...over }
}

describe('session status transitions', () => {
  beforeEach(() => {
    vi.useFakeTimers()
    vi.setSystemTime(0)
    clearAllSessionStates()
    $unreadFinishedSessionIds.set([])
    $selectedStoredSessionId.set(null)
    $activeSessionId.set(null)
  })

  afterEach(() => {
    vi.runOnlyPendingTimers()
    vi.useRealTimers()
    clearAllSessionStates()
    $unreadFinishedSessionIds.set([])
    $selectedStoredSessionId.set(null)
    $activeSessionId.set(null)
  })

  it('adds a session to $workingSessionIds when busy transitions to true', () => {
    const s = state({ busy: false, storedSessionId: 's1' })
    publishSessionState('rt1', s)

    publishSessionState('rt1', { ...s, busy: true })

    expect($workingSessionIds.get()).toContain('s1')
  })

  it('removes a session from $workingSessionIds when busy transitions to false', () => {
    const working = state({ busy: true, storedSessionId: 's1' })
    publishSessionState('rt1', working)

    expect($workingSessionIds.get()).toContain('s1')

    publishSessionState('rt1', { ...working, busy: false })

    expect($workingSessionIds.get()).not.toContain('s1')
  })

  it('adds a session to $attentionSessionIds when needsInput is true', () => {
    const s = state({ busy: true, needsInput: false, storedSessionId: 's1' })
    publishSessionState('rt1', s)

    publishSessionState('rt1', { ...s, needsInput: true })

    expect($attentionSessionIds.get()).toContain('s1')
  })

  it('marks a background session unread when its turn finishes', () => {
    $selectedStoredSessionId.set('other-session')
    const working = state({ busy: true, storedSessionId: 's1' })
    publishSessionState('rt1', working)

    publishSessionState('rt1', { ...working, busy: false })

    expect($unreadFinishedSessionIds.get()).toEqual(['s1'])
  })

  it('does NOT mark unread when the finishing session is the active one', () => {
    $selectedStoredSessionId.set('s1')
    const working = state({ busy: true, storedSessionId: 's1' })
    publishSessionState('rt1', working)

    publishSessionState('rt1', { ...working, busy: false })

    expect($unreadFinishedSessionIds.get()).toEqual([])
  })

  it('does NOT mark unread on idle→idle re-asserts (no prior working state)', () => {
    $selectedStoredSessionId.set('other-session')
    publishSessionState('rt1', state({ busy: false, storedSessionId: 's1' }))

    expect($unreadFinishedSessionIds.get()).toEqual([])
  })

  it('grants settle grace when a working session goes idle', () => {
    $selectedStoredSessionId.set('other')
    const working = state({ busy: true, storedSessionId: 's1' })
    publishSessionState('rt1', working)

    publishSessionState('rt1', { ...working, busy: false })

    expect(getRecentlySettledSessionIds()).toEqual(['s1'])
  })

  it('does not grant grace on idle→idle re-asserts', () => {
    publishSessionState('rt1', state({ busy: false, storedSessionId: 's1' }))
    expect(getRecentlySettledSessionIds()).toEqual([])
  })

  it('clears settle grace when the session goes busy again', () => {
    $selectedStoredSessionId.set('other')
    const working = state({ busy: true, storedSessionId: 's2' })
    publishSessionState('rt1', working)
    const idle = { ...working, busy: false }
    publishSessionState('rt1', idle)

    expect(getRecentlySettledSessionIds()).toEqual(['s2'])

    publishSessionState('rt1', { ...idle, busy: true })

    expect(getRecentlySettledSessionIds()).toEqual([])
  })
})

describe('session watchdog', () => {
  beforeEach(() => {
    vi.useFakeTimers()
    clearAllSessionStates()
    $unreadFinishedSessionIds.set([])
    $selectedStoredSessionId.set(null)
    $activeSessionId.set(null)
  })

  afterEach(() => {
    vi.runOnlyPendingTimers()
    vi.useRealTimers()
    clearAllSessionStates()
    $unreadFinishedSessionIds.set([])
    $selectedStoredSessionId.set(null)
    $activeSessionId.set(null)
  })

  it('marks a silent session stalled without pretending it finished', () => {
    publishSessionState('rt1', state({ busy: true, storedSessionId: 's1' }))

    vi.advanceTimersByTime(WATCHDOG_MS)

    expect($workingSessionIds.get()).toContain('s1')
    expect($stalledSessionIds.get()).toContain('s1')
  })

  it('clears stalled on new activity and rearms the watchdog', () => {
    const working = state({ busy: true, storedSessionId: 's2' })
    publishSessionState('rt2', working)
    vi.advanceTimersByTime(WATCHDOG_MS)
    expect($stalledSessionIds.get()).toContain('s2')

    publishSessionState('rt2', { ...working, awaitingResponse: true })
    expect($stalledSessionIds.get()).not.toContain('s2')

    vi.advanceTimersByTime(WATCHDOG_MS - 1)
    expect($stalledSessionIds.get()).not.toContain('s2')
    expect($workingSessionIds.get()).toContain('s2')
  })

  it('clears both running and stalled on an authoritative terminal transition', () => {
    const working = state({ busy: true, storedSessionId: 's3' })
    publishSessionState('rt3', working)
    vi.advanceTimersByTime(WATCHDOG_MS)
    expect($stalledSessionIds.get()).toContain('s3')

    publishSessionState('rt3', { ...working, busy: false })

    expect($workingSessionIds.get()).not.toContain('s3')
    expect($stalledSessionIds.get()).not.toContain('s3')
  })

  it('never marks a session stalled when it settles before the window', () => {
    const working = state({ busy: true, storedSessionId: 's4' })
    publishSessionState('rt4', working)
    publishSessionState('rt4', { ...working, busy: false })
    vi.advanceTimersByTime(WATCHDOG_MS)

    expect($workingSessionIds.get()).not.toContain('s4')
    expect($stalledSessionIds.get()).not.toContain('s4')
  })

  it('clears stalled state and disarms timers on a gateway wipe', () => {
    publishSessionState('rt1', state({ busy: true, storedSessionId: 's1' }))
    vi.advanceTimersByTime(WATCHDOG_MS)
    expect($stalledSessionIds.get()).toEqual(['s1'])

    clearAllSessionStates()
    vi.advanceTimersByTime(WATCHDOG_MS)

    expect($workingSessionIds.get()).toEqual([])
    expect($stalledSessionIds.get()).toEqual([])
  })
})

describe('computed $workingSessionIds', () => {
  beforeEach(() => {
    clearAllSessionStates()
  })

  afterEach(() => {
    clearAllSessionStates()
  })

  it('is empty when no sessions are busy', () => {
    expect($workingSessionIds.get()).toEqual([])
  })

  it('reflects busy sessions under the id their surfaces key on', () => {
    publishSessionState('rt1', state({ busy: true, storedSessionId: 's1' }))
    publishSessionState('rt2', state({ busy: false, storedSessionId: 's2' }))
    // Not yet persisted, so the runtime id is the only id it has — and the one
    // the row is keyed by until the backend hands a stored id back.
    publishSessionState('rt3', state({ busy: true, storedSessionId: null }))

    expect($workingSessionIds.get()).toEqual(['s1', 'rt3'])
  })

  it('updates when session state changes', () => {
    publishSessionState('rt1', state({ busy: true, storedSessionId: 's1' }))
    expect($workingSessionIds.get()).toEqual(['s1'])

    publishSessionState('rt1', state({ busy: false, storedSessionId: 's1' }))
    expect($workingSessionIds.get()).toEqual([])
  })
})

describe('computed $attentionSessionIds', () => {
  beforeEach(() => {
    clearAllSessionStates()
  })

  afterEach(() => {
    clearAllSessionStates()
  })

  it('reflects sessions with needsInput=true and a storedSessionId', () => {
    publishSessionState('rt1', state({ needsInput: true, storedSessionId: 's1' }))
    publishSessionState('rt2', state({ needsInput: false, storedSessionId: 's2' }))

    expect($attentionSessionIds.get()).toEqual(['s1'])
  })

  it('clears when $sessionStates is cleared', () => {
    publishSessionState('rt1', state({ needsInput: true, storedSessionId: 's1' }))
    expect($attentionSessionIds.get()).toEqual(['s1'])

    clearAllSessionStates()
    expect($attentionSessionIds.get()).toEqual([])
  })
})
