// Copyright (C) 2014 Nippon Telegraph and Telephone Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
// implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package table

import (
	//"fmt"

	"fmt"
	"net/netip"
	"testing"
	"time"

	"github.com/osrg/gobgp/v4/pkg/packet/bgp"

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

func TestDestinationNewIPv4(t *testing.T) {
	peerD := DestCreatePeer()
	pathD := DestCreatePath(peerD)
	ipv4d := newDestination(pathD[0].GetNlri(), 0)
	assert.NotNil(t, ipv4d)
}

func TestDestinationNewIPv6(t *testing.T) {
	peerD := DestCreatePeer()
	pathD := DestCreatePath(peerD)
	ipv6d := newDestination(pathD[0].GetNlri(), 0)
	assert.NotNil(t, ipv6d)
}

func TestDestinationGetNlri(t *testing.T) {
	dd := &destination{}
	nlri, _ := bgp.NewIPAddrPrefix(netip.MustParsePrefix("10.110.123.1/24"))
	dd.nlri = nlri
	r_nlri := dd.GetNlri()
	assert.Equal(t, r_nlri, nlri)

	nlri2, _ := bgp.NewIPAddrPrefix(netip.MustParsePrefix("10.110.123.2/24"))
	dd2 := newDestination(nlri2, 0)
	r_nlri2 := dd2.GetNlri()
	assert.Equal(t, r_nlri2, nlri2)
}

func TestCalculate2(t *testing.T) {
	origin := bgp.NewPathAttributeOrigin(0)
	aspathParam := []bgp.AsPathParamInterface{bgp.NewAs4PathParam(2, []uint32{65001})}
	aspath := bgp.NewPathAttributeAsPath(aspathParam)
	nexthop, _ := bgp.NewPathAttributeNextHop(netip.MustParseAddr("10.0.0.1"))
	med := bgp.NewPathAttributeMultiExitDisc(0)
	pathAttributes := []bgp.PathAttributeInterface{origin, aspath, nexthop, med}
	nlri, _ := bgp.NewIPAddrPrefix(netip.MustParsePrefix("10.10.0.0/24"))

	// peer1 sends normal update message 10.10.0.0/24
	update1 := bgp.NewBGPUpdateMessage(nil, pathAttributes, []bgp.PathNLRI{{NLRI: nlri}})
	peer1 := &PeerInfo{AS: 1, Address: netip.MustParseAddr("1.1.1.1")}
	path1 := ProcessMessage(update1, peer1, time.Now(), false)[0]

	d := newDestination(nlri, 0)
	d.Calculate(logger, path1)

	// suppose peer2 sends grammaatically correct but semantically flawed update message
	// which has a withdrawal nlri not advertised before
	update2 := bgp.NewBGPUpdateMessage([]bgp.PathNLRI{{NLRI: nlri}}, pathAttributes, nil)
	peer2 := &PeerInfo{AS: 2, Address: netip.MustParseAddr("2.2.2.2")}
	path2 := ProcessMessage(update2, peer2, time.Now(), false)[0]
	assert.Equal(t, path2.IsWithdraw, true)

	d.Calculate(logger, path2)

	// we have a path from peer1 here
	assert.Equal(t, len(d.knownPathList), 1)

	// after that, new update with the same nlri comes from peer2
	update3 := bgp.NewBGPUpdateMessage(nil, pathAttributes, []bgp.PathNLRI{{NLRI: nlri}})
	path3 := ProcessMessage(update3, peer2, time.Now(), false)[0]
	assert.Equal(t, path3.IsWithdraw, false)

	d.Calculate(logger, path3)

	// this time, we have paths from peer1 and peer2
	assert.Equal(t, len(d.knownPathList), 2)

	// now peer3 sends normal update message 10.10.0.0/24
	peer3 := &PeerInfo{AS: 3, Address: netip.MustParseAddr("3.3.3.3")}
	update4 := bgp.NewBGPUpdateMessage(nil, pathAttributes, []bgp.PathNLRI{{NLRI: nlri}})
	path4 := ProcessMessage(update4, peer3, time.Now(), false)[0]

	d.Calculate(logger, path4)

	// we must have paths from peer1, peer2 and peer3
	assert.Equal(t, len(d.knownPathList), 3)
}

func TestNeighAddrTieBreak(t *testing.T) {
	nlri, _ := bgp.NewIPAddrPrefix(netip.MustParsePrefix("10.10.0.0/24"))

	peer0 := &PeerInfo{AS: 65001, LocalAS: 1, Address: netip.MustParseAddr("2.2.2.2"), ID: netip.MustParseAddr("2.2.2.2")}

	p0 := func() *Path {
		aspath := bgp.NewPathAttributeAsPath([]bgp.AsPathParamInterface{bgp.NewAs4PathParam(bgp.BGP_ASPATH_ATTR_TYPE_SEQ, []uint32{65001})})
		attrs := []bgp.PathAttributeInterface{aspath, bgp.NewPathAttributeMultiExitDisc(0)}
		return NewPath(bgp.RF_IPv4_UC, peer0, bgp.PathNLRI{NLRI: nlri}, false, attrs, time.Now(), false)
	}()

	peer1 := &PeerInfo{AS: 65001, LocalAS: 1, Address: netip.MustParseAddr("3.3.3.3"), ID: netip.MustParseAddr("2.2.2.2")} // same ID as peer0, separate eBGP session

	p1 := func() *Path {
		aspath := bgp.NewPathAttributeAsPath([]bgp.AsPathParamInterface{bgp.NewAs4PathParam(bgp.BGP_ASPATH_ATTR_TYPE_SEQ, []uint32{65001})})
		attrs := []bgp.PathAttributeInterface{aspath, bgp.NewPathAttributeMultiExitDisc(0)}
		return NewPath(bgp.RF_IPv4_UC, peer1, bgp.PathNLRI{NLRI: nlri}, false, attrs, time.Now(), false)
	}()

	assert.Equal(t, compareByNeighborAddress(p0, p1), p0)
}

func TestMedTieBreaker(t *testing.T) {
	nlri, _ := bgp.NewIPAddrPrefix(netip.MustParsePrefix("10.10.0.0/24"))

	p0 := func() *Path {
		aspath := bgp.NewPathAttributeAsPath([]bgp.AsPathParamInterface{bgp.NewAs4PathParam(bgp.BGP_ASPATH_ATTR_TYPE_SEQ, []uint32{65001, 65002}), bgp.NewAs4PathParam(bgp.BGP_ASPATH_ATTR_TYPE_SEQ, []uint32{65003, 65004})})
		attrs := []bgp.PathAttributeInterface{aspath, bgp.NewPathAttributeMultiExitDisc(0)}
		return NewPath(bgp.RF_IPv4_UC, nil, bgp.PathNLRI{NLRI: nlri}, false, attrs, time.Now(), false)
	}()

	p1 := func() *Path {
		aspath := bgp.NewPathAttributeAsPath([]bgp.AsPathParamInterface{bgp.NewAs4PathParam(bgp.BGP_ASPATH_ATTR_TYPE_SEQ, []uint32{65001, 65002}), bgp.NewAs4PathParam(bgp.BGP_ASPATH_ATTR_TYPE_SEQ, []uint32{65003, 65005})})
		attrs := []bgp.PathAttributeInterface{aspath, bgp.NewPathAttributeMultiExitDisc(10)}
		return NewPath(bgp.RF_IPv4_UC, nil, bgp.PathNLRI{NLRI: nlri}, false, attrs, time.Now(), false)
	}()

	// same AS
	assert.Equal(t, compareByMED(p0, p1), p0)

	p2 := func() *Path {
		aspath := bgp.NewPathAttributeAsPath([]bgp.AsPathParamInterface{bgp.NewAs4PathParam(bgp.BGP_ASPATH_ATTR_TYPE_SEQ, []uint32{65003})})
		attrs := []bgp.PathAttributeInterface{aspath, bgp.NewPathAttributeMultiExitDisc(10)}
		return NewPath(bgp.RF_IPv4_UC, nil, bgp.PathNLRI{NLRI: nlri}, false, attrs, time.Now(), false)
	}()

	// different AS
	assert.Equal(t, compareByMED(p0, p2), (*Path)(nil))

	p3 := func() *Path {
		aspath := bgp.NewPathAttributeAsPath([]bgp.AsPathParamInterface{bgp.NewAs4PathParam(bgp.BGP_ASPATH_ATTR_TYPE_CONFED_SEQ, []uint32{65003, 65004}), bgp.NewAs4PathParam(bgp.BGP_ASPATH_ATTR_TYPE_SEQ, []uint32{65001, 65003})})
		attrs := []bgp.PathAttributeInterface{aspath, bgp.NewPathAttributeMultiExitDisc(0)}
		return NewPath(bgp.RF_IPv4_UC, nil, bgp.PathNLRI{NLRI: nlri}, false, attrs, time.Now(), false)
	}()

	p4 := func() *Path {
		aspath := bgp.NewPathAttributeAsPath([]bgp.AsPathParamInterface{bgp.NewAs4PathParam(bgp.BGP_ASPATH_ATTR_TYPE_SEQ, []uint32{65001, 65002}), bgp.NewAs4PathParam(bgp.BGP_ASPATH_ATTR_TYPE_CONFED_SEQ, []uint32{65005, 65006})})
		attrs := []bgp.PathAttributeInterface{aspath, bgp.NewPathAttributeMultiExitDisc(10)}
		return NewPath(bgp.RF_IPv4_UC, nil, bgp.PathNLRI{NLRI: nlri}, false, attrs, time.Now(), false)
	}()

	// ignore confed
	assert.Equal(t, compareByMED(p3, p4), p3)

	p5 := func() *Path {
		attrs := []bgp.PathAttributeInterface{bgp.NewPathAttributeMultiExitDisc(0)}
		return NewPath(bgp.RF_IPv4_UC, nil, bgp.PathNLRI{NLRI: nlri}, false, attrs, time.Now(), false)
	}()

	p6 := func() *Path {
		attrs := []bgp.PathAttributeInterface{bgp.NewPathAttributeMultiExitDisc(10)}
		return NewPath(bgp.RF_IPv4_UC, nil, bgp.PathNLRI{NLRI: nlri}, false, attrs, time.Now(), false)
	}()

	// no aspath
	assert.Equal(t, compareByMED(p5, p6), p5)
}

func TestTimeTieBreaker(t *testing.T) {
	origin := bgp.NewPathAttributeOrigin(0)
	aspathParam := []bgp.AsPathParamInterface{bgp.NewAs4PathParam(2, []uint32{65001})}
	aspath := bgp.NewPathAttributeAsPath(aspathParam)
	nexthop, _ := bgp.NewPathAttributeNextHop(netip.MustParseAddr("10.0.0.1"))
	med := bgp.NewPathAttributeMultiExitDisc(0)
	pathAttributes := []bgp.PathAttributeInterface{origin, aspath, nexthop, med}
	nlri, _ := bgp.NewIPAddrPrefix(netip.MustParsePrefix("10.10.0.0/24"))
	updateMsg := bgp.NewBGPUpdateMessage(nil, pathAttributes, []bgp.PathNLRI{{NLRI: nlri}})
	peer1 := &PeerInfo{AS: 2, LocalAS: 1, Address: netip.MustParseAddr("1.1.1.1"), ID: netip.MustParseAddr("1.1.1.1")}
	path1 := ProcessMessage(updateMsg, peer1, time.Now(), false)[0]

	peer2 := &PeerInfo{AS: 2, LocalAS: 1, Address: netip.MustParseAddr("2.2.2.2"), ID: netip.MustParseAddr("2.2.2.2")} // weaker router-id
	path2 := ProcessMessage(updateMsg, peer2, time.Now().Add(-1*time.Hour), false)[0]                                  // older than path1

	d := newDestination(nlri, 0)
	d.Calculate(logger, path1)
	d.Calculate(logger, path2)

	assert.Equal(t, len(d.knownPathList), 2)
	assert.Equal(t, true, d.GetBestPath("", 0).GetSource().ID == netip.MustParseAddr("2.2.2.2")) // path from peer2 win

	// this option disables tie breaking by age
	SelectionOptions.ExternalCompareRouterId = true
	d = newDestination(nlri, 0)
	d.Calculate(logger, path1)
	d.Calculate(logger, path2)

	assert.Equal(t, len(d.knownPathList), 2)
	assert.Equal(t, true, d.GetBestPath("", 0).GetSource().ID == netip.MustParseAddr("1.1.1.1")) // path from peer1 win
}

func DestCreatePeer() []*PeerInfo {
	peerD1 := &PeerInfo{AS: 65000}
	peerD2 := &PeerInfo{AS: 65001}
	peerD3 := &PeerInfo{AS: 65002}
	peerD := []*PeerInfo{peerD1, peerD2, peerD3}
	return peerD
}

func DestCreatePath(peerD []*PeerInfo) []*Path {
	bgpMsgD1 := updateMsgD1()
	bgpMsgD2 := updateMsgD2()
	bgpMsgD3 := updateMsgD3()
	pathD := make([]*Path, 3)
	for i, msg := range []*bgp.BGPMessage{bgpMsgD1, bgpMsgD2, bgpMsgD3} {
		updateMsgD := msg.Body.(*bgp.BGPUpdate)
		nlriList := updateMsgD.NLRI
		pathAttributes := updateMsgD.PathAttributes
		nlri_info := nlriList[0]
		pathD[i] = NewPath(bgp.RF_IPv4_UC, peerD[i], bgp.PathNLRI{NLRI: nlri_info.NLRI}, false, pathAttributes, time.Now(), false)
	}
	return pathD
}

func updateMsgD1() *bgp.BGPMessage {
	origin := bgp.NewPathAttributeOrigin(0)
	aspathParam := []bgp.AsPathParamInterface{bgp.NewAsPathParam(2, []uint16{65000})}
	aspath := bgp.NewPathAttributeAsPath(aspathParam)
	nexthop, _ := bgp.NewPathAttributeNextHop(netip.MustParseAddr("192.168.50.1"))
	med := bgp.NewPathAttributeMultiExitDisc(0)

	pathAttributes := []bgp.PathAttributeInterface{
		origin,
		aspath,
		nexthop,
		med,
	}
	nlri, _ := bgp.NewIPAddrPrefix(netip.MustParsePrefix("10.10.10.0/24"))
	updateMsg := bgp.NewBGPUpdateMessage(nil, pathAttributes, []bgp.PathNLRI{{NLRI: nlri}})
	UpdatePathAttrs4ByteAs(logger, updateMsg.Body.(*bgp.BGPUpdate))
	return updateMsg
}

func updateMsgD2() *bgp.BGPMessage {
	origin := bgp.NewPathAttributeOrigin(0)
	aspathParam := []bgp.AsPathParamInterface{bgp.NewAsPathParam(2, []uint16{65100})}
	aspath := bgp.NewPathAttributeAsPath(aspathParam)
	nexthop, _ := bgp.NewPathAttributeNextHop(netip.MustParseAddr("192.168.100.1"))
	med := bgp.NewPathAttributeMultiExitDisc(100)

	pathAttributes := []bgp.PathAttributeInterface{
		origin,
		aspath,
		nexthop,
		med,
	}

	nlri, _ := bgp.NewIPAddrPrefix(netip.MustParsePrefix("20.20.20.0/24"))
	updateMsg := bgp.NewBGPUpdateMessage(nil, pathAttributes, []bgp.PathNLRI{{NLRI: nlri}})
	UpdatePathAttrs4ByteAs(logger, updateMsg.Body.(*bgp.BGPUpdate))
	return updateMsg
}

func updateMsgD3() *bgp.BGPMessage {
	origin := bgp.NewPathAttributeOrigin(0)
	aspathParam := []bgp.AsPathParamInterface{bgp.NewAsPathParam(2, []uint16{65100})}
	aspath := bgp.NewPathAttributeAsPath(aspathParam)
	nexthop, _ := bgp.NewPathAttributeNextHop(netip.MustParseAddr("192.168.150.1"))
	med := bgp.NewPathAttributeMultiExitDisc(100)

	pathAttributes := []bgp.PathAttributeInterface{
		origin,
		aspath,
		nexthop,
		med,
	}

	nlri, _ := bgp.NewIPAddrPrefix(netip.MustParsePrefix("30.30.30.0/24"))
	w1, _ := bgp.NewIPAddrPrefix(netip.MustParsePrefix("40.40.40.0/23"))
	withdrawnRoutes := []bgp.PathNLRI{{NLRI: w1}}
	updateMsg := bgp.NewBGPUpdateMessage(withdrawnRoutes, pathAttributes, []bgp.PathNLRI{{NLRI: nlri}})
	UpdatePathAttrs4ByteAs(logger, updateMsg.Body.(*bgp.BGPUpdate))
	return updateMsg
}

func TestMultipath(t *testing.T) {
	UseMultiplePaths.Enabled = true
	origin := bgp.NewPathAttributeOrigin(0)
	aspathParam := []bgp.AsPathParamInterface{bgp.NewAs4PathParam(2, []uint32{65000})}
	aspath := bgp.NewPathAttributeAsPath(aspathParam)
	nexthop, _ := bgp.NewPathAttributeNextHop(netip.MustParseAddr("192.168.150.1"))
	med := bgp.NewPathAttributeMultiExitDisc(100)

	pathAttributes := []bgp.PathAttributeInterface{
		origin,
		aspath,
		nexthop,
		med,
	}

	nlri, _ := bgp.NewIPAddrPrefix(netip.MustParsePrefix("10.10.10.0/24"))
	updateMsg := bgp.NewBGPUpdateMessage(nil, pathAttributes, []bgp.PathNLRI{{NLRI: nlri}})
	peer1 := &PeerInfo{AS: 1, Address: netip.MustParseAddr("1.1.1.1"), ID: netip.MustParseAddr("1.1.1.1")}
	path1 := ProcessMessage(updateMsg, peer1, time.Now(), false)[0]
	peer2 := &PeerInfo{AS: 2, Address: netip.MustParseAddr("2.2.2.2"), ID: netip.MustParseAddr("2.2.2.2")}

	med = bgp.NewPathAttributeMultiExitDisc(100)
	nexthop, _ = bgp.NewPathAttributeNextHop(netip.MustParseAddr("192.168.150.2"))
	pathAttributes = []bgp.PathAttributeInterface{
		origin,
		aspath,
		nexthop,
		med,
	}
	updateMsg = bgp.NewBGPUpdateMessage(nil, pathAttributes, []bgp.PathNLRI{{NLRI: nlri}})
	path2 := ProcessMessage(updateMsg, peer2, time.Now(), false)[0]

	d := newDestination(nlri, 0)
	d.Calculate(logger, path2)

	dd, _ := d.Calculate(logger, path1)
	best, old, multi := dd.GetChanges(GLOBAL_RIB_NAME, 0, false)
	assert.NotNil(t, best)
	assert.Equal(t, old, path2)
	assert.Equal(t, len(multi), 2)
	assert.Equal(t, len(d.GetKnownPathList(GLOBAL_RIB_NAME, 0)), 2)

	path3 := path2.Clone(true)
	dd, _ = d.Calculate(logger, path3)
	best, old, multi = dd.GetChanges(GLOBAL_RIB_NAME, 0, false)
	assert.Nil(t, best)
	assert.Equal(t, old, path1)
	assert.Equal(t, len(multi), 1)
	assert.Equal(t, len(d.GetKnownPathList(GLOBAL_RIB_NAME, 0)), 1)

	peer3 := &PeerInfo{AS: 3, Address: netip.MustParseAddr("3.3.3.3"), ID: netip.MustParseAddr("3.3.3.3")}
	med = bgp.NewPathAttributeMultiExitDisc(50)
	nexthop, _ = bgp.NewPathAttributeNextHop(netip.MustParseAddr("192.168.150.3"))
	pathAttributes = []bgp.PathAttributeInterface{
		origin,
		aspath,
		nexthop,
		med,
	}
	updateMsg = bgp.NewBGPUpdateMessage(nil, pathAttributes, []bgp.PathNLRI{{NLRI: nlri}})
	path4 := ProcessMessage(updateMsg, peer3, time.Now(), false)[0]
	dd, _ = d.Calculate(logger, path4)
	best, _, multi = dd.GetChanges(GLOBAL_RIB_NAME, 0, false)
	assert.NotNil(t, best)
	assert.Equal(t, len(multi), 1)
	assert.Equal(t, len(d.GetKnownPathList(GLOBAL_RIB_NAME, 0)), 2)

	nexthop, _ = bgp.NewPathAttributeNextHop(netip.MustParseAddr("192.168.150.2"))
	pathAttributes = []bgp.PathAttributeInterface{
		origin,
		aspath,
		nexthop,
		med,
	}
	updateMsg = bgp.NewBGPUpdateMessage(nil, pathAttributes, []bgp.PathNLRI{{NLRI: nlri}})
	path5 := ProcessMessage(updateMsg, peer2, time.Now(), false)[0]
	dd, _ = d.Calculate(logger, path5)
	best, _, multi = dd.GetChanges(GLOBAL_RIB_NAME, 0, false)
	assert.NotNil(t, best)
	assert.Equal(t, len(multi), 2)
	assert.Equal(t, len(d.GetKnownPathList(GLOBAL_RIB_NAME, 0)), 3)

	UseMultiplePaths.Enabled = false
}

func TestIdMap(t *testing.T) {
	nlri, _ := bgp.NewIPAddrPrefix(netip.MustParsePrefix("10.10.0.101/24"))
	d := newDestination(nlri, 64)
	for i := 0; ; i++ {
		if id, err := d.localIdMap.FindandSetZeroBit(); err == nil {
			assert.Equal(t, uint(i+1), id)
		} else {
			assert.Equal(t, i, 63)
			break
		}
	}
	d.localIdMap.Expand()
	for i := range 64 {
		id, _ := d.localIdMap.FindandSetZeroBit()
		assert.Equal(t, id, uint(64+i))
	}
	_, err := d.localIdMap.FindandSetZeroBit()
	assert.NotNil(t, err)
}

func TestGetWithdrawnPath(t *testing.T) {
	attrs := []bgp.PathAttributeInterface{
		bgp.NewPathAttributeOrigin(0),
	}
	nlri, _ := bgp.NewIPAddrPrefix(netip.MustParsePrefix("13.2.3.0/24"))
	p1 := NewPath(bgp.RF_IPv4_UC, nil, bgp.PathNLRI{NLRI: nlri}, false, attrs, time.Now(), false)
	nlri, _ = bgp.NewIPAddrPrefix(netip.MustParsePrefix("13.2.4.0/24"))
	p2 := NewPath(bgp.RF_IPv4_UC, nil, bgp.PathNLRI{NLRI: nlri}, false, attrs, time.Now(), false)
	nlri, _ = bgp.NewIPAddrPrefix(netip.MustParsePrefix("13.2.5.0/24"))
	p3 := NewPath(bgp.RF_IPv4_UC, nil, bgp.PathNLRI{NLRI: nlri}, false, attrs, time.Now(), false)

	u := &Update{
		KnownPathList:    []*Path{p2},
		OldKnownPathList: []*Path{p1, p2, p3},
	}

	l := u.GetWithdrawnPath()
	assert.Equal(t, len(l), 2)
	assert.Equal(t, l[0].GetNlri(), p1.GetNlri())
}

func TestDestination_Calculate_ExplicitWithdraw(t *testing.T) {
	attrs := []bgp.PathAttributeInterface{
		bgp.NewPathAttributeOrigin(0),
	}

	nlri, _ := bgp.NewIPAddrPrefix(netip.MustParsePrefix("10.0.0.0/24"))

	peer1 := &PeerInfo{AS: 65001, Address: netip.MustParseAddr("1.1.1.1")}
	peer2 := &PeerInfo{AS: 65002, Address: netip.MustParseAddr("2.2.2.2")}

	// Create initial paths
	p1 := NewPath(bgp.RF_IPv4_UC, peer1, bgp.PathNLRI{NLRI: nlri}, false, attrs, time.Now(), false)
	p2 := NewPath(bgp.RF_IPv4_UC, peer2, bgp.PathNLRI{NLRI: nlri}, false, attrs, time.Now(), false)

	d := newDestination(nlri, 1, p1, p2)

	// Test explicit withdraw
	withdrawPath := NewPath(bgp.RF_IPv4_UC, peer1, bgp.PathNLRI{NLRI: nlri}, true, attrs, time.Now(), false)
	dd, oldPath := d.Calculate(logger, withdrawPath)
	update := dd

	assert.Same(t, p1, oldPath)
	assert.Len(t, update.KnownPathList, 1)
	assert.Equal(t, peer2.Address.String(), update.KnownPathList[0].GetSource().Address.String())

	_, oldPath = d.Calculate(logger, withdrawPath)
	assert.Nil(t, oldPath)
}

func TestDestination_Calculate_ImplicitWithdraw(t *testing.T) {
	attrs := []bgp.PathAttributeInterface{
		bgp.NewPathAttributeOrigin(0),
	}

	nlri, _ := bgp.NewIPAddrPrefix(netip.MustParsePrefix("10.0.0.0/24"))
	peer1 := &PeerInfo{AS: 65001, Address: netip.MustParseAddr("1.1.1.1")}

	p1 := NewPath(bgp.RF_IPv4_UC, peer1, bgp.PathNLRI{NLRI: nlri}, false, attrs, time.Now(), false)
	d := newDestination(nlri, 0)
	_, oldPath := d.Calculate(logger, p1)
	assert.Nil(t, oldPath)

	// Send new path from same peer (should trigger implicit withdraw)
	newAttrs := []bgp.PathAttributeInterface{
		bgp.NewPathAttributeOrigin(0),
		bgp.NewPathAttributeMultiExitDisc(100),
	}
	p2 := NewPath(bgp.RF_IPv4_UC, peer1, bgp.PathNLRI{NLRI: nlri}, false, newAttrs, time.Now(), false)
	dd, oldPath := d.Calculate(logger, p2)
	update := dd

	assert.Same(t, p1, oldPath)
	assert.Len(t, update.KnownPathList, 1)
	assert.Equal(t, uint32(100), update.KnownPathList[0].getPathAttr(bgp.BGP_ATTR_TYPE_MULTI_EXIT_DISC).(*bgp.PathAttributeMultiExitDisc).Value)
}

func TestDestination_GetBestPath_InvalidNexthop(t *testing.T) {
	attrs := []bgp.PathAttributeInterface{
		bgp.NewPathAttributeOrigin(0),
	}

	nlri, _ := bgp.NewIPAddrPrefix(netip.MustParsePrefix("10.0.0.0/24"))
	peer1 := &PeerInfo{AS: 65001, Address: netip.MustParseAddr("1.1.1.1")}

	p1 := NewPath(bgp.RF_IPv4_UC, peer1, bgp.PathNLRI{NLRI: nlri}, false, attrs, time.Now(), false)

	d := newDestination(nlri, 0, p1)

	p1.IsNexthopInvalid = false
	bestPath := d.GetBestPath("", 0)
	assert.Equal(t, p1, bestPath)

	p1.IsNexthopInvalid = true
	bestPath = d.GetBestPath("", 0)
	assert.Nil(t, bestPath)
}

func TestDestination_Select_BestAndMultiPath(t *testing.T) {
	attrs := []bgp.PathAttributeInterface{
		bgp.NewPathAttributeOrigin(0),
	}

	nlri, _ := bgp.NewIPAddrPrefix(netip.MustParsePrefix("10.0.0.0/24"))
	peer1 := &PeerInfo{AS: 65001, Address: netip.MustParseAddr("1.1.1.1")}
	peer2 := &PeerInfo{AS: 65002, Address: netip.MustParseAddr("2.2.2.2")}

	p1 := NewPath(bgp.RF_IPv4_UC, peer1, bgp.PathNLRI{NLRI: nlri}, false, attrs, time.Now(), false)
	p2 := NewPath(bgp.RF_IPv4_UC, peer2, bgp.PathNLRI{NLRI: nlri}, false, attrs, time.Now(), false)

	d := newDestination(nlri, 0, p1, p2)

	// Test best path selection
	selected := d.Select(DestinationSelectOption{Best: true})
	assert.NotNil(t, selected)
	assert.Len(t, selected.GetAllKnownPathList(), 1)

	// Test multipath selection
	selected = d.Select(DestinationSelectOption{Best: true, MultiPath: true})
	assert.NotNil(t, selected)
	assert.Len(t, selected.GetAllKnownPathList(), 2)
}

func TestCompareByLLGRStaleCommunity(t *testing.T) {
	attrs := []bgp.PathAttributeInterface{
		bgp.NewPathAttributeOrigin(0),
	}

	nlri, _ := bgp.NewIPAddrPrefix(netip.MustParsePrefix("10.0.0.0/24"))
	peer1 := &PeerInfo{AS: 65001, Address: netip.MustParseAddr("1.1.1.1")}
	peer2 := &PeerInfo{AS: 65002, Address: netip.MustParseAddr("2.2.2.2")}

	p1 := NewPath(bgp.RF_IPv4_UC, peer1, bgp.PathNLRI{NLRI: nlri}, false, attrs, time.Now(), false)
	p2 := NewPath(bgp.RF_IPv4_UC, peer2, bgp.PathNLRI{NLRI: nlri}, false, attrs, time.Now(), false)

	// Mock LLGR stale state
	p1.SetCommunities([]uint32{uint32(bgp.COMMUNITY_LLGR_STALE)}, false)

	result := compareByLLGRStaleCommunity(p1, p2)
	assert.Equal(t, p2, result)
	result = compareByLLGRStaleCommunity(p2, p1)
	assert.Equal(t, p2, result)

	// Both stale
	p2.SetCommunities([]uint32{uint32(bgp.COMMUNITY_LLGR_STALE)}, false)
	result = compareByLLGRStaleCommunity(p1, p2)
	assert.Nil(t, result)
}

func TestCompareByLocalOrigin(t *testing.T) {
	attrs := []bgp.PathAttributeInterface{
		bgp.NewPathAttributeOrigin(0),
	}

	nlri, _ := bgp.NewIPAddrPrefix(netip.MustParsePrefix("10.0.0.0/24"))
	peer1 := &PeerInfo{AS: 65001, Address: netip.MustParseAddr("1.1.1.1")}

	// Local path (peer = nil)
	localPath := NewPath(bgp.RF_IPv4_UC, nil, bgp.PathNLRI{NLRI: nlri}, false, attrs, time.Now(), false)
	peerPath := NewPath(bgp.RF_IPv4_UC, peer1, bgp.PathNLRI{NLRI: nlri}, false, attrs, time.Now(), false)

	result := compareByLocalOrigin(localPath, peerPath)
	assert.Equal(t, localPath, result)

	result = compareByLocalOrigin(peerPath, localPath)
	assert.Equal(t, localPath, result)

	// Same source
	result = compareByLocalOrigin(peerPath, peerPath)
	assert.Nil(t, result)
}

func TestCompareByASPath_IgnoreLength(t *testing.T) {
	oldIgnoreAsPathLength := SelectionOptions.IgnoreAsPathLength
	defer func() {
		SelectionOptions.IgnoreAsPathLength = oldIgnoreAsPathLength
	}()

	nlri, _ := bgp.NewIPAddrPrefix(netip.MustParsePrefix("10.0.0.0/24"))

	aspath1 := bgp.NewPathAttributeAsPath([]bgp.AsPathParamInterface{
		bgp.NewAs4PathParam(bgp.BGP_ASPATH_ATTR_TYPE_SEQ, []uint32{65001}),
	})
	attrs1 := []bgp.PathAttributeInterface{aspath1}
	p1 := NewPath(bgp.RF_IPv4_UC, nil, bgp.PathNLRI{NLRI: nlri}, false, attrs1, time.Now(), false)

	aspath2 := bgp.NewPathAttributeAsPath([]bgp.AsPathParamInterface{
		bgp.NewAs4PathParam(bgp.BGP_ASPATH_ATTR_TYPE_SEQ, []uint32{65001, 65002}),
	})
	attrs2 := []bgp.PathAttributeInterface{aspath2}
	p2 := NewPath(bgp.RF_IPv4_UC, nil, bgp.PathNLRI{NLRI: nlri}, false, attrs2, time.Now(), false)

	SelectionOptions.IgnoreAsPathLength = false
	result := compareByASPath(p1, p2)
	assert.Equal(t, result, p1)
	result = compareByASPath(p2, p1)
	assert.Equal(t, result, p1)

	SelectionOptions.IgnoreAsPathLength = true
	result = compareByASPath(p1, p2)
	assert.Nil(t, result)
}

func TestCompareByMED_AlwaysCompare(t *testing.T) {
	oldAlwaysCompareMed := SelectionOptions.AlwaysCompareMed
	defer func() {
		SelectionOptions.AlwaysCompareMed = oldAlwaysCompareMed
	}()
	SelectionOptions.AlwaysCompareMed = true

	nlri, _ := bgp.NewIPAddrPrefix(netip.MustParsePrefix("10.0.0.0/24"))

	aspath1 := bgp.NewPathAttributeAsPath([]bgp.AsPathParamInterface{
		bgp.NewAs4PathParam(bgp.BGP_ASPATH_ATTR_TYPE_SEQ, []uint32{65001}),
	})
	attrs1 := []bgp.PathAttributeInterface{aspath1, bgp.NewPathAttributeMultiExitDisc(50)}
	p1 := NewPath(bgp.RF_IPv4_UC, nil, bgp.PathNLRI{NLRI: nlri}, false, attrs1, time.Now(), false)

	aspath2 := bgp.NewPathAttributeAsPath([]bgp.AsPathParamInterface{
		bgp.NewAs4PathParam(bgp.BGP_ASPATH_ATTR_TYPE_SEQ, []uint32{65002}),
	})
	attrs2 := []bgp.PathAttributeInterface{aspath2, bgp.NewPathAttributeMultiExitDisc(100)}
	p2 := NewPath(bgp.RF_IPv4_UC, nil, bgp.PathNLRI{NLRI: nlri}, false, attrs2, time.Now(), false)

	SelectionOptions.AlwaysCompareMed = false
	result := compareByMED(p1, p2)
	assert.Nil(t, result)

	SelectionOptions.AlwaysCompareMed = true
	result = compareByMED(p1, p2)
	assert.Equal(t, p1, result)
}

func BenchmarkMultiPath(b *testing.B) {
	b.StopTimer()
	nlri, _ := bgp.NewIPAddrPrefix(netip.MustParsePrefix("10.10.0.0/24"))

	// Create a 4 path setup for the given NLRI
	origin := bgp.NewPathAttributeOrigin(0)
	aspathParam := []bgp.AsPathParamInterface{bgp.NewAs4PathParam(2, []uint32{65001})}
	aspath := bgp.NewPathAttributeAsPath(aspathParam)
	nexthop, _ := bgp.NewPathAttributeNextHop(netip.MustParseAddr("10.0.0.1"))
	med := bgp.NewPathAttributeMultiExitDisc(0)
	pathAttributes := []bgp.PathAttributeInterface{origin, aspath, nexthop, med}

	numPaths := 4
	pathList := make([]*Path, numPaths)
	for i := range numPaths {
		// peer1 sends normal update message 10.10.0.0/24
		update := bgp.NewBGPUpdateMessage(nil, pathAttributes, []bgp.PathNLRI{{NLRI: nlri}})
		peeri := &PeerInfo{AS: uint32(i), ID: netip.MustParseAddr(fmt.Sprintf("%d.%d.%d.%d", byte(i), byte(i), byte(i), byte(i)))}
		pathList[i] = ProcessMessage(update, peeri, time.Now(), false)[0]
	}

	b.Run("Benchmark Calculate", func(b *testing.B) {
		for range b.N {
			d := newDestination(nlri, 0)
			b.StartTimer()
			for j := range pathList {
				d.Calculate(logger, pathList[j])
			}
			b.StopTimer()
		}
	})

	b.Run("Benchmark GetMultiBestPath", func(b *testing.B) {
		d := newDestination(nlri, 0)
		for j := range pathList {
			d.Calculate(logger, pathList[j])
		}
		for range b.N {
			b.StartTimer()
			d.GetMultiBestPath(GLOBAL_RIB_NAME)
			b.StopTimer()
		}
	})
}

func TestDestination_Calculate_AddAndWithdrawPath(t *testing.T) {
	attrs := []bgp.PathAttributeInterface{
		bgp.NewPathAttributeOrigin(0),
	}
	nlri, _ := bgp.NewIPAddrPrefix(netip.MustParsePrefix("13.2.3.0/24"))
	p1 := NewPath(bgp.RF_IPv4_UC, nil, bgp.PathNLRI{NLRI: nlri}, false, attrs, time.Now(), false)
	nlri, _ = bgp.NewIPAddrPrefix(netip.MustParsePrefix("13.2.4.0/24"))
	p2 := NewPath(bgp.RF_IPv4_UC, nil, bgp.PathNLRI{NLRI: nlri}, false, attrs, time.Now(), false)
	nlri, _ = bgp.NewIPAddrPrefix(netip.MustParsePrefix("13.2.5.0/24"))
	p3 := NewPath(bgp.RF_IPv4_UC, nil, bgp.PathNLRI{NLRI: nlri}, false, attrs, time.Now(), false)
	d := newDestination(nlri, 0, p1, p2, p3)

	nlri, _ = bgp.NewIPAddrPrefix(netip.MustParsePrefix("13.2.6.0/24"))
	p4 := NewPath(bgp.RF_IPv4_UC, nil, bgp.PathNLRI{NLRI: nlri}, false, attrs, time.Now(), false)
	dd, _ := d.Calculate(logger, p4)
	update := dd
	assert.Len(t, update.KnownPathList, 3)
	assert.Len(t, update.KnownPathList, 3)
	assert.NotEqualValues(t, update.OldKnownPathList, update.KnownPathList)
	assert.Equal(t, "13.2.6.0/24", update.KnownPathList[0].GetNlri().String())
	assert.Equal(t, "13.2.4.0/24", update.KnownPathList[1].GetNlri().String())

	// p1 is no implecit withdrawn
	nlri, _ = bgp.NewIPAddrPrefix(netip.MustParsePrefix("13.2.3.0/24"))
	p1 = NewPath(bgp.RF_IPv4_UC, nil, bgp.PathNLRI{NLRI: nlri}, false, attrs, time.Now(), true)
	d = newDestination(nlri, 0, p1, p2, p3)
	update, _ = d.Calculate(logger, p4)
	assert.Len(t, update.KnownPathList, 3)
	assert.Len(t, update.KnownPathList, 3)
	assert.NotEqualValues(t, update.OldKnownPathList, update.KnownPathList)

	assert.Equal(t, "13.2.6.0/24", update.KnownPathList[0].GetNlri().String())
	assert.Equal(t, "13.2.3.0/24", update.KnownPathList[1].GetNlri().String())
	assert.Equal(t, "13.2.5.0/24", update.KnownPathList[2].GetNlri().String())

	nlri, _ = bgp.NewIPAddrPrefix(netip.MustParsePrefix("13.2.8.0/24"))
	p5 := NewPath(bgp.RF_IPv4_UC, nil, bgp.PathNLRI{NLRI: nlri}, false, attrs, time.Now(), false)
	d = newDestination(nlri, 0, p1, p2, p3, p5)
	update, _ = d.Calculate(logger, p4)

	assert.Len(t, update.KnownPathList, 4)
	assert.Len(t, update.KnownPathList, 4)
	assert.NotEqualValues(t, update.OldKnownPathList, update.KnownPathList)
	assert.Equal(t, "13.2.6.0/24", update.KnownPathList[0].GetNlri().String())
	assert.Equal(t, "13.2.3.0/24", update.KnownPathList[1].GetNlri().String())
	assert.Equal(t, "13.2.5.0/24", update.KnownPathList[2].GetNlri().String())
	assert.Equal(t, "13.2.8.0/24", update.KnownPathList[3].GetNlri().String())
}

// TestNHT_InvalidateNewPathWithoutMED reproduces the zebra-nht flaky failure:
// a locally-added path (no MED) whose nexthop is already unreachable must
// produce a withdrawal when the nexthop-invalidated clone is fed back through
// Calculate/GetChanges.
func TestNHT_InvalidateNewPathWithoutMED(t *testing.T) {
	nlri, _ := bgp.NewIPAddrPrefix(netip.MustParsePrefix("10.3.2.0/24"))

	// Step 1: local path added via CLI — no MED, no source (nil → localSource)
	attrs := []bgp.PathAttributeInterface{
		bgp.NewPathAttributeOrigin(bgp.BGP_ORIGIN_ATTR_TYPE_INCOMPLETE),
	}
	nh, _ := bgp.NewPathAttributeNextHop(netip.MustParseAddr("10.3.1.1"))
	attrs = append(attrs, nh)

	original := NewPath(bgp.RF_IPv4_UC, nil, bgp.PathNLRI{NLRI: nlri}, false, attrs, time.Now(), false)

	// Step 2: path is added to destination (first time — no old entry)
	d := &destination{nlri: nlri, localIdMap: NewBitmap(64)}
	d.localIdMap.Flag(0)
	update1, _ := d.Calculate(logger, original)

	best1, old1, _ := update1.GetChanges(GLOBAL_RIB_NAME, 0, false)
	assert.NotNil(t, best1, "new path should become best")
	assert.Nil(t, old1, "no old path expected")
	assert.False(t, best1.IsWithdraw, "new path should not be a withdrawal")
	assert.False(t, best1.IsNexthopInvalid, "new path should be valid")

	// Step 3: clone with IsNexthopInvalid=true (what applyToPathList does)
	invalidClone := original.Clone(false)
	invalidClone.IsNexthopInvalid = true

	// Step 4: feed the invalidated clone into Calculate (what updatePath does)
	update2, _ := d.Calculate(logger, invalidClone)

	best2, old2, _ := update2.GetChanges(GLOBAL_RIB_NAME, 0, false)
	assert.NotNil(t, best2, "invalidation should produce a change")
	assert.NotNil(t, old2, "old path should exist")
	assert.True(t, best2.IsWithdraw, "invalidated path must produce a withdrawal")
}

// TestNHT_InvalidateExistingPathWithMED tests the case where a path that
// already has a MED (from a previous NHT metric) becomes unreachable.
func TestNHT_InvalidateExistingPathWithMED(t *testing.T) {
	nlri, _ := bgp.NewIPAddrPrefix(netip.MustParsePrefix("10.3.1.0/24"))

	attrs := []bgp.PathAttributeInterface{
		bgp.NewPathAttributeOrigin(bgp.BGP_ORIGIN_ATTR_TYPE_INCOMPLETE),
		bgp.NewPathAttributeMultiExitDisc(20),
	}
	nh, _ := bgp.NewPathAttributeNextHop(netip.MustParseAddr("10.3.1.1"))
	attrs = append(attrs, nh)

	original := NewPath(bgp.RF_IPv4_UC, nil, bgp.PathNLRI{NLRI: nlri}, false, attrs, time.Now(), false)

	d := &destination{nlri: nlri, localIdMap: NewBitmap(64)}
	d.localIdMap.Flag(0)
	d.Calculate(logger, original)

	// Invalidate
	invalidClone := original.Clone(false)
	invalidClone.IsNexthopInvalid = true

	update, _ := d.Calculate(logger, invalidClone)
	best, old, _ := update.GetChanges(GLOBAL_RIB_NAME, 0, false)

	assert.NotNil(t, best, "invalidation should produce a change")
	assert.NotNil(t, old, "old path should exist")
	assert.True(t, best.IsWithdraw, "invalidated path must produce a withdrawal")
}

// TestNHT_RevalidatePath tests that a path that was invalid becomes valid
// again when the nexthop is restored.
func TestNHT_RevalidatePath(t *testing.T) {
	nlri, _ := bgp.NewIPAddrPrefix(netip.MustParsePrefix("10.3.1.0/24"))

	attrs := []bgp.PathAttributeInterface{
		bgp.NewPathAttributeOrigin(bgp.BGP_ORIGIN_ATTR_TYPE_INCOMPLETE),
		bgp.NewPathAttributeMultiExitDisc(20),
	}
	nh, _ := bgp.NewPathAttributeNextHop(netip.MustParseAddr("10.3.1.1"))
	attrs = append(attrs, nh)

	original := NewPath(bgp.RF_IPv4_UC, nil, bgp.PathNLRI{NLRI: nlri}, false, attrs, time.Now(), false)

	d := &destination{nlri: nlri, localIdMap: NewBitmap(64)}
	d.localIdMap.Flag(0)
	d.Calculate(logger, original)

	// Invalidate
	invalidClone := original.Clone(false)
	invalidClone.IsNexthopInvalid = true
	d.Calculate(logger, invalidClone)

	// Revalidate with new MED
	validClone := invalidClone.Clone(false)
	validClone.IsNexthopInvalid = false
	err := validClone.SetMed(30, true)
	assert.NoError(t, err)

	update, _ := d.Calculate(logger, validClone)
	best, _, _ := update.GetChanges(GLOBAL_RIB_NAME, 0, false)

	assert.NotNil(t, best, "revalidation should produce a change")
	assert.False(t, best.IsWithdraw, "revalidated path should not be a withdrawal")
	assert.False(t, best.IsNexthopInvalid, "revalidated path should be valid")
	med, err := best.GetMed()
	assert.NoError(t, err)
	assert.Equal(t, uint32(30), med)
}

// TestNHT_NewPathWithInvalidNexthop tests that a newly added path with
// IsNexthopInvalid=true is not treated as the best path. This covers the
// scenario where a path is added via API while its nexthop is already
// unreachable in the nexthop cache.
func TestNHT_NewPathWithInvalidNexthop(t *testing.T) {
	nlri, _ := bgp.NewIPAddrPrefix(netip.MustParsePrefix("10.3.2.0/24"))

	attrs := []bgp.PathAttributeInterface{
		bgp.NewPathAttributeOrigin(bgp.BGP_ORIGIN_ATTR_TYPE_INCOMPLETE),
	}
	nh, _ := bgp.NewPathAttributeNextHop(netip.MustParseAddr("10.3.1.1"))
	attrs = append(attrs, nh)

	// First-time path that is already nexthop-invalid.
	newPath := NewPath(bgp.RF_IPv4_UC, nil, bgp.PathNLRI{NLRI: nlri}, false, attrs, time.Now(), false)
	newPath.IsNexthopInvalid = true

	d := &destination{nlri: nlri, localIdMap: NewBitmap(64)}
	d.localIdMap.Flag(0)

	update, _ := d.Calculate(logger, newPath)
	best, old, _ := update.GetChanges(GLOBAL_RIB_NAME, 0, false)

	// No old path, new path is invalid -> no change should be emitted.
	assert.Nil(t, best, "invalid new path must not be advertised as best")
	assert.Nil(t, old, "no old path expected")
}

func makeDeltaTestPath(t *testing.T, prefix, nexthop, src string, srcAS uint32, remoteID uint32) *Path {
	t.Helper()

	nlri, err := bgp.NewIPAddrPrefix(netip.MustParsePrefix(prefix))
	assert.NoError(t, err)
	nh, err := bgp.NewPathAttributeNextHop(netip.MustParseAddr(nexthop))
	assert.NoError(t, err)
	attrs := []bgp.PathAttributeInterface{
		bgp.NewPathAttributeOrigin(0),
		bgp.NewPathAttributeAsPath([]bgp.AsPathParamInterface{
			bgp.NewAsPathParam(2, []uint16{uint16(srcAS)}),
		}),
		nh,
	}
	source := &PeerInfo{
		AS:      srcAS,
		ID:      netip.MustParseAddr(src),
		Address: netip.MustParseAddr(src),
	}
	return NewPath(
		bgp.RF_IPv4_UC,
		source,
		bgp.PathNLRI{NLRI: nlri, ID: remoteID},
		false,
		attrs,
		time.Unix(100, 0),
		false,
	)
}

func TestUpdateGetMultiBestPathDiff(t *testing.T) {
	oldA := makeDeltaTestPath(t, "10.10.0.0/24", "192.0.2.1", "198.51.100.1", 65001, 1)
	oldB := makeDeltaTestPath(t, "10.10.0.0/24", "192.0.2.2", "198.51.100.2", 65002, 2)
	newC := makeDeltaTestPath(t, "10.10.0.0/24", "192.0.2.3", "198.51.100.3", 65003, 3)

	u := &Update{
		OldKnownPathList: []*Path{oldA, oldB},
		KnownPathList:    []*Path{oldB, newC},
	}

	update, withdraw := u.GetMultiBestPathDiff(GLOBAL_RIB_NAME)
	assert.Len(t, update, 1)
	assert.Len(t, withdraw, 1)
	assert.Equal(t, newC.GetNexthop(), update[0].GetNexthop())
	assert.Equal(t, oldA.GetNexthop(), withdraw[0].GetNexthop())
	assert.True(t, withdraw[0].IsWithdraw)
}

// TestGetChanges_NonKeyNlriOrNexthopOnlyChange reproduces update suppression
// for a locally re-added path whose route key is unchanged but whose NLRI
// payload (MUP TEID) or MP_REACH nexthop is: such a change must be
// re-advertised, even though the attributes hash (which excludes
// MP_REACH_NLRI) stays the same.
func TestGetChanges_NonKeyNlriOrNexthopOnlyChange(t *testing.T) {
	newPath := func(teid, nexthop netip.Addr) *Path {
		p := mupT1stPath(t, teid, nexthop)
		p.SetHash(attrsHashLikeEagerSites(p))
		return p
	}

	p1 := newPath(netip.MustParseAddr("0.0.0.100"), netip.MustParseAddr("10.0.0.1"))
	d := &destination{nlri: p1.GetNlri(), localIdMap: NewBitmap(64)}
	d.localIdMap.Flag(0)

	u1, _ := d.Calculate(logger, p1)
	best1, _, _ := u1.GetChanges(GLOBAL_RIB_NAME, 0, false)
	assert.NotNil(t, best1, "initial add should be advertised")

	// same route key and nexthop, TEID changed
	p2 := newPath(netip.MustParseAddr("0.0.0.200"), netip.MustParseAddr("10.0.0.1"))
	assert.Equal(t, p1.GetHash(), p2.GetHash())
	u2, _ := d.Calculate(logger, p2)
	best2, _, _ := u2.GetChanges(GLOBAL_RIB_NAME, 0, false)
	assert.NotNil(t, best2, "TEID-only change should be advertised")

	// same route key and TEID, nexthop changed
	p3 := newPath(netip.MustParseAddr("0.0.0.200"), netip.MustParseAddr("10.0.0.2"))
	u3, _ := d.Calculate(logger, p3)
	best3, _, _ := u3.GetChanges(GLOBAL_RIB_NAME, 0, false)
	assert.NotNil(t, best3, "nexthop-only change should be advertised")

	// unchanged re-add stays suppressed
	p4 := newPath(netip.MustParseAddr("0.0.0.200"), netip.MustParseAddr("10.0.0.2"))
	u4, _ := d.Calculate(logger, p4)
	best4, _, _ := u4.GetChanges(GLOBAL_RIB_NAME, 0, false)
	assert.Nil(t, best4, "identical re-add should stay suppressed")
}
