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

import type { SessionInfo } from '@/types/hermes'

import { makeSessionInfo } from '../test/session-info'

import { $activeGatewayProfile } from './profile'
import {
  $cronSessions,
  $messagingSessions,
  $selectedStoredSessionId,
  $sessions,
  $unreadFinishedSessionIds,
  setSelectedStoredSessionId,
  setSessions
} from './session'
import { clearAllSessionStates } from './session-states'
import {
  $sessionSeenCounts,
  $unreadFinishedMarkers,
  forgetSessionUnread,
  markSessionUnreadFinished
} from './session-unread'

const session = (over: Partial<SessionInfo>): SessionInfo => makeSessionInfo({ id: 'live', ...over })

function resetAll() {
  clearAllSessionStates()
  // Lists first: their listeners recompute the unread atom, so wiping the
  // atom afterwards leaves a genuinely clean slate.
  $sessions.set([])
  $cronSessions.set([])
  $messagingSessions.set([])
  $selectedStoredSessionId.set(null)
  $activeGatewayProfile.set('default')
  $sessionSeenCounts.set({})
  $unreadFinishedMarkers.set({})
  $unreadFinishedSessionIds.set([])
}

describe('persisted unread (session-unread)', () => {
  beforeEach(resetAll)

  it('reconstructs the green dot from a seen watermark after a cold start', () => {
    // "Restart": persisted watermark says 3 messages were seen; the freshly
    // loaded list says the session now has 5 — it finished while we were away.
    $sessionSeenCounts.set({ default: { s1: 3 } })

    setSessions([session({ id: 's1', message_count: 5 })])

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

  it('seeds the watermark on first sight instead of painting every row green', () => {
    setSessions([session({ id: 's1', message_count: 5 })])

    expect($unreadFinishedSessionIds.get()).toEqual([])
    expect($sessionSeenCounts.get()).toEqual({ default: { s1: 5 } })
  })

  it('keeps a live-edge marker across a transient wipe (gateway/profile switch)', () => {
    setSessions([session({ id: 's1', message_count: 4 })])
    markSessionUnreadFinished('s1')
    expect($unreadFinishedSessionIds.get()).toEqual(['s1'])

    // Gateway switch: transient paint layer wiped, lists drained…
    setSessions([])
    $unreadFinishedSessionIds.set([])

    // …then the profile's lists load again: the persisted marker repaints.
    setSessions([session({ id: 's1', message_count: 4 })])

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

  it('acks watermark + marker when the user opens the session', () => {
    $sessionSeenCounts.set({ default: { s1: 3 } })
    setSessions([session({ id: 's1', message_count: 5 })])
    markSessionUnreadFinished('s1')

    setSelectedStoredSessionId('s1')

    expect($unreadFinishedSessionIds.get()).toEqual([])
    expect($unreadFinishedMarkers.get()).toEqual({})
    expect($sessionSeenCounts.get()).toEqual({ default: { s1: 5 } })

    // A later refresh with the same count stays read.
    setSessions([session({ id: 's1', message_count: 5 })])
    expect($unreadFinishedSessionIds.get()).toEqual([])
  })

  it('tracks the selected session’s count so on-screen activity never reads as unread', () => {
    $selectedStoredSessionId.set('s1')
    setSessions([session({ id: 's1', message_count: 2 })])

    // The turn completes while the user is looking at it.
    setSessions([session({ id: 's1', message_count: 7 })])

    expect($unreadFinishedSessionIds.get()).toEqual([])
    expect($sessionSeenCounts.get()).toEqual({ default: { s1: 7 } })

    // Navigating away must not retroactively flag it.
    setSelectedStoredSessionId(null)
    setSessions([session({ id: 's1', message_count: 7 })])
    expect($unreadFinishedSessionIds.get()).toEqual([])
  })

  it('follows auto-compression id rotation via the durable lineage id', () => {
    $sessionSeenCounts.set({ default: { root: 3 } })

    setSessions([session({ _lineage_root_id: 'root', id: 'tip-2', message_count: 6 })])

    expect($unreadFinishedSessionIds.get()).toEqual(['tip-2'])

    // Opening the rotated tip acks the lineage watermark.
    setSelectedStoredSessionId('tip-2')
    expect($sessionSeenCounts.get()).toEqual({ default: { root: 6 } })
  })

  it('reconstructs unread for cron rows too', () => {
    $sessionSeenCounts.set({ default: { cron1: 1 } })

    $cronSessions.set([session({ id: 'cron1', message_count: 3 })])

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

  it('never paints messaging rows from count growth, but honors live-edge markers', () => {
    // First sight seeds…
    $messagingSessions.set([session({ id: 'tg1', message_count: 10 })])
    // …then inbound messages bump the count: NOT "finished — unread".
    $messagingSessions.set([session({ id: 'tg1', message_count: 14 })])

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

    // A real background turn edge still marks it, durably.
    markSessionUnreadFinished('tg1')
    $unreadFinishedSessionIds.set([])
    $messagingSessions.set([session({ id: 'tg1', message_count: 14 })])

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

  it('preserves a live-edge id whose row is not in any loaded list yet', () => {
    // A brand-new session's first turn isn't flushed to the sidebar list until
    // persisted — the flag must survive list refreshes in the meantime.
    markSessionUnreadFinished('fresh')

    setSessions([session({ id: 'other', message_count: 2 })])

    expect($unreadFinishedSessionIds.get()).toContain('fresh')
  })

  it('keeps same-id sessions in different profiles independent', () => {
    // Session ids are caller-supplied and every profile backend is its own
    // namespace, so "s1" in alpha and "s1" in beta are different sessions —
    // and cron/messaging lists always mix profiles into one array.
    $sessionSeenCounts.set({ alpha: { s1: 3 }, beta: { s1: 5 } })
    $unreadFinishedMarkers.set({ alpha: ['s1'] })

    setSessions([
      session({ id: 's1', message_count: 5, profile: 'beta' }),
      session({ id: 's1', message_count: 5, profile: 'alpha' })
    ])

    // Only alpha's row is past its watermark; beta's sits exactly on it.
    expect($unreadFinishedSessionIds.get()).toEqual(['s1'])
    expect($sessionSeenCounts.get()).toEqual({ alpha: { s1: 3 }, beta: { s1: 5 } })

    // Opening beta's row (the gateway is on beta) acks beta only.
    $activeGatewayProfile.set('beta')
    setSelectedStoredSessionId('s1')

    expect($sessionSeenCounts.get()).toEqual({ alpha: { s1: 3 }, beta: { s1: 5 } })
    // alpha's marker survives an ack of beta's identically-named session.
    expect($unreadFinishedMarkers.get()).toEqual({ alpha: ['s1'] })

    // A later refresh must not launder alpha's gap through beta's selection.
    setSessions([
      session({ id: 's1', message_count: 6, profile: 'beta' }),
      session({ id: 's1', message_count: 5, profile: 'alpha' })
    ])

    expect($sessionSeenCounts.get()).toEqual({ alpha: { s1: 3 }, beta: { s1: 6 } })
    expect($unreadFinishedMarkers.get()).toEqual({ alpha: ['s1'] })
  })

  it('files a live-edge marker with no loaded row under the active gateway profile', () => {
    $activeGatewayProfile.set('work')

    markSessionUnreadFinished('fresh')

    expect($unreadFinishedMarkers.get()).toEqual({ work: ['fresh'] })
    expect($unreadFinishedSessionIds.get()).toEqual(['fresh'])
  })

  it('forgets a deleted session’s persisted unread, per profile', () => {
    $sessionSeenCounts.set({ alpha: { root: 3, other: 1 }, beta: { root: 9 } })
    $unreadFinishedMarkers.set({ alpha: ['root', 'keep'], beta: ['root'] })
    $unreadFinishedSessionIds.set(['tip-2'])

    forgetSessionUnread(['tip-2', 'root', null, undefined], 'alpha')

    expect($sessionSeenCounts.get()).toEqual({ alpha: { other: 1 }, beta: { root: 9 } })
    expect($unreadFinishedMarkers.get()).toEqual({ alpha: ['keep'], beta: ['root'] })
    expect($unreadFinishedSessionIds.get()).toEqual([])
  })

  it('drops an emptied profile bucket instead of persisting an empty shell', () => {
    $sessionSeenCounts.set({ alpha: { s1: 3 } })
    $unreadFinishedMarkers.set({ alpha: ['s1'] })

    forgetSessionUnread(['s1'], 'alpha')

    expect($sessionSeenCounts.get()).toEqual({})
    expect($unreadFinishedMarkers.get()).toEqual({})
  })

  it('caps markers per profile, evicting the oldest', () => {
    for (let i = 0; i < 201; i += 1) {
      markSessionUnreadFinished(`s${i}`)
    }

    const markers = $unreadFinishedMarkers.get().default

    expect(markers).toHaveLength(200)
    expect(markers[0]).toBe('s1')
    expect(markers.at(-1)).toBe('s200')
  })
})
