import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { act, render, renderHook } from '@testing-library/react'
import type { ReactNode } from 'react'
import { createRealtimeClient } from '@platform-modules/realtime/client'
import {
  Channel,
  RealtimeProvider,
  RealtimeReactError,
  useChannel,
  usePresence,
  useRealtimeClient,
} from './index'

class MockWebSocket {
  static readonly CONNECTING = 0
  static readonly OPEN = 1
  static readonly CLOSING = 2
  static readonly CLOSED = 3
  static instances: MockWebSocket[] = []

  readyState = MockWebSocket.CONNECTING
  onopen: ((event: Event) => void) | null = null
  onmessage: ((event: MessageEvent) => void) | null = null
  onclose: ((event: CloseEvent) => void) | null = null
  onerror: ((event: Event) => void) | null = null
  sent: unknown[] = []
  closedCode: number | undefined

  constructor(public url: string) {
    MockWebSocket.instances.push(this)
  }

  open() {
    this.readyState = MockWebSocket.OPEN
    this.onopen?.(new Event('open'))
  }
  emit(data: unknown) {
    this.onmessage?.({ data } as MessageEvent)
  }
  remoteClose() {
    this.readyState = MockWebSocket.CLOSED
    this.onclose?.({ code: 1006 } as CloseEvent)
  }
  send(data: unknown) {
    this.sent.push(data)
  }
  close(code?: number) {
    this.readyState = MockWebSocket.CLOSED
    this.closedCode = code
  }
}

const last = () => MockWebSocket.instances[MockWebSocket.instances.length - 1]!

function makeClient(url = 'wss://x/realtime') {
  return createRealtimeClient({
    url,
    reconnect: false,
    createWebSocket: (nextUrl) => new MockWebSocket(nextUrl) as unknown as WebSocket,
  })
}

function wrapper(client = makeClient()) {
  return function Wrapper({ children }: { children: ReactNode }) {
    return <RealtimeProvider client={client}>{children}</RealtimeProvider>
  }
}

beforeEach(() => {
  MockWebSocket.instances = []
})

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

describe('RealtimeProvider/useChannel', () => {
  it('uses one upstream socket for two React consumers and fans frames to both', () => {
    const client = makeClient()
    const framesA = vi.fn()
    const framesB = vi.fn()

    function Pair() {
      const a = useChannel({ onFrame: framesA })
      const b = useChannel({ onFrame: framesB })
      return <span>{a.status}:{b.status}</span>
    }

    const view = render(
      <RealtimeProvider client={client}>
        <Pair />
      </RealtimeProvider>,
    )
    expect(MockWebSocket.instances).toHaveLength(1)
    act(() => last().open())
    act(() => last().emit('{"v":1,"type":"ping"}'))
    expect(framesA).toHaveBeenCalledWith({ v: 1, type: 'ping' })
    expect(framesB).toHaveBeenCalledWith({ v: 1, type: 'ping' })
    expect(view.container.textContent).toBe('open:open')
  })

  it('does not close until the last consumer releases ownership', () => {
    const client = makeClient()

    function Consumer() {
      useChannel()
      return null
    }
    function Tree({ second }: { second: boolean }) {
      return (
        <RealtimeProvider client={client}>
          <Consumer />
          {second ? <Consumer /> : null}
        </RealtimeProvider>
      )
    }

    const view = render(<Tree second />)
    const socket = last()
    act(() => socket.open())
    view.rerender(<Tree second={false} />)
    expect(socket.closedCode).toBeUndefined()
    view.unmount()
    expect(socket.closedCode).toBe(1000)
  })

  it('surfaces status, last frame, callbacks and guarded send from the client', () => {
    const onStatus = vi.fn()
    const onFrame = vi.fn()
    const { result } = renderHook(() => useChannel({ onStatus, onFrame }), {
      wrapper: wrapper(),
    })

    expect(result.current.status).toBe('connecting')
    expect(result.current.send('early')).toBe(false)
    act(() => last().open())
    expect(result.current.status).toBe('open')
    expect(onStatus).toHaveBeenCalledWith('open')

    act(() => last().emit('{"v":1,"type":"ping"}'))
    expect(result.current.lastFrame).toEqual({ v: 1, type: 'ping' })
    expect(onFrame).toHaveBeenCalledWith({ v: 1, type: 'ping' })
    expect(result.current.send('hi')).toBe(true)
    expect(last().sent).toEqual(['hi'])
  })

  it('throws a typed contextual error outside a provider', () => {
    let thrown: unknown
    try {
      renderHook(() => useRealtimeClient())
    } catch (error) {
      thrown = error
    }

    expect(thrown).toBeInstanceOf(RealtimeReactError)
    expect(thrown).toMatchObject({
      name: 'RealtimeReactError',
      code: 'missing-provider',
      message: 'useRealtimeClient must be used within <RealtimeProvider client={...}>',
    })
  })
})

describe('usePresence', () => {
  it('tracks only the configured presence frames', () => {
    const { result } = renderHook(
      () => usePresence({ presenceType: 'roster', select: (f) => (f as { ids?: string[] }).ids ?? [] }),
      { wrapper: wrapper() },
    )
    act(() => last().open())
    act(() => last().emit('{"v":1,"type":"message","ids":["x"]}'))
    expect(result.current.online).toEqual([])
    act(() => last().emit('{"v":1,"type":"roster","ids":["a","b"]}'))
    expect(result.current.online).toEqual(['a', 'b'])
    expect(result.current.status).toBe('open')
  })

  it('resets stale presence when provider client identity changes', () => {
    const first = makeClient('wss://x/a')
    const second = makeClient('wss://x/b')

    function Probe() {
      const presence = usePresence()
      return <span>{presence.online.join(',')}</span>
    }

    const view = render(
      <RealtimeProvider client={first}><Probe /></RealtimeProvider>,
    )
    act(() => last().open())
    act(() => last().emit('{"v":1,"type":"presence","online":["a"]}'))
    expect(view.container.textContent).toBe('a')

    view.rerender(<RealtimeProvider client={second}><Probe /></RealtimeProvider>)
    expect(view.container.textContent).toBe('')
    expect(MockWebSocket.instances).toHaveLength(2)
  })
})

describe('Channel', () => {
  it('is a headless render-prop binding', () => {
    const view = render(
      <RealtimeProvider client={makeClient()}>
        <Channel render={(channel) => <span data-testid="s">{channel.status}</span>} />
      </RealtimeProvider>,
    )
    expect(view.container.querySelector('[data-testid="s"]')?.textContent).toBe('connecting')
    act(() => last().open())
    expect(view.container.querySelector('[data-testid="s"]')?.textContent).toBe('open')
  })
})
