import { fetchWebSocketTicket } from '@/websocket/api/fetchWebSocketTicket'
import { createWebSocketConnection } from '@/websocket/connection/createConnection'
import { WS_INBOUND_EVENTS } from '@/websocket/protocols'
import { waitFor } from '@testing-library/dom'
import { setupWebsocket } from './utils/setupWebsocket'

jest.mock('@/websocket/api/fetchWebSocketTicket')

describe('createWebSocketConnection', () => {
  let mockWebSocket: jest.Mock
  let cleanup: () => void
  let webSocketInstances: any[] = []

  const mockTicket = {
    value: 'test-ticket-123',
    clientAddress: '127.0.0.1',
    generatedOn: '2025-01-12T10:00:00Z',
    validUntil: '2025-01-12T11:00:00Z',
    username: 'testuser'
  }

  /** ---------- Helpers ---------- */

  const getWs = () => webSocketInstances[0]

  const triggerEvent = (ws: any, event: string, payload?: any) => {
    ws._listeners[event]?.[0]?.(payload)
  }

  const createAndOpenConnection = async () => {
    const mockCallbacks = {
      onMessage: jest.fn(),
      onClose: jest.fn(),
      onError: jest.fn()
    }
    const promise = createWebSocketConnection(mockCallbacks)

    await waitFor(() => {
      expect(webSocketInstances.length).toBe(1)
    })

    triggerEvent(getWs(), WS_INBOUND_EVENTS.CONNECTION_OPENED)
    const socket = await promise

    return { socket, ws: getWs(), promise, mockCallbacks }
  }

  /** ---------- Setup ---------- */

  beforeEach(() => {
    ;({ webSocketInstances, mockWebSocket, cleanup } = setupWebsocket())
    window.WEBSOCKET_URL = 'wss://calendar.example.com'
    ;(fetchWebSocketTicket as jest.Mock).mockResolvedValue(mockTicket)
  })

  afterEach(() => {
    jest.clearAllMocks()
    delete window.WEBSOCKET_URL
    delete window.CALENDAR_BASE_URL
    cleanup()
  })

  /** ---------- Tests ---------- */

  it('throws when WEBSOCKET_URL is not defined', async () => {
    delete window.WEBSOCKET_URL
    const mockCallbacks = {
      onMessage: jest.fn()
    }

    await expect(createWebSocketConnection(mockCallbacks)).rejects.toThrow(
      'WEBSOCKET_URL is not defined'
    )
  })

  it('fetches WebSocket ticket', async () => {
    await createAndOpenConnection()
    expect(fetchWebSocketTicket).toHaveBeenCalledTimes(1)
  })

  it('creates WebSocket with correct URL and ticket', async () => {
    await createAndOpenConnection()

    expect(mockWebSocket).toHaveBeenCalledWith(
      'wss://calendar.example.com/ws?ticket=test-ticket-123'
    )
  })

  it('creates WebSocket with correct URL and ticket without the WEBSOCKET_URL', async () => {
    delete window.WEBSOCKET_URL
    window.CALENDAR_BASE_URL = 'https://calendar.example.com'
    await createAndOpenConnection()

    expect(mockWebSocket).toHaveBeenCalledWith(
      'wss://calendar.example.com/ws?ticket=test-ticket-123'
    )
  })

  it('resolves with socket when connection opens', async () => {
    const { socket, ws } = await createAndOpenConnection()
    expect(socket).toBe(ws)
  })

  it('rejects when connection fails', async () => {
    const mockCallbacks = {
      onMessage: jest.fn()
    }
    const promise = createWebSocketConnection(mockCallbacks)

    await waitFor(() => {
      expect(webSocketInstances.length).toBe(1)
    })

    triggerEvent(getWs(), WS_INBOUND_EVENTS.ERROR, new Event('error'))

    await expect(promise).rejects.toThrow('WebSocket connection failed')
  })

  it('attaches message event listener', async () => {
    const { ws } = await createAndOpenConnection()

    expect(ws._listeners[WS_INBOUND_EVENTS.MESSAGE]).toBeDefined()
    expect(ws._listeners[WS_INBOUND_EVENTS.MESSAGE].length).toBeGreaterThan(0)
  })

  it('attaches close event listener', async () => {
    const { ws } = await createAndOpenConnection()
    expect(ws._listeners[WS_INBOUND_EVENTS.CONNECTION_CLOSED]).toBeDefined()
  })

  it('handles invalid JSON messages', async () => {
    const errorSpy = jest.spyOn(console, 'error').mockImplementation()

    const { ws } = await createAndOpenConnection()

    triggerEvent(ws, WS_INBOUND_EVENTS.MESSAGE, { data: 'invalid json' })

    expect(errorSpy).toHaveBeenCalledWith(
      'Failed to parse WebSocket message:',
      expect.any(Error)
    )

    errorSpy.mockRestore()
  })

  it('rejects on timeout', async () => {
    jest.useFakeTimers()
    const mockCallbacks = {
      onMessage: jest.fn()
    }
    const promise = createWebSocketConnection(mockCallbacks)

    await waitFor(() => {
      expect(webSocketInstances.length).toBe(1)
    })

    jest.advanceTimersByTime(10000)

    await expect(promise).rejects.toThrow('WebSocket connection timed out')

    jest.useRealTimers()
  })

  it('calls onMessage callback when message received', async () => {
    const { ws, mockCallbacks } = await createAndOpenConnection()

    const testMessage = { type: 'test', payload: 'data' }
    triggerEvent(ws, WS_INBOUND_EVENTS.MESSAGE, {
      data: JSON.stringify(testMessage)
    })

    expect(mockCallbacks.onMessage).toHaveBeenCalledWith(testMessage)
  })

  it('does not call onMessage when JSON parsing fails', async () => {
    const errorSpy = jest.spyOn(console, 'error').mockImplementation()
    const { ws, mockCallbacks } = await createAndOpenConnection()

    triggerEvent(ws, WS_INBOUND_EVENTS.MESSAGE, { data: 'invalid json' })

    expect(mockCallbacks.onMessage).not.toHaveBeenCalled()
    expect(errorSpy).toHaveBeenCalledWith(
      'Failed to parse WebSocket message:',
      expect.any(Error)
    )

    errorSpy.mockRestore()
  })

  it('calls onClose callback when connection closes', async () => {
    const { ws, mockCallbacks } = await createAndOpenConnection()

    const closeEvent = new CloseEvent('close', {
      code: 1000,
      reason: 'Normal closure'
    })
    triggerEvent(ws, WS_INBOUND_EVENTS.CONNECTION_CLOSED, closeEvent)

    expect(mockCallbacks.onClose).toHaveBeenCalledWith(closeEvent)
  })

  it('calls onError callback when error occurs', async () => {
    const { ws, mockCallbacks } = await createAndOpenConnection()

    const errorEvent = new Event('error')
    triggerEvent(ws, WS_INBOUND_EVENTS.ERROR, errorEvent)

    expect(mockCallbacks.onError).toHaveBeenCalledWith(errorEvent)
  })
})
