/**
 * Tests for the useSSE React hook.
 * Focuses on the post-mount event registration bug: event types added
 * after the initial mount must still be registered on the SSEManager.
 */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { render } from '@testing-library/react'

import { useSSE, type UseSSEOptions } from '../sse-react'

// ---------------------------------------------------------------------------
// 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

  static instances: MockEventSource[] = []

  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, Set<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 set = this.listeners.get(type)
    if (!set) {
      set = new Set()
      this.listeners.set(type, set)
    }
    set.add(listener)
  }

  removeEventListener(type: string, listener: ESListener): void {
    this.listeners.get(type)?.delete(listener)
  }

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

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

  simulateEvent(type: string, data: unknown): void {
    const event = new MessageEvent(type, { data: JSON.stringify(data) })
    const listeners = this.listeners.get(type)
    if (listeners) {
      for (const listener of listeners) {
        listener(event)
      }
    }
  }

  hasListenerFor(type: string): boolean {
    const set = this.listeners.get(type)
    return set !== undefined && set.size > 0
  }
}

// ---------------------------------------------------------------------------
// Stub global EventSource
// ---------------------------------------------------------------------------

let originalEventSource: typeof EventSource

beforeEach(() => {
  originalEventSource = globalThis.EventSource
  MockEventSource.instances = []
  globalThis.EventSource = MockEventSource as unknown as typeof EventSource
})

afterEach(() => {
  globalThis.EventSource = originalEventSource
})

// ---------------------------------------------------------------------------
// Test harness
// ---------------------------------------------------------------------------

function Harness({ options }: { options: UseSSEOptions }) {
  const result = useSSE(options)
  return (
    <div
      data-testid="sse-result"
      data-connected={String(result.connected)}
      data-error={result.lastError?.message ?? ''}
    />
  )
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe('useSSE', () => {
  it('registers event types present at mount time', () => {
    const logHandler = vi.fn()
    render(
      <Harness
        options={{
          url: '/api/stream',
          events: { log: logHandler },
        }}
      />
    )

    expect(MockEventSource.instances).toHaveLength(1)
    const es = MockEventSource.instances[0]!
    es.simulateOpen()
    expect(es.hasListenerFor('log')).toBe(true)
  })

  it('delivers events for mount-time keys through to the handler', () => {
    const logHandler = vi.fn()
    render(
      <Harness
        options={{
          url: '/api/stream',
          events: { log: logHandler },
        }}
      />
    )

    const es = MockEventSource.instances[0]!
    es.simulateOpen()
    es.simulateEvent('log', { line: 'hello' })

    expect(logHandler).toHaveBeenCalledTimes(1)
  })

  it('registers NEW event types added after initial mount', () => {
    const logHandler = vi.fn()
    const phaseHandler = vi.fn()

    const { rerender } = render(
      <Harness
        options={{
          url: '/api/stream',
          events: { log: logHandler },
        }}
      />
    )

    const es = MockEventSource.instances[0]!
    es.simulateOpen()

    // Confirm 'log' is registered but 'phase' is not
    expect(es.hasListenerFor('log')).toBe(true)
    expect(es.hasListenerFor('phase')).toBe(false)

    // Now rerender with an additional 'phase' event type
    rerender(
      <Harness
        options={{
          url: '/api/stream',
          events: { log: logHandler, phase: phaseHandler },
        }}
      />
    )

    // The new 'phase' event type MUST be registered on the existing
    // EventSource WITHOUT tearing down and reconnecting.
    expect(MockEventSource.instances).toHaveLength(1) // no new EventSource
    expect(es.hasListenerFor('phase')).toBe(true)

    // Deliver a phase event — it must reach the handler
    es.simulateEvent('phase', { key: 'train' })
    expect(phaseHandler).toHaveBeenCalledTimes(1)
  })

  it('cleans up when unmounted', () => {
    const logHandler = vi.fn()

    const { unmount } = render(
      <Harness
        options={{
          url: '/api/stream',
          events: { log: logHandler },
        }}
      />
    )

    const es = MockEventSource.instances[0]!
    es.simulateOpen()

    unmount()

    expect(es.readyState).toBe(MockEventSource.CLOSED)
  })
})
