import { EventEmitter } from 'node:events'
import Fastify from 'fastify'
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import { buildSseListeners, formatHeartbit, formatSSE, isAdminUser, registerSseRoutes } from '../sse.js'

describe('formatSSE', () => {
  it('formats event with data', () => {
    const result = formatSSE('message:new', { orderId: 'o1' })
    expect(result).toBe('event: message:new\ndata: {"orderId":"o1"}\n\n')
  })

  it('formats event without data', () => {
    const result = formatSSE('heartbit')
    expect(result).toBe('event: heartbit\n\n')
  })
})

describe('formatHeartbit', () => {
  it('returns SSE comment', () => {
    expect(formatHeartbit()).toBe(':heartbit\n\n')
  })
})

describe('isAdminUser', () => {
  it('returns false for non-matching email', () => {
    expect(isAdminUser({ email: 'user@test.com' })).toBe(false)
  })

  it('returns true when email matches ADMIN_EMAIL', () => {
    const adminEmail = process.env.ADMIN_EMAIL
    if (!adminEmail) {
      console.warn('ADMIN_EMAIL not set, skipping')
      return
    }
    expect(isAdminUser({ email: adminEmail })).toBe(true)
  })

  it('returns false for null/undefined user', () => {
    expect(isAdminUser(null)).toBe(false)
    expect(isAdminUser(undefined)).toBe(false)
  })
})

describe('buildSseListeners', () => {
  let eventBus
  let write

  beforeEach(() => {
    eventBus = new EventEmitter()
    eventBus.setMaxListeners(50)
    write = vi.fn()
  })

  it('forwards orderMessage:adminReply to matching userId', () => {
    const cleanup = buildSseListeners('user-1', false, eventBus, write)
    eventBus.emit('orderMessage:adminReply', { orderId: 'o1', userId: 'user-1', messageId: 'm1', preview: 'Hi' })
    expect(write).toHaveBeenCalledTimes(1)
    expect(write.mock.calls[0][0]).toContain('event: message:new')
    expect(write.mock.calls[0][0]).toContain('"orderId":"o1"')
    cleanup()
  })

  it('ignores orderMessage:adminReply for non-matching userId', () => {
    const cleanup = buildSseListeners('user-2', false, eventBus, write)
    eventBus.emit('orderMessage:adminReply', { orderId: 'o1', userId: 'user-1', messageId: 'm1', preview: 'Hi' })
    expect(write).not.toHaveBeenCalled()
    cleanup()
  })

  it('forwards orderMessage:sent to admin', () => {
    const cleanup = buildSseListeners('admin-1', true, eventBus, write)
    eventBus.emit('orderMessage:sent', { orderId: 'o1', authorType: 'user', messageId: 'm1', preview: 'Hello' })
    expect(write).toHaveBeenCalledTimes(1)
    expect(write.mock.calls[0][0]).toContain('event: message:new')
    cleanup()
  })

  it('ignores orderMessage:sent for non-admin', () => {
    const cleanup = buildSseListeners('user-1', false, eventBus, write)
    eventBus.emit('orderMessage:sent', { orderId: 'o1', authorType: 'user', messageId: 'm1', preview: 'Hello' })
    expect(write).not.toHaveBeenCalled()
    cleanup()
  })

  it('forwards order:statusChanged to matching userId', () => {
    const cleanup = buildSseListeners('user-1', false, eventBus, write)
    eventBus.emit('order:statusChanged', {
      orderId: 'o1',
      userId: 'user-1',
      oldStatus: 'PENDING_PAYMENT',
      newStatus: 'PAID',
    })
    expect(write).toHaveBeenCalledTimes(1)
    expect(write.mock.calls[0][0]).toContain('event: order:statusChanged')
    expect(write.mock.calls[0][0]).toContain('"newStatus":"PAID"')
    cleanup()
  })

  it('forwards payment:statusChanged to matching userId', () => {
    const cleanup = buildSseListeners('user-1', false, eventBus, write)
    eventBus.emit('payment:statusChanged', { orderId: 'o1', userId: 'user-1', paymentStatus: 'paid' })
    expect(write).toHaveBeenCalledTimes(1)
    expect(write.mock.calls[0][0]).toContain('event: order:statusChanged')
    cleanup()
  })

  it('forwards order:deliveryFeeAdjusted to matching userId', () => {
    const cleanup = buildSseListeners('user-1', false, eventBus, write)
    eventBus.emit('order:deliveryFeeAdjusted', { orderId: 'o1', userId: 'user-1', totalCents: 50000 })
    expect(write).toHaveBeenCalledTimes(1)
    expect(write.mock.calls[0][0]).toContain('event: order:updated')
    cleanup()
  })

  it('forwards order:created to admin', () => {
    const cleanup = buildSseListeners('admin-1', true, eventBus, write)
    eventBus.emit('order:created', { orderId: 'o1', userId: 'user-1', totalCents: 50000 })
    expect(write).toHaveBeenCalledTimes(1)
    expect(write.mock.calls[0][0]).toContain('event: order:new')
    cleanup()
  })

  it('forwards order:created:admin to admin', () => {
    const cleanup = buildSseListeners('admin-1', true, eventBus, write)
    eventBus.emit('order:created:admin', { orderId: 'o1', userId: 'user-1', userEmail: 'user@test.com' })
    expect(write).toHaveBeenCalledTimes(1)
    expect(write.mock.calls[0][0]).toContain('event: order:new')
    cleanup()
  })

  it('ignores order:created for non-admin', () => {
    const cleanup = buildSseListeners('user-1', false, eventBus, write)
    eventBus.emit('order:created', { orderId: 'o1', userId: 'user-1', totalCents: 50000 })
    expect(write).not.toHaveBeenCalled()
    cleanup()
  })

  it('cleanup removes all listeners', () => {
    const cleanup = buildSseListeners('user-1', false, eventBus, write)
    cleanup()
    eventBus.emit('orderMessage:adminReply', { orderId: 'o1', userId: 'user-1', messageId: 'm1', preview: 'Hi' })
    expect(write).not.toHaveBeenCalled()
  })
})

describe('GET /api/sse/stream (integration)', () => {
  let app

  beforeAll(async () => {
    app = Fastify({ logger: false })
    app.decorate('authenticate', async function (request, reply) {
      try {
        const token = request.query?.token
        if (!token) throw new Error('no token')
        if (token === 'user-token') {
          request.user = { sub: 'user-1', email: 'user@test.com' }
        } else if (token === 'admin-token') {
          request.user = { sub: 'admin-1', email: process.env.ADMIN_EMAIL || 'admin@test.com' }
        } else {
          throw new Error('bad token')
        }
      } catch {
        return reply.code(401).send({ error: 'Unauthorized' })
      }
    })
    app.decorate('eventBus', new EventEmitter())
    await registerSseRoutes(app)
    await app.ready()
  })

  afterAll(async () => {
    await app.close()
  })

  it('returns 401 without token', async () => {
    const res = await app.inject({ method: 'GET', url: '/api/sse/stream' })
    expect(res.statusCode).toBe(401)
  })

  it('returns 200 and event-stream headers for authenticated user', async () => {
    const res = await app.inject({ method: 'GET', url: '/api/sse/stream?token=user-token', payloadAsStream: true })
    expect(res.statusCode).toBe(200)
    expect(res.headers['content-type']).toBe('text/event-stream')
    expect(res.headers['cache-control']).toBe('no-cache')
    expect(res.headers['connection']).toBe('keep-alive')
  })

  it('sends initial heartbit', async () => {
    const res = await app.inject({ method: 'GET', url: '/api/sse/stream?token=user-token', payloadAsStream: true })
    const body = res.stream().read().toString()
    expect(body).toContain(':heartbit')
  })
})
