package nebula

import (
	"bytes"
	"log/slog"
	"net"
	"net/netip"
	"reflect"
	"testing"

	"github.com/slackhq/nebula/cert"
	"github.com/slackhq/nebula/test"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestControl_GetHostInfoByVpnIp(t *testing.T) {
	//TODO: CERT-V2 with multiple certificate versions we have a problem with this test
	// Some certs versions have different characteristics and each version implements their own Copy() func
	// which means this is not a good place to test for exposing memory
	l := test.NewLogger()
	// Special care must be taken to re-use all objects provided to the hostmap and certificate in the expectedInfo object
	// To properly ensure we are not exposing core memory to the caller
	hm := newHostMap(l)
	hm.preferredRanges.Store(&[]netip.Prefix{})

	remote1 := netip.MustParseAddrPort("0.0.0.100:4444")
	remote2 := netip.MustParseAddrPort("[1:2:3:4:5:6:7:8]:4444")

	ipNet := net.IPNet{
		IP:   remote1.Addr().AsSlice(),
		Mask: net.IPMask{255, 255, 255, 0},
	}

	ipNet2 := net.IPNet{
		IP:   remote2.Addr().AsSlice(),
		Mask: net.IPMask{255, 255, 255, 0},
	}

	remotes := NewRemoteList([]netip.Addr{netip.IPv4Unspecified()}, nil)
	remotes.unlockedPrependV4(netip.IPv4Unspecified(), netAddrToProtoV4AddrPort(remote1.Addr(), remote1.Port()))
	remotes.unlockedPrependV6(netip.IPv4Unspecified(), netAddrToProtoV6AddrPort(remote2.Addr(), remote2.Port()))

	vpnIp, ok := netip.AddrFromSlice(ipNet.IP)
	assert.True(t, ok)

	crt := &dummyCert{}
	hi := &HostInfo{
		remotes: remotes,
		ConnectionState: &ConnectionState{
			peerCert: &cert.CachedCertificate{Certificate: crt},
		},
		remoteIndexId: 200,
		localIndexId:  201,
		vpnAddrs:      []netip.Addr{vpnIp},
		relayState: RelayState{
			relays:         nil,
			relayForByAddr: map[netip.Addr]*Relay{},
			relayForByIdx:  map[uint32]*Relay{},
		},
	}
	hi.remote.Store(&remote1)
	hm.unlockedAddHostInfo(hi, &Interface{})

	vpnIp2, ok := netip.AddrFromSlice(ipNet2.IP)
	assert.True(t, ok)

	hi2 := &HostInfo{
		remotes: remotes,
		ConnectionState: &ConnectionState{
			peerCert: nil,
		},
		remoteIndexId: 200,
		localIndexId:  201,
		vpnAddrs:      []netip.Addr{vpnIp2},
		relayState: RelayState{
			relays:         nil,
			relayForByAddr: map[netip.Addr]*Relay{},
			relayForByIdx:  map[uint32]*Relay{},
		},
	}
	hi2.remote.Store(&remote1)
	hm.unlockedAddHostInfo(hi2, &Interface{})

	c := Control{
		state: StateReady,
		f: &Interface{
			hostMap: hm,
		},
		l: test.NewLogger(),
	}

	thi := c.GetHostInfoByVpnAddr(vpnIp, false)

	expectedInfo := ControlHostInfo{
		VpnAddrs:               []netip.Addr{vpnIp},
		LocalIndex:             201,
		RemoteIndex:            200,
		RemoteAddrs:            []netip.AddrPort{remote2, remote1},
		Cert:                   crt.Copy(),
		MessageCounter:         0,
		CurrentRemote:          remote1,
		CurrentRelaysToMe:      []netip.Addr{},
		CurrentRelaysThroughMe: []netip.Addr{},
	}

	// Make sure we don't have any unexpected fields
	assertFields(t, []string{"VpnAddrs", "LocalIndex", "RemoteIndex", "RemoteAddrs", "Cert", "MessageCounter", "CurrentRemote", "CurrentRelaysToMe", "CurrentRelaysThroughMe"}, thi)
	assert.Equal(t, &expectedInfo, thi)
	test.AssertDeepCopyEqual(t, &expectedInfo, thi)

	// Make sure we don't panic if the host info doesn't have a cert yet
	assert.NotPanics(t, func() {
		thi = c.GetHostInfoByVpnAddr(vpnIp2, false)
	})
}

func assertFields(t *testing.T, expected []string, actualStruct any) {
	val := reflect.ValueOf(actualStruct).Elem()
	fields := make([]string, val.NumField())
	for i := 0; i < val.NumField(); i++ {
		fields[i] = val.Type().Field(i).Name
	}

	assert.Equal(t, expected, fields)
}

// alwaysAllowV4/V6 are check funcs that accept every entry (including nil pointers),
// letting us inject a nil *V4AddrPort/*V6AddrPort into a RemoteList's reported cache
// the same way a malformed proto message off the wire could.
func alwaysAllowV4(netip.Addr, *V4AddrPort) bool { return true }
func alwaysAllowV6(netip.Addr, *V6AddrPort) bool { return true }

// TestGetRelays_SkipsNilRelayAddrs proves GetRelays tolerates nil entries in the
// RelayVpnAddrs proto slice (which protoAddrToNetAddr would nil-deref on) and still
// returns the valid relays, including the legacy OldRelayVpnAddrs.
func TestGetRelays_SkipsNilRelayAddrs(t *testing.T) {
	good := netip.MustParseAddr("10.0.0.9")

	d := &NebulaMetaDetails{
		OldRelayVpnAddrs: []uint32{0x0a000001}, // 10.0.0.1
		RelayVpnAddrs: []*Addr{
			nil,
			netAddrToProtoAddr(good),
			nil,
		},
	}

	var relays []netip.Addr
	require.NotPanics(t, func() { relays = d.GetRelays() })

	assert.Equal(t, []netip.Addr{
		netip.MustParseAddr("10.0.0.1"),
		good,
	}, relays)
}

// TestGetRelays_AllNil ensures an all-nil RelayVpnAddrs slice yields no relays and no panic.
func TestGetRelays_AllNil(t *testing.T) {
	d := &NebulaMetaDetails{RelayVpnAddrs: []*Addr{nil, nil}}
	var relays []netip.Addr
	require.NotPanics(t, func() { relays = d.GetRelays() })
	assert.Empty(t, relays)
}

// TestRemoteList_CopyCache_SkipsNilReported proves CopyCache skips nil reported
// pointers (v4 and v6) instead of nil-dereferencing them in protoV*AddrPortToNetAddrPort.
func TestRemoteList_CopyCache_SkipsNilReported(t *testing.T) {
	owner := netip.MustParseAddr("10.0.0.1")
	rl := NewRemoteList([]netip.Addr{owner}, nil)

	rl.unlockedSetV4(owner, owner, []*V4AddrPort{
		nil,
		newIp4AndPortFromString("1.2.3.4:5"),
		nil,
	}, alwaysAllowV4)

	rl.unlockedSetV6(owner, owner, []*V6AddrPort{
		nil,
		newIp6AndPortFromString("[1::1]:6"),
		nil,
	}, alwaysAllowV6)

	var cm *CacheMap
	require.NotPanics(t, func() { cm = rl.CopyCache() })

	c := (*cm)[owner.String()]
	require.NotNil(t, c)
	assert.ElementsMatch(t, []netip.AddrPort{
		netip.MustParseAddrPort("1.2.3.4:5"),
		netip.MustParseAddrPort("[1::1]:6"),
	}, c.Reported)
}

// TestRemoteList_Rebuild_SkipsNilReported drives unlockedCollect (via Rebuild) with
// nil reported entries and confirms only the valid addresses survive, with no panic.
func TestRemoteList_Rebuild_SkipsNilReported(t *testing.T) {
	owner := netip.MustParseAddr("10.0.0.1")
	rl := NewRemoteList([]netip.Addr{owner}, nil)

	rl.unlockedSetV4(owner, owner, []*V4AddrPort{
		nil,
		newIp4AndPortFromString("1.2.3.4:5"),
	}, alwaysAllowV4)
	rl.unlockedSetV6(owner, owner, []*V6AddrPort{
		newIp6AndPortFromString("[1::1]:6"),
		nil,
	}, alwaysAllowV6)

	require.NotPanics(t, func() { rl.Rebuild([]netip.Prefix{}) })

	assert.ElementsMatch(t, []netip.AddrPort{
		netip.MustParseAddrPort("1.2.3.4:5"),
		netip.MustParseAddrPort("[1::1]:6"),
	}, rl.addrs)
}

// newRelayControl marshals a NebulaControl the way it arrives on the wire so we can feed
// it through HandleControlMsg's unmarshal + validate path.
func newRelayControl(t *testing.T, typ NebulaControl_MessageType, from, to *Addr) []byte {
	t.Helper()
	msg := &NebulaControl{
		Type:          typ,
		RelayFromAddr: from,
		RelayToAddr:   to,
	}
	b, err := msg.Marshal()
	require.NoError(t, err)
	return b
}

// TestRelayManager_HandleControlMsg_NilRelayAddrs verifies the validation block added to
// HandleControlMsg: CreateRelay{Request,Response} carrying a nil RelayFromAddr or
// RelayToAddr are dropped with a debug log rather than nil-dereferencing downstream.
func TestRelayManager_HandleControlMsg_NilRelayAddrs(t *testing.T) {
	good := netAddrToProtoAddr(netip.MustParseAddr("10.0.0.9"))

	cases := []struct {
		name    string
		typ     NebulaControl_MessageType
		from    *Addr
		to      *Addr
		wantLog string // debug substring expected, "" == expect no drop log
	}{
		{"request nil from", NebulaControl_CreateRelayRequest, nil, good, "nil RelayFromAddr"},
		{"request nil to", NebulaControl_CreateRelayRequest, good, nil, "nil RelayToAddr"},
		{"request both nil", NebulaControl_CreateRelayRequest, nil, nil, "nil RelayFromAddr"},
		{"response nil from", NebulaControl_CreateRelayResponse, nil, good, "nil RelayFromAddr"},
		{"response nil to", NebulaControl_CreateRelayResponse, good, nil, "nil RelayToAddr"},
		// A non-relay control type is not subject to the relay-addr validation and must
		// pass through it untouched (the final switch simply no-ops on it).
		{"unrelated type nil addrs", NebulaControl_None, nil, nil, ""},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			var buf bytes.Buffer
			l := test.NewLoggerWithOutputAndLevel(&buf, slog.LevelDebug)
			rm := &relayManager{l: l, hostmap: newHostMap(l)}
			rm.useRelays.Store(true)

			f := &Interface{l: l}
			h := &HostInfo{vpnAddrs: []netip.Addr{netip.MustParseAddr("10.0.0.2")}, localIndexId: 1}

			d := newRelayControl(t, tc.typ, tc.from, tc.to)

			require.NotPanics(t, func() { rm.HandleControlMsg(h, d, f) })

			if tc.wantLog == "" {
				assert.NotContains(t, buf.String(), "nil Relay")
			} else {
				assert.Contains(t, buf.String(), tc.wantLog)
			}
		})
	}
}
