/**
 * Tests for SSEManager class (non-React).
 *
 * Bug under test: calling `on()` for the same event type multiple times
 * after `connect()` adds duplicate `addEventListener` calls to the
 * underlying EventSource. Each listener dispatches to ALL handlers in
 * the set, so handlers are invoked N times instead of once per event.
 */

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

// ---------------------------------------------------------------------------
// Mock EventSource
// ---------------------------------------------------------------------------

type ESListener = (event: MessageEvent) => void

class MockEventSource {
  static readonly CONNECTING = 0
  static readonly OPEN = 1
  static readonly CLOSED = 2

  readonly CONNECTING = 0
  readonly OPEN = 1
  readonly CLOSED = 2

  url: string
  withCredentials: boolean
  readyState: number = MockEventSource.CONNECTING

  onopen: (() => void) | null = null
  onerror: (() => void) | null = null
  onmessage: ((event: MessageEvent) => void) | null = null

  private listeners = new Map<string, ESListener[]>()

  constructor(url: string, opts?: { withCredentials?: boolean }) {
    this.url = url
    this.withCredentials = opts?.withCredentials ?? false
    MockEventSource.instances.push(this)
  }

  addEventListener(type: string, listener: ESListener): void {
    let list = this.listeners.get(type)
    if (!list) {
      list = []
      this.listeners.set(type, list)
    }
    list.push(listener)
  }

  removeEventListener(type: string, listener: ESListener): void {
    const list = this.listeners.get(type)
    if (list) {
      const idx = list.indexOf(listener)
      if (idx !== -1) list.splice(idx, 1)
    }
  }

  close(): void {
    this.readyState = MockEventSource.CLOSED
  }

  dispatchEvent(_event: Event): boolean {
    return true
  }

  simulateOpen(): void {
    this.readyState = MockEventSource.OPEN
    this.onopen?.()
  }

  simulateMessage(type: string, data: string): void {
    const event = new MessageEvent('message', { data })
    Object.defineProperty(event, 'type', { value: type })
    const list = this.listeners.get(type)
    if (list) {
      for (const fn of list) {
        fn(event)
      }
    }
  }

  listenerCount(type: string): number {
    return this.listeners.get(type)?.length ?? 0
  }

  static instances: MockEventSource[] = []
  static reset(): void {
    MockEventSource.instances = []
  }
}

vi.stubGlobal('EventSource', MockEventSource)

// Import after mock is installed.
import { SSEManager } from '../sse'

const latestES = (): MockEventSource =>
  MockEventSource.instances[MockEventSource.instances.length - 1]!

describe('SSEManager', () => {
  beforeEach(() => {
    vi.useFakeTimers()
    MockEventSource.reset()
  })

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

  it('dispatches each handler exactly once when multiple handlers registered for same event type after connect', () => {
    const manager = new SSEManager({ url: '/stream', reconnect: false })
    manager.connect()

    const es = latestES()
    es.simulateOpen()

    const calls: string[] = []
    manager.on('log', () => calls.push('h1'))
    manager.on('log', () => calls.push('h2'))

    es.simulateMessage('log', JSON.stringify({ line: 'hello' }))

    // Each handler should fire exactly once per event.
    // BUG: without fix, `attachEventListener` is called twice (once per
    // `on()` call), creating two EventSource listeners that each dispatch
    // to both handlers → h1 and h2 each fire twice.
    expect(calls).toEqual(['h1', 'h2'])
  })

  it('does not add duplicate EventSource listeners for the same event type', () => {
    const manager = new SSEManager({ url: '/stream', reconnect: false })
    manager.connect()

    const es = latestES()
    es.simulateOpen()

    manager.on('log', vi.fn())
    manager.on('log', vi.fn())

    // Should have exactly 1 EventSource listener for 'log', not 2.
    expect(es.listenerCount('log')).toBe(1)
  })

  it('attaches listeners for each distinct event type', () => {
    const manager = new SSEManager({ url: '/stream', reconnect: false })
    manager.connect()

    const es = latestES()
    es.simulateOpen()

    manager.on('log', vi.fn())
    manager.on('phase', vi.fn())

    expect(es.listenerCount('log')).toBe(1)
    expect(es.listenerCount('phase')).toBe(1)
  })

  it('re-attaches listeners after reconnect', () => {
    const manager = new SSEManager({ url: '/stream', reconnect: false })

    const handler = vi.fn()
    manager.on('log', handler)
    manager.connect()

    const es1 = latestES()
    es1.simulateOpen()
    es1.simulateMessage('log', JSON.stringify({ line: 'first' }))
    expect(handler).toHaveBeenCalledTimes(1)

    // Simulate disconnect + reconnect
    manager.disconnect()
    handler.mockClear()

    manager.connect()
    const es2 = latestES()
    es2.simulateOpen()
    es2.simulateMessage('log', JSON.stringify({ line: 'second' }))
    expect(handler).toHaveBeenCalledTimes(1)
  })
})
