import { describe, expect, it, vi, beforeEach } from 'vitest'
import type { RealtimeEvent } from '@zync/realtime'
import { TenantRealtimeDO } from '../src/durable-objects/TenantRealtimeDO'
import { handleRealtimeBatch } from '../src/queues/realtime'

vi.mock('../src/middleware/auth', () => ({
  authMiddleware: async (c: any, next: () => Promise<void>) => {
    c.set('session', {
      type: 'user',
      tid: 'tenant-1',
      sub: 'user-1',
    })
    await next()
  },
}))

const { realtimeRoutes } = await import('../src/routes/realtime')

type SocketLike = {
  send: ReturnType<typeof vi.fn>
  close: ReturnType<typeof vi.fn>
}

function createEvent(overrides: Partial<RealtimeEvent> = {}): RealtimeEvent {
  return {
    id: 'event-1',
    type: 'task.assigned',
    tenantId: 'tenant-1',
    payload: { taskId: 'task-1' },
    timestamp: '2026-07-01T00:00:00.000Z',
    ...overrides,
  }
}

function createDoState() {
  const broadcastSockets: SocketLike[] = []
  const userSockets = new Map<string, SocketLike[]>()

  return {
    state: {
      getWebSockets: (tag?: string) => {
        if (!tag) return broadcastSockets
        return userSockets.get(tag) ?? []
      },
      acceptWebSocket: vi.fn(),
    } as unknown as DurableObjectState,
    broadcastSockets,
    userSockets,
  }
}

describe('TenantRealtimeDO', () => {
  beforeEach(() => {
    vi.stubGlobal(
      'WebSocketPair',
      vi.fn(() => ({
        0: { close: vi.fn() },
        1: { close: vi.fn() },
      })),
    )
  })

  it('fans out broadcast events from the spec endpoint', async () => {
    const { state, broadcastSockets } = createDoState()
    const socket = { send: vi.fn(), close: vi.fn() }
    broadcastSockets.push(socket)
    const durableObject = new TenantRealtimeDO(state, {})

    const res = await durableObject.fetch(
      new Request('https://do-internal/fanout', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(createEvent({ type: 'activity.new' })),
      }),
    )

    expect(res.status).toBe(204)
    expect(socket.send).toHaveBeenCalledWith(
      JSON.stringify(createEvent({ type: 'activity.new' })),
    )
  })

  it('delivers user-scoped events only to the tagged user sockets', async () => {
    const { state, userSockets } = createDoState()
    const targetSocket = { send: vi.fn(), close: vi.fn() }
    const otherSocket = { send: vi.fn(), close: vi.fn() }
    userSockets.set('user-1', [targetSocket])
    userSockets.set('user-2', [otherSocket])
    const durableObject = new TenantRealtimeDO(state, {})

    const res = await durableObject.fetch(
      new Request('https://do-internal/fanout', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(createEvent({ targetUserId: 'user-1' })),
      }),
    )

    expect(res.status).toBe(204)
    expect(targetSocket.send).toHaveBeenCalledTimes(1)
    expect(otherSocket.send).not.toHaveBeenCalled()
  })
})

describe('handleRealtimeBatch', () => {
  it('retries the queue message when the DO fanout response is non-2xx', async () => {
    const ack = vi.fn()
    const retry = vi.fn()
    const fetch = vi.fn().mockResolvedValue(new Response('nope', { status: 503 }))
    const env = {
      DO_REALTIME: {
        idFromName: vi.fn().mockReturnValue('do-id'),
        get: vi.fn().mockReturnValue({ fetch }),
      },
    }

    await handleRealtimeBatch(
      {
        queue: 'zync-realtime',
        messages: [{ body: createEvent(), ack, retry }],
      } as unknown as MessageBatch<RealtimeEvent>,
      env as any,
    )

    expect(ack).not.toHaveBeenCalled()
    expect(retry).toHaveBeenCalledTimes(1)
  })
})

describe('realtimeRoutes', () => {
  it('rewrites the upgrade request to the internal DO connect path and trusted user header', async () => {
    const fetch = vi.fn().mockResolvedValue(new Response(null, { status: 204 }))
    const res = await realtimeRoutes.fetch(
      new Request('http://example.com/connect', {
        headers: {
          Upgrade: 'websocket',
        },
      }),
      {
        DO_REALTIME: {
          idFromName: vi.fn().mockReturnValue('do-id'),
          get: vi.fn().mockReturnValue({ fetch }),
        },
      } as any,
    )

    expect(res.status).toBe(204)
    expect(fetch).toHaveBeenCalledTimes(1)
    const forwarded = fetch.mock.calls[0][0] as Request
    expect(forwarded.url).toBe('https://do-internal/connect')
    expect(forwarded.headers.get('X-User-Id')).toBe('user-1')
    expect(forwarded.headers.get('Upgrade')).toBe('websocket')
  })
})
