import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
import { setupWebSocketHandler } from '../../src/websocket-handler.js'
import { punteggio } from '../../src/gameState.js'
import { EventEmitter } from 'events'

// Il punteggio non è memorizzato nello stato: si ricava dalla striscia.
const puntHome = (state) => punteggio(state.sp.striscia).home

// Mock parziale di una WebSocket e del Server
class MockWebSocket extends EventEmitter {
    constructor() {
        super()
        this.readyState = 1 // OPEN
    }
    send = vi.fn()
    terminate = vi.fn()
}

class MockWebSocketServer extends EventEmitter {
    clients = new Set()
}

// Helper: connette e registra un client
function connectAndRegister(wss, role) {
    const ws = new MockWebSocket()
    wss.emit('connection', ws)
    wss.clients.add(ws)
    ws.emit('message', JSON.stringify({ type: 'register', role }))
    ws.send.mockClear()
    return ws
}

// Helper: ultimo messaggio inviato a un ws
function lastSent(ws) {
    const calls = ws.send.mock.calls
    return JSON.parse(calls[calls.length - 1][0])
}

describe('WebSocket Integration (websocket-handler.js)', () => {
    let wss
    let handler

    beforeEach(() => {
        wss = new MockWebSocketServer()
        handler = setupWebSocketHandler(wss)
    })

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

    // =============================================
    // REGISTRAZIONE
    // =============================================
    describe('Registrazione', () => {
        it('dovrebbe registrare un client come "display" e inviare lo stato', () => {
            const ws = new MockWebSocket()
            wss.emit('connection', ws)
            wss.clients.add(ws)
            ws.emit('message', JSON.stringify({ type: 'register', role: 'display' }))

            expect(ws.send).toHaveBeenCalled()
            const sentMsg = JSON.parse(ws.send.mock.calls[0][0])
            expect(sentMsg.type).toBe('state')
            expect(sentMsg.state).toBeDefined()
        })

        it('dovrebbe registrare un client come "controller"', () => {
            connectAndRegister(wss, 'controller')
            expect(handler.getClients().size).toBe(1)
        })

        it('dovrebbe rifiutare ruolo non valido', () => {
            const ws = new MockWebSocket()
            wss.emit('connection', ws)
            wss.clients.add(ws)
            ws.emit('message', JSON.stringify({ type: 'register', role: 'hacker' }))

            const sentMsg = JSON.parse(ws.send.mock.calls[0][0])
            expect(sentMsg.type).toBe('error')
            expect(sentMsg.message).toContain('Invalid role')
        })

        it('dovrebbe usare "display" come ruolo default se mancante', () => {
            const ws = new MockWebSocket()
            wss.emit('connection', ws)
            wss.clients.add(ws)
            ws.emit('message', JSON.stringify({ type: 'register' }))

            const sentMsg = JSON.parse(ws.send.mock.calls[0][0])
            expect(sentMsg.type).toBe('state')
        })
    })

    // =============================================
    // AZIONI
    // =============================================
    describe('Azioni', () => {
        it('dovrebbe permettere al controller di cambiare il punteggio', () => {
            const controller = connectAndRegister(wss, 'controller')

            controller.emit('message', JSON.stringify({
                type: 'action',
                action: { type: 'incPunt', team: 'home' }
            }))

            expect(controller.send).toHaveBeenCalled()
            const sentMsg = lastSent(controller)
            expect(sentMsg.type).toBe('state')
            expect(puntHome(sentMsg.state)).toBe(1)
        })

        it('dovrebbe impedire al display di inviare azioni', () => {
            const display = connectAndRegister(wss, 'display')

            display.emit('message', JSON.stringify({
                type: 'action',
                action: { type: 'incPunt', team: 'home' }
            }))

            const sentMsg = lastSent(display)
            expect(sentMsg.type).toBe('error')
            expect(sentMsg.message).toContain('Only controllers')
        })

        it('dovrebbe impedire azioni da client non registrati', () => {
            const ws = new MockWebSocket()
            wss.emit('connection', ws)
            wss.clients.add(ws)

            ws.emit('message', JSON.stringify({
                type: 'action',
                action: { type: 'incPunt', team: 'home' }
            }))

            const sentMsg = JSON.parse(ws.send.mock.calls[0][0])
            expect(sentMsg.type).toBe('error')
            expect(sentMsg.message).toContain('Only controllers')
        })

        it('dovrebbe rifiutare azione con formato invalido (missing action)', () => {
            const controller = connectAndRegister(wss, 'controller')

            controller.emit('message', JSON.stringify({
                type: 'action'
            }))

            const sentMsg = lastSent(controller)
            expect(sentMsg.type).toBe('error')
            expect(sentMsg.message).toContain('Invalid action format')
        })

        it('dovrebbe rifiutare azione con formato invalido (missing action.type)', () => {
            const controller = connectAndRegister(wss, 'controller')

            controller.emit('message', JSON.stringify({
                type: 'action',
                action: { team: 'home' }
            }))

            const sentMsg = lastSent(controller)
            expect(sentMsg.type).toBe('error')
            expect(sentMsg.message).toContain('Invalid action format')
        })
    })

    // =============================================
    // TIME OUT (timer server-side)
    // =============================================
    describe('Time out', () => {
        beforeEach(() => {
            vi.useFakeTimers()
        })

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

        function inviaStartTimeout(controller, overrides = {}) {
            controller.emit('message', JSON.stringify({
                type: 'action',
                action: { type: 'startTimeout', team: 'home', video: 'spot.mp4', ...overrides }
            }))
        }

        it('dovrebbe iniettare startedAt lato server (ignorando quello del client)', () => {
            vi.setSystemTime(50000)
            const controller = connectAndRegister(wss, 'controller')

            inviaStartTimeout(controller, { startedAt: 12345 })

            const state = handler.getState()
            expect(state.timeout).toBe(true)
            expect(state.timeoutStartedAt).toBe(50000)
        })

        it('dovrebbe chiudere automaticamente il time out dopo 30 secondi', () => {
            const controller = connectAndRegister(wss, 'controller')
            const display = connectAndRegister(wss, 'display')

            inviaStartTimeout(controller)
            expect(handler.getState().timeout).toBe(true)

            vi.advanceTimersByTime(29999)
            expect(handler.getState().timeout).toBe(true)

            vi.advanceTimersByTime(1)
            const state = handler.getState()
            expect(state.timeout).toBe(false)
            expect(state.timeoutTeam).toBe(null)
            expect(state.timeoutVideo).toBe(null)
            expect(state.timeoutStartedAt).toBe(null)

            // La chiusura automatica viene broadcastata ai client
            const msg = lastSent(display)
            expect(msg.type).toBe('state')
            expect(msg.state.timeout).toBe(false)
        })

        it('la chiusura automatica dovrebbe persistere lo stato (onStateChange)', () => {
            const onStateChange = vi.fn()
            wss = new MockWebSocketServer()
            handler = setupWebSocketHandler(wss, { onStateChange })
            const controller = connectAndRegister(wss, 'controller')

            inviaStartTimeout(controller)
            onStateChange.mockClear()

            vi.advanceTimersByTime(30000)
            expect(onStateChange).toHaveBeenCalledTimes(1)
            expect(onStateChange.mock.calls[0][0].timeout).toBe(false)
        })

        it('stopTimeout manuale dovrebbe annullare il timer pendente', () => {
            const controller = connectAndRegister(wss, 'controller')

            inviaStartTimeout(controller)
            controller.emit('message', JSON.stringify({
                type: 'action',
                action: { type: 'stopTimeout' }
            }))
            expect(handler.getState().timeout).toBe(false)
            controller.send.mockClear()

            // Allo scadere dei 30s non deve partire nessun broadcast fantasma
            vi.advanceTimersByTime(30000)
            expect(controller.send).not.toHaveBeenCalled()
        })

        it('un nuovo startTimeout dovrebbe riarmare il timer da capo', () => {
            const controller = connectAndRegister(wss, 'controller')

            inviaStartTimeout(controller)
            vi.advanceTimersByTime(20000)

            // Riavvio: il timer riparte da zero
            inviaStartTimeout(controller, { team: 'guest' })
            vi.advanceTimersByTime(20000)
            expect(handler.getState().timeout).toBe(true)
            expect(handler.getState().timeoutTeam).toBe('guest')

            vi.advanceTimersByTime(10000)
            expect(handler.getState().timeout).toBe(false)
        })

        it('startTimeout con squadra non valida non dovrebbe armare il timer', () => {
            const controller = connectAndRegister(wss, 'controller')

            inviaStartTimeout(controller, { team: 'arbitro' })
            expect(handler.getState().timeout).toBe(false)
            controller.send.mockClear()

            vi.advanceTimersByTime(30000)
            expect(controller.send).not.toHaveBeenCalled()
        })
    })

    // =============================================
    // BROADCAST MULTI-CLIENT
    // =============================================
    describe('Broadcast', () => {
        it('dovrebbe inviare lo stato a tutti i client dopo un\'azione', () => {
            const controller = connectAndRegister(wss, 'controller')
            const display1 = connectAndRegister(wss, 'display')
            const display2 = connectAndRegister(wss, 'display')

            controller.emit('message', JSON.stringify({
                type: 'action',
                action: { type: 'incPunt', team: 'home' }
            }))

            // Tutti i client nel set dovrebbero aver ricevuto lo stato
            expect(controller.send).toHaveBeenCalled()
            expect(display1.send).toHaveBeenCalled()
            expect(display2.send).toHaveBeenCalled()

            const msg1 = lastSent(display1)
            const msg2 = lastSent(display2)
            expect(msg1.type).toBe('state')
            expect(puntHome(msg1.state)).toBe(1)
            expect(puntHome(msg2.state)).toBe(1)
        })

        it('non dovrebbe inviare a client con readyState != OPEN', () => {
            const controller = connectAndRegister(wss, 'controller')
            const closedClient = connectAndRegister(wss, 'display')
            closedClient.readyState = 3 // CLOSED

            controller.emit('message', JSON.stringify({
                type: 'action',
                action: { type: 'incPunt', team: 'home' }
            }))

            // closedClient non dovrebbe aver ricevuto il broadcast
            expect(closedClient.send).not.toHaveBeenCalled()
        })
    })

    // =============================================
    // SPEAK
    // =============================================
    describe('Speak', () => {
        it('dovrebbe inoltrare il messaggio speak solo ai display', () => {
            const controller = connectAndRegister(wss, 'controller')
            const display = connectAndRegister(wss, 'display')

            controller.emit('message', JSON.stringify({
                type: 'speak',
                text: 'quindici a dieci'
            }))

            // Il display riceve il messaggio speak
            expect(display.send).toHaveBeenCalled()
            const msg = lastSent(display)
            expect(msg.type).toBe('speak')
            expect(msg.text).toBe('quindici a dieci')
        })

        it('non dovrebbe permettere al display di inviare speak', () => {
            const display = connectAndRegister(wss, 'display')

            display.emit('message', JSON.stringify({
                type: 'speak',
                text: 'test'
            }))

            const msg = lastSent(display)
            expect(msg.type).toBe('error')
            expect(msg.message).toContain('Only controllers')
        })

        it('dovrebbe rifiutare speak con testo vuoto', () => {
            const controller = connectAndRegister(wss, 'controller')

            controller.emit('message', JSON.stringify({
                type: 'speak',
                text: '   '
            }))

            const msg = lastSent(controller)
            expect(msg.type).toBe('error')
            expect(msg.message).toContain('Invalid speak payload')
        })

        it('dovrebbe rifiutare speak senza testo', () => {
            const controller = connectAndRegister(wss, 'controller')

            controller.emit('message', JSON.stringify({
                type: 'speak'
            }))

            const msg = lastSent(controller)
            expect(msg.type).toBe('error')
        })

        it('dovrebbe fare trim del testo speak', () => {
            const controller = connectAndRegister(wss, 'controller')
            const display = connectAndRegister(wss, 'display')

            controller.emit('message', JSON.stringify({
                type: 'speak',
                text: '  dieci a otto  '
            }))

            const msg = lastSent(display)
            expect(msg.text).toBe('dieci a otto')
        })
    })

    // =============================================
    // MESSAGGI MALFORMATI
    // =============================================
    describe('Messaggi malformati', () => {
        it('dovrebbe gestire JSON non valido senza crash', () => {
            const ws = new MockWebSocket()
            wss.emit('connection', ws)
            wss.clients.add(ws)

            expect(() => {
                ws.emit('message', 'questo non è JSON {{{')
            }).not.toThrow()

            const msg = lastSent(ws)
            expect(msg.type).toBe('error')
            expect(msg.message).toContain('Invalid message format')
        })

        it('dovrebbe gestire Buffer come input', () => {
            const controller = connectAndRegister(wss, 'controller')

            const buf = Buffer.from(JSON.stringify({
                type: 'action',
                action: { type: 'incPunt', team: 'home' }
            }))

            controller.emit('message', buf)

            const msg = lastSent(controller)
            expect(msg.type).toBe('state')
            expect(puntHome(msg.state)).toBe(1)
        })
    })

    // =============================================
    // DISCONNESSIONE
    // =============================================
    describe('Disconnessione', () => {
        it('dovrebbe rimuovere il client dalla mappa alla disconnessione', () => {
            const controller = connectAndRegister(wss, 'controller')
            expect(handler.getClients().size).toBe(1)

            controller.emit('close')

            expect(handler.getClients().size).toBe(0)
        })

        it('i client rimanenti non dovrebbero essere affetti dalla disconnessione', () => {
            const controller = connectAndRegister(wss, 'controller')
            const display = connectAndRegister(wss, 'display')
            expect(handler.getClients().size).toBe(2)

            controller.emit('close')
            expect(handler.getClients().size).toBe(1)

            expect(handler.getClients().has(display)).toBe(true)
        })
    })

    // =============================================
    // ERRORI WEBSOCKET
    // =============================================
    describe('Errori WebSocket', () => {
        it('dovrebbe terminare la connessione per errore UTF8 invalido', () => {
            const ws = new MockWebSocket()
            wss.emit('connection', ws)

            const err = new Error('Invalid UTF8')
            err.code = 'WS_ERR_INVALID_UTF8'
            ws.emit('error', err)

            expect(ws.terminate).toHaveBeenCalled()
        })

        it('dovrebbe terminare la connessione per close code invalido', () => {
            const ws = new MockWebSocket()
            wss.emit('connection', ws)

            const err = new Error('Invalid close code')
            err.code = 'WS_ERR_INVALID_CLOSE_CODE'
            ws.emit('error', err)

            expect(ws.terminate).toHaveBeenCalled()
        })

        it('non dovrebbe terminare per altri errori', () => {
            const ws = new MockWebSocket()
            wss.emit('connection', ws)

            const err = new Error('Generic error')
            ws.emit('error', err)

            expect(ws.terminate).not.toHaveBeenCalled()
        })
    })

    // =============================================
    // API PUBBLICA
    // =============================================
    describe('API pubblica', () => {
        it('getState dovrebbe restituire lo stato corrente', () => {
            const state = handler.getState()
            expect(puntHome(state)).toBe(0)
            expect(punteggio(state.sp.striscia).guest).toBe(0)
        })

        it('setState dovrebbe sovrascrivere lo stato', () => {
            const newState = handler.getState()
            newState.sp.striscia.at(-1).ris = 'hh'
            handler.setState(newState)
            expect(puntHome(handler.getState())).toBe(2)
        })

        it('broadcastState dovrebbe inviare a tutti i client', () => {
            const display = connectAndRegister(wss, 'display')
            handler.broadcastState()
            expect(display.send).toHaveBeenCalled()
            const msg = lastSent(display)
            expect(msg.type).toBe('state')
        })

        it('getClients dovrebbe restituire la mappa dei client', () => {
            expect(handler.getClients()).toBeInstanceOf(Map)
            expect(handler.getClients().size).toBe(0)
            connectAndRegister(wss, 'display')
            expect(handler.getClients().size).toBe(1)
        })
    })
})
