package ca

import (
	"net"
	"testing"

	"github.com/uopi/goca/proto"
)

// buildOldSearchReply builds a CA_PROTO_SEARCH UDP reply using the old
// payload format (CA < 4.9): [minor_version(2)][pad(2)][IP(4)].
// When ip is nil, the IP field is 0xFFFFFFFF (use-sender convention).
func buildOldSearchReply(searchID uint32, serverIP net.IP, port int) []byte {
	payload := make([]byte, 8)
	payload[0] = 0x00
	payload[1] = proto.MinorVersion // e.g. 0x0D = 13
	// payload[2:4] = 0x00 0x00 (pad)
	if serverIP == nil {
		payload[4] = 0xFF
		payload[5] = 0xFF
		payload[6] = 0xFF
		payload[7] = 0xFF
	} else {
		copy(payload[4:], serverIP.To4())
	}
	h := proto.Header{
		Command:    proto.CmdSearch,
		DataType:   uint16(port),
		Parameter1: proto.MinorVersion,
		Parameter2: searchID,
	}
	return proto.BuildMessage(h, payload)
}

// TestParseReplyOldFormatAllFF verifies the old format with 0xFFFFFFFF IP.
func TestParseReplyOldFormatAllFF(t *testing.T) {
	se := &searchEngine{waiters: make(map[uint32]*searchWaiter)}
	const searchID = uint32(42)
	result := make(chan searchResult, 1)
	se.mu.Lock()
	se.waiters[searchID] = &searchWaiter{pvName: "TEST:PV", ch: result}
	se.mu.Unlock()

	pkt := buildOldSearchReply(searchID, nil, 5064) // nil → 0xFFFFFFFF
	src := &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 5064}
	se.parseReply(pkt, src)

	select {
	case r := <-result:
		if r.addr != "127.0.0.1:5064" {
			t.Errorf("addr = %q, want 127.0.0.1:5064", r.addr)
		}
	default:
		t.Fatal("parseReply did not deliver a result")
	}
}

// TestParseReplyOldFormatAllZero verifies the old format with 0.0.0.0 IP
// (used by some EPICS Base versions to also mean "use sender IP").
func TestParseReplyOldFormatAllZero(t *testing.T) {
	se := &searchEngine{waiters: make(map[uint32]*searchWaiter)}
	const searchID = uint32(43)
	result := make(chan searchResult, 1)
	se.mu.Lock()
	se.waiters[searchID] = &searchWaiter{pvName: "TEST:PV", ch: result}
	se.mu.Unlock()

	pkt := buildOldSearchReply(searchID, net.IPv4(0, 0, 0, 0).To4(), 5064)
	src := &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 5064}
	se.parseReply(pkt, src)

	select {
	case r := <-result:
		if r.addr != "127.0.0.1:5064" {
			t.Errorf("addr = %q, want 127.0.0.1:5064", r.addr)
		}
	default:
		t.Fatal("parseReply did not deliver a result")
	}
}

// TestParseReplyOldFormatExplicitIP verifies that an explicit server IP in
// the old payload format is read from the correct offset (bytes [4:8]).
func TestParseReplyOldFormatExplicitIP(t *testing.T) {
	se := &searchEngine{
		waiters: make(map[uint32]*searchWaiter),
	}

	const searchID = uint32(7)
	result := make(chan searchResult, 1)
	se.mu.Lock()
	se.waiters[searchID] = &searchWaiter{pvName: "TEST:PV2", ch: result}
	se.mu.Unlock()

	explicitIP := net.ParseIP("10.0.0.5").To4()
	pkt := buildOldSearchReply(searchID, explicitIP, 5064)
	src := &net.UDPAddr{IP: net.ParseIP("10.0.0.5"), Port: 5064}

	se.parseReply(pkt, src)

	select {
	case r := <-result:
		if r.addr != "10.0.0.5:5064" {
			t.Errorf("addr = %q, want 10.0.0.5:5064", r.addr)
		}
	default:
		t.Fatal("parseReply did not deliver a result")
	}
}

// TestParseReplyNewFormat verifies the new payload format (IP at [0:4]).
func TestParseReplyNewFormat(t *testing.T) {
	se := &searchEngine{
		waiters: make(map[uint32]*searchWaiter),
	}

	const searchID = uint32(99)
	result := make(chan searchResult, 1)
	se.mu.Lock()
	se.waiters[searchID] = &searchWaiter{pvName: "TEST:PV3", ch: result}
	se.mu.Unlock()

	// New format: IP = 0xFFFFFFFF at [0:4].
	pkt := EncodeSearchReply(searchID, nil, 5064)
	src := &net.UDPAddr{IP: net.ParseIP("192.168.1.10"), Port: 5064}

	se.parseReply(pkt, src)

	select {
	case r := <-result:
		if r.addr != "192.168.1.10:5064" {
			t.Errorf("addr = %q, want 192.168.1.10:5064", r.addr)
		}
	default:
		t.Fatal("parseReply did not deliver a result")
	}
}
