// @vitest-environment happy-dom
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { mount } from '@vue/test-utils'
import { createWsMixin } from '../../src/wsMixin.js'
import { createInitialState } from '../../src/gameState.js'

// WebSocket mock controllabile: i gestori (onopen/onmessage/onclose) vengono
// assegnati dal mixin e li invochiamo manualmente nei test.
class MockWebSocket {
    static CONNECTING = 0
    static OPEN = 1
    static CLOSING = 2
    static CLOSED = 3
    constructor(url) {
        this.url = url
        this.readyState = MockWebSocket.CONNECTING
        this.send = vi.fn()
        this.close = vi.fn()
        MockWebSocket.instances.push(this)
    }
}
MockWebSocket.instances = []

vi.stubGlobal('WebSocket', MockWebSocket)

// Monta un componente che usa il mixin. `extra` permette di aggiungere hook.
function mountWith(role = 'controller', extra = {}) {
    const Comp = {
        mixins: [createWsMixin(role)],
        template: '<div></div>',
        ...extra,
    }
    return mount(Comp)
}

function ultimaWs() {
    return MockWebSocket.instances.at(-1)
}

describe('createWsMixin (wsMixin.js)', () => {
    beforeEach(() => {
        MockWebSocket.instances = []
    })

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

    describe('computed derivati', () => {
        it('punt/servHome/set delegano alle funzioni pure sulla striscia', async () => {
            const wrapper = mountWith()
            wrapper.vm.state.sp.striscia = [
                { serv: 'h', ris: 'h', vinc: 'h' },
                { serv: 'h', ris: 'h'.repeat(10) + 'g'.repeat(8), vinc: null },
            ]
            await wrapper.vm.$nextTick()
            expect(wrapper.vm.punt).toEqual({ home: 10, guest: 8 })
            expect(wrapper.vm.set).toEqual({ home: 1, guest: 0 })
            // ultimo punto 'g' → serve guest
            expect(wrapper.vm.servHome).toBe(false)
        })
    })

    describe('connessione', () => {
        it('apre una WebSocket verso /ws al mount', () => {
            mountWith()
            const ws = ultimaWs()
            expect(ws).toBeDefined()
            expect(ws.url).toMatch(/^ws:\/\/.+\/ws$/)
        })

        it('invia il messaggio register all\'apertura', () => {
            const wrapper = mountWith('controller')
            const ws = ultimaWs()
            ws.readyState = MockWebSocket.OPEN
            ws.onopen()
            expect(ws.send).toHaveBeenCalledTimes(1)
            const msg = JSON.parse(ws.send.mock.calls[0][0])
            expect(msg).toEqual({ type: 'register', role: 'controller' })
            expect(wrapper.vm.wsConnected).toBe(true)
        })

        it('un messaggio "state" aggiorna lo stato locale', () => {
            const wrapper = mountWith()
            const ws = ultimaWs()
            const nuovo = createInitialState()
            nuovo.sp.nomi.home = 'Nuova Squadra'
            ws.onmessage({ data: JSON.stringify({ type: 'state', state: nuovo }) })
            expect(wrapper.vm.state.sp.nomi.home).toBe('Nuova Squadra')
        })

        it('un messaggio non-state invoca l\'hook onWsMessage', () => {
            const onWsMessage = vi.fn()
            const wrapper = mountWith('display', { methods: { onWsMessage } })
            const ws = ultimaWs()
            ws.onmessage({ data: JSON.stringify({ type: 'speak', text: 'ciao' }) })
            expect(onWsMessage).toHaveBeenCalledWith({ type: 'speak', text: 'ciao' })
        })
    })

    describe('riconnessione', () => {
        it('scheduleReconnect usa backoff esponenziale con cap a 30s', () => {
            const wrapper = mountWith()
            const delays = []
            const spy = vi.spyOn(globalThis, 'setTimeout').mockImplementation(() => 123)
            // intercetta i delay leggendoli dalle chiamate
            spy.mockImplementation((_fn, d) => { delays.push(d); return 123 })

            wrapper.vm.reconnectAttempts = 0
            wrapper.vm.reconnectTimeout = null
            wrapper.vm.scheduleReconnect() // 1000
            wrapper.vm.reconnectTimeout = null
            wrapper.vm.scheduleReconnect() // 2000
            wrapper.vm.reconnectTimeout = null
            wrapper.vm.scheduleReconnect() // 4000
            expect(delays).toEqual([1000, 2000, 4000])

            // attempts alto → cap a 30000
            delays.length = 0
            wrapper.vm.reconnectAttempts = 20
            wrapper.vm.reconnectTimeout = null
            wrapper.vm.scheduleReconnect()
            expect(delays[0]).toBe(30000)

            spy.mockRestore()
        })

        it('non riconnette su chiusura pulita (1000/1001)', () => {
            const wrapper = mountWith()
            const ws = ultimaWs()
            const spy = vi.spyOn(wrapper.vm, 'scheduleReconnect')
            ws.onclose({ code: 1000 })
            ws.onclose({ code: 1001 })
            expect(spy).not.toHaveBeenCalled()
            expect(wrapper.vm.wsConnected).toBe(false)
        })

        it('riconnette su chiusura anomala (es. 1006)', () => {
            const wrapper = mountWith()
            const ws = ultimaWs()
            const spy = vi.spyOn(wrapper.vm, 'scheduleReconnect')
            ws.onclose({ code: 1006 })
            expect(spy).toHaveBeenCalled()
        })

        it('allo scadere del timer riapre davvero la connessione', () => {
            vi.useFakeTimers()
            try {
                const wrapper = mountWith()
                // il mock non fa mai scattare onopen: sblocca la guardia isConnecting
                wrapper.vm.isConnecting = false
                const prima = MockWebSocket.instances.length
                wrapper.vm.scheduleReconnect()
                vi.advanceTimersByTime(1000)
                expect(MockWebSocket.instances.length).toBe(prima + 1)
                expect(wrapper.vm.reconnectTimeout).toBe(null)
            } finally {
                vi.useRealTimers()
            }
        })

        it('se il costruttore WebSocket lancia, programma la riconnessione', () => {
            const wrapper = mountWith()
            wrapper.vm.isConnecting = false
            const spy = vi.spyOn(wrapper.vm, 'scheduleReconnect').mockImplementation(() => {})
            const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
            vi.stubGlobal('WebSocket', class { constructor() { throw new Error('boom') } })
            try {
                wrapper.vm.connectWebSocket()
                expect(spy).toHaveBeenCalled()
                expect(wrapper.vm.isConnecting).toBe(false)
                expect(wrapper.vm.ws).toBe(null)
            } finally {
                vi.stubGlobal('WebSocket', MockWebSocket)
                consoleSpy.mockRestore()
            }
        })

        it('onerror azzera lo stato di connessione', () => {
            const wrapper = mountWith()
            const ws = ultimaWs()
            wrapper.vm.wsConnected = true
            wrapper.vm.isConnecting = true
            ws.onerror()
            expect(wrapper.vm.wsConnected).toBe(false)
            expect(wrapper.vm.isConnecting).toBe(false)
        })
    })

    describe('sendWs', () => {
        it('ritorna false se non connesso', () => {
            const wrapper = mountWith()
            wrapper.vm.wsConnected = false
            expect(wrapper.vm.sendWs({ type: 'action' })).toBe(false)
        })

        it('serializza e invia se connesso e aperto', () => {
            const wrapper = mountWith()
            const ws = ultimaWs()
            ws.readyState = MockWebSocket.OPEN
            wrapper.vm.wsConnected = true
            const ok = wrapper.vm.sendWs({ type: 'action', action: { type: 'incPunt' } })
            expect(ok).toBe(true)
            const inviato = JSON.parse(ws.send.mock.calls.at(-1)[0])
            expect(inviato.type).toBe('action')
        })

        it('ritorna false se send lancia', () => {
            const wrapper = mountWith()
            const ws = ultimaWs()
            ws.readyState = MockWebSocket.OPEN
            wrapper.vm.wsConnected = true
            ws.send.mockImplementation(() => { throw new Error('boom') })
            const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
            expect(wrapper.vm.sendWs({ type: 'action' })).toBe(false)
            consoleSpy.mockRestore()
        })
    })

    describe('cleanup', () => {
        it('beforeUnmount chiude la WebSocket', () => {
            const wrapper = mountWith()
            const ws = ultimaWs()
            wrapper.unmount()
            expect(ws.close).toHaveBeenCalled()
        })

        it('beforeUnmount con connessione OPEN chiude con codice 1000', () => {
            const wrapper = mountWith()
            const ws = ultimaWs()
            ws.readyState = MockWebSocket.OPEN
            wrapper.unmount()
            expect(ws.close).toHaveBeenCalledWith(1000, 'Component unmounting')
        })

        it('beforeUnmount non lancia se ws.close lancia', () => {
            const wrapper = mountWith()
            const ws = ultimaWs()
            ws.readyState = MockWebSocket.OPEN
            ws.close.mockImplementation(() => { throw new Error('già chiusa') })
            const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
            expect(() => wrapper.unmount()).not.toThrow()
            expect(consoleSpy).toHaveBeenCalled()
        })

        it('beforeUnmount annulla un reconnectTimeout pendente', () => {
            vi.useFakeTimers()
            try {
                const wrapper = mountWith()
                wrapper.vm.isConnecting = false
                wrapper.vm.scheduleReconnect()
                expect(wrapper.vm.reconnectTimeout).not.toBe(null)
                wrapper.unmount()
                // se il timer non fosse stato annullato, allo scoccare del delay
                // verrebbe creata una nuova connessione anche a componente smontato
                const primaDelTimeout = MockWebSocket.instances.length
                vi.advanceTimersByTime(30000)
                expect(MockWebSocket.instances.length).toBe(primaDelTimeout)
            } finally {
                vi.useRealTimers()
            }
        })
    })

    describe('rami aggiuntivi', () => {
        it('connectWebSocket non fa nulla se è già in corso un tentativo di connessione', () => {
            const wrapper = mountWith()
            const primaDelTimeout = MockWebSocket.instances.length
            wrapper.vm.isConnecting = true
            wrapper.vm.connectWebSocket()
            expect(MockWebSocket.instances.length).toBe(primaDelTimeout)
        })

        it('riconnettendosi con la WebSocket precedente OPEN, la chiude con codice 1000', () => {
            const wrapper = mountWith()
            const prima = ultimaWs()
            prima.readyState = MockWebSocket.OPEN
            wrapper.vm.isConnecting = false
            wrapper.vm.connectWebSocket()
            expect(prima.close).toHaveBeenCalledWith(1000, 'Reconnecting')
        })

        it('se la chiusura della WebSocket precedente lancia, logga e continua a riconnettersi', () => {
            const wrapper = mountWith()
            const prima = ultimaWs()
            prima.readyState = MockWebSocket.OPEN
            prima.close.mockImplementation(() => { throw new Error('già chiusa') })
            const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
            wrapper.vm.isConnecting = false

            expect(() => wrapper.vm.connectWebSocket()).not.toThrow()
            expect(consoleSpy).toHaveBeenCalled()
            expect(ultimaWs()).not.toBe(prima)
        })

        it('onopen non lancia se il registro fallisce (send che lancia)', () => {
            const wrapper = mountWith()
            const ws = ultimaWs()
            ws.readyState = MockWebSocket.OPEN
            ws.send.mockImplementation(() => { throw new Error('boom') })
            const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})

            expect(() => ws.onopen()).not.toThrow()
            expect(wrapper.vm.wsConnected).toBe(true)
            expect(consoleSpy).toHaveBeenCalled()
        })

        it('onmessage non lancia su JSON non valido', () => {
            mountWith()
            const ws = ultimaWs()
            const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})

            expect(() => ws.onmessage({ data: 'non è JSON {{{' })).not.toThrow()
            expect(consoleSpy).toHaveBeenCalled()
        })

        it('scheduleReconnect non arma un secondo timer se uno è già pendente', () => {
            const wrapper = mountWith()
            const spy = vi.spyOn(globalThis, 'setTimeout')
            wrapper.vm.reconnectTimeout = 123 // timer già pendente (valore fittizio)
            wrapper.vm.scheduleReconnect()
            expect(spy).not.toHaveBeenCalled()
        })
    })
})
