package engine

import (
	"encoding/binary"
	"net"
	"testing"

	"github.com/google/gopacket/layers"
)

func TestParseL3IPv6UDP(t *testing.T) {
	src := net.ParseIP("2001:db8::10").To16()
	dst := net.ParseIP("2001:db8::20").To16()
	payload := []byte("hello")

	pkt := buildIPv6UDPPacket(t, src, dst, 12345, 443, payload)
	l3, transport, ok := ParseL3(pkt)
	if !ok {
		t.Fatal("ParseL3 should parse IPv6 packet")
	}
	if l3.Version != 6 {
		t.Fatalf("version=%d want=6", l3.Version)
	}
	if l3.Protocol != 17 {
		t.Fatalf("protocol=%d want=17 (udp)", l3.Protocol)
	}
	if !l3.SrcIPAddr().Equal(src) {
		t.Fatalf("src=%v want=%v", l3.SrcIPAddr(), src)
	}
	if !l3.DstIPAddr().Equal(dst) {
		t.Fatalf("dst=%v want=%v", l3.DstIPAddr(), dst)
	}
	udp, gotPayload, ok := ParseUDP(transport)
	if !ok {
		t.Fatal("ParseUDP should parse transport payload")
	}
	if udp.SrcPort != 12345 || udp.DstPort != 443 {
		t.Fatalf("ports=%d->%d want=12345->443", udp.SrcPort, udp.DstPort)
	}
	if string(gotPayload) != string(payload) {
		t.Fatalf("payload=%q want=%q", string(gotPayload), string(payload))
	}
}

func TestParseL3IPv6HopByHopThenUDP(t *testing.T) {
	src := net.ParseIP("2001:db8::1").To16()
	dst := net.ParseIP("2001:db8::2").To16()
	udpPayload := []byte("abc")
	udpLen := 8 + len(udpPayload)
	totalPayloadLen := 8 + udpLen // 8-byte hop-by-hop extension + udp packet

	pkt := make([]byte, 40+totalPayloadLen)
	pkt[0] = 0x60
	binary.BigEndian.PutUint16(pkt[4:6], uint16(totalPayloadLen))
	pkt[6] = 0  // Hop-by-hop
	pkt[7] = 64 // Hop limit
	copy(pkt[8:24], src)
	copy(pkt[24:40], dst)

	off := 40
	pkt[off+0] = 17 // Next header: UDP
	pkt[off+1] = 0  // Hdr Ext Len: (0+1)*8 = 8 bytes
	off += 8

	binary.BigEndian.PutUint16(pkt[off:off+2], 5353)
	binary.BigEndian.PutUint16(pkt[off+2:off+4], 53)
	binary.BigEndian.PutUint16(pkt[off+4:off+6], uint16(udpLen))
	copy(pkt[off+8:], udpPayload)

	l3, transport, ok := ParseL3(pkt)
	if !ok {
		t.Fatal("ParseL3 should parse IPv6 packet with extension headers")
	}
	if l3.Version != 6 || l3.Protocol != 17 {
		t.Fatalf("version/protocol=%d/%d want=6/17", l3.Version, l3.Protocol)
	}
	udp, gotPayload, ok := ParseUDP(transport)
	if !ok {
		t.Fatal("ParseUDP should parse UDP after hop-by-hop extension")
	}
	if udp.SrcPort != 5353 || udp.DstPort != 53 {
		t.Fatalf("ports=%d->%d want=5353->53", udp.SrcPort, udp.DstPort)
	}
	if string(gotPayload) != string(udpPayload) {
		t.Fatalf("payload=%q want=%q", string(gotPayload), string(udpPayload))
	}
}

func buildIPv6UDPPacket(t *testing.T, src, dst net.IP, srcPort, dstPort uint16, payload []byte) []byte {
	t.Helper()
	udpLen := 8 + len(payload)
	pkt := make([]byte, 40+udpLen)
	pkt[0] = 0x60
	binary.BigEndian.PutUint16(pkt[4:6], uint16(udpLen))
	pkt[6] = 17
	pkt[7] = 64
	copy(pkt[8:24], src.To16())
	copy(pkt[24:40], dst.To16())
	off := 40
	binary.BigEndian.PutUint16(pkt[off:off+2], srcPort)
	binary.BigEndian.PutUint16(pkt[off+2:off+4], dstPort)
	binary.BigEndian.PutUint16(pkt[off+4:off+6], uint16(udpLen))
	copy(pkt[off+8:], payload)
	return pkt
}

func TestParseL3IPv6FragmentNonFirst(t *testing.T) {
	src := net.ParseIP("2001:db8::a").To16()
	dst := net.ParseIP("2001:db8::b").To16()

	// IPv6 header + fragment header (offset != 0)
	pkt := make([]byte, 48)
	pkt[0] = 0x60
	binary.BigEndian.PutUint16(pkt[4:6], 8)
	pkt[6] = 44
	pkt[7] = 64
	copy(pkt[8:24], src)
	copy(pkt[24:40], dst)
	pkt[40] = uint8(layers.IPProtocolUDP)
	// fragment offset in 8-byte units: 1 (non-first fragment)
	binary.BigEndian.PutUint16(pkt[42:44], 1<<3)

	_, _, ok := ParseL3(pkt)
	if ok {
		t.Fatal("ParseL3 should reject non-first IPv6 fragments for L4 parsing")
	}
}
