package checker

import (
	"context"
	"encoding/binary"
	"net"
	"sync/atomic"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// fakeUDPRelay listens on a UDP socket and echoes SOCKS5-wrapped STUN
// binding requests as a synthetic Binding Success Response, just like
// fakeProxy.runRelay in checker_test.go but standalone (no SOCKS5 TCP
// control channel needed). dropEveryN > 0 drops every Nth packet.
type fakeUDPRelay struct {
	conn       *net.UDPConn
	addr       *net.UDPAddr
	dropEveryN atomic.Int32
	count      atomic.Int32
}

func newFakeUDPRelay(t *testing.T) *fakeUDPRelay {
	t.Helper()
	pc, err := net.ListenPacket("udp", "127.0.0.1:0")
	require.NoError(t, err)
	uconn := pc.(*net.UDPConn)
	r := &fakeUDPRelay{
		conn: uconn,
		addr: uconn.LocalAddr().(*net.UDPAddr),
	}
	t.Cleanup(func() { _ = uconn.Close() })
	go r.serve()
	return r
}

func (r *fakeUDPRelay) serve() {
	buf := make([]byte, 2048)
	for {
		n, src, err := r.conn.ReadFromUDP(buf)
		if err != nil {
			return
		}
		if dropN := r.dropEveryN.Load(); dropN > 0 {
			c := r.count.Add(1)
			if c%dropN == 0 {
				continue
			}
		} else {
			r.count.Add(1)
		}
		if n < 10 {
			continue
		}
		if buf[0] != 0x00 || buf[1] != 0x00 || buf[2] != 0x00 {
			continue
		}
		var hdrLen int
		switch buf[3] {
		case 0x01:
			hdrLen = 10
		case 0x04:
			hdrLen = 22
		case 0x03:
			if n < 5 {
				continue
			}
			hdrLen = 4 + 1 + int(buf[4]) + 2
		default:
			continue
		}
		if n < hdrLen+20 {
			continue
		}
		stunReq := buf[hdrLen:n]
		var txID [12]byte
		copy(txID[:], stunReq[8:20])

		ip4 := src.IP.To4()
		if ip4 == nil {
			continue
		}
		xport := uint16(src.Port) ^ uint16(stunMagicCookie>>16)
		xaddr := binary.BigEndian.Uint32(ip4) ^ stunMagicCookie

		stunResp := make([]byte, 20+12)
		binary.BigEndian.PutUint16(stunResp[0:2], stunBindingSuccessResponse)
		binary.BigEndian.PutUint16(stunResp[2:4], 12)
		binary.BigEndian.PutUint32(stunResp[4:8], stunMagicCookie)
		copy(stunResp[8:20], txID[:])
		binary.BigEndian.PutUint16(stunResp[20:22], stunAttrXORMappedAddress)
		binary.BigEndian.PutUint16(stunResp[22:24], 8)
		stunResp[24] = 0
		stunResp[25] = 0x01
		binary.BigEndian.PutUint16(stunResp[26:28], xport)
		binary.BigEndian.PutUint32(stunResp[28:32], xaddr)

		out := make([]byte, 0, 10+len(stunResp))
		out = append(out, 0x00, 0x00, 0x00, 0x01)
		out = append(out, ip4...)
		var portBuf [2]byte
		binary.BigEndian.PutUint16(portBuf[:], uint16(src.Port))
		out = append(out, portBuf[:]...)
		out = append(out, stunResp...)

		_, _ = r.conn.WriteToUDP(out, src)
	}
}

// TestVoiceQualityBurst_Math: full 30-of-30 reception on localhost, all
// RTTs in single-digit milliseconds.
func TestVoiceQualityBurst_Math(t *testing.T) {
	relay := newFakeUDPRelay(t)
	clientPC, err := net.ListenPacket("udp", "127.0.0.1:0")
	require.NoError(t, err)
	defer clientPC.Close()

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	res, err := runVoiceQualityBurst(ctx, clientPC, relay.addr,
		"localhost", 19302, 30, 5*time.Millisecond)
	require.NoError(t, err)

	assert.Equal(t, 30, res.Sent)
	assert.Equal(t, 30, res.Received)
	assert.InDelta(t, 0.0, res.LossPct, 0.001)
	assert.Less(t, res.P50RTTMS, 50.0, "loopback p50 should be tiny")
}

// TestVoiceQualityBurst_HalfLoss verifies the loss-percentage math when
// the relay drops half the packets.
func TestVoiceQualityBurst_HalfLoss(t *testing.T) {
	relay := newFakeUDPRelay(t)
	relay.dropEveryN.Store(2) // every other packet → 50% loss

	clientPC, err := net.ListenPacket("udp", "127.0.0.1:0")
	require.NoError(t, err)
	defer clientPC.Close()

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	res, err := runVoiceQualityBurst(ctx, clientPC, relay.addr,
		"localhost", 19302, 20, 3*time.Millisecond)
	require.NoError(t, err)

	assert.Equal(t, 20, res.Sent)
	assert.InDelta(t, 50.0, res.LossPct, 5.0, "expected ~50%% loss got %+v", res)
}

// TestVoiceQualityBurst_AllDropped: dropEveryN=1 → 100% loss. Should NOT
// return an error; should report Sent=N, Received=0.
func TestVoiceQualityBurst_AllDropped(t *testing.T) {
	relay := newFakeUDPRelay(t)
	relay.dropEveryN.Store(1)

	clientPC, err := net.ListenPacket("udp", "127.0.0.1:0")
	require.NoError(t, err)
	defer clientPC.Close()

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	res, err := runVoiceQualityBurst(ctx, clientPC, relay.addr,
		"localhost", 19302, 10, 3*time.Millisecond)
	require.NoError(t, err)

	assert.Equal(t, 10, res.Sent)
	assert.Equal(t, 0, res.Received)
	assert.InDelta(t, 100.0, res.LossPct, 0.001)
	assert.Equal(t, 0.0, res.P50RTTMS)
	assert.Equal(t, 0.0, res.JitterMS)
}

// TestVoiceQualityBurst_ZeroCount: count=0 → error (defensive).
func TestVoiceQualityBurst_ZeroCount(t *testing.T) {
	relay := newFakeUDPRelay(t)
	clientPC, err := net.ListenPacket("udp", "127.0.0.1:0")
	require.NoError(t, err)
	defer clientPC.Close()

	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancel()

	_, err = runVoiceQualityBurst(ctx, clientPC, relay.addr,
		"localhost", 19302, 0, 5*time.Millisecond)
	assert.Error(t, err)
}
