// SPDX-FileCopyrightText: 2022 Free Mobile
// SPDX-License-Identifier: AGPL-3.0-only

package bmp

import (
	"fmt"
	"math/rand/v2"
	"net/netip"
	"testing"
	"unsafe"

	"akvorado/common/helpers"

	"github.com/osrg/gobgp/v3/pkg/packet/bgp"
)

func TestLargeCommunitiesAlign(t *testing.T) {
	largeCommunities := []bgp.LargeCommunity{
		{ASN: 1, LocalData1: 2, LocalData2: 3},
		{ASN: 4, LocalData1: 5, LocalData2: 6},
	}
	first := unsafe.Pointer(&largeCommunities[0])
	second := unsafe.Pointer(&largeCommunities[1])
	diff := uintptr(second) - uintptr(first)
	if diff != 12 {
		t.Errorf("Alignment error for large community slices. Got %d, expected 12",
			diff)
	}

	// Also check other stuff we think are true about "unsafe"
	if unsafe.Sizeof(largeCommunities[0]) != 12 {
		t.Errorf("Large community size: got %d, expected 12", unsafe.Sizeof(largeCommunities[0]))
	}
	const _ = unsafe.Sizeof(largeCommunities[0])
}

func TestRTAEqual(t *testing.T) {
	cases := []struct {
		pos   helpers.Pos
		rta1  routeAttributes
		rta2  routeAttributes
		equal bool
	}{
		{helpers.Mark(), routeAttributes{asn: 2038}, routeAttributes{asn: 2038}, true},
		{helpers.Mark(), routeAttributes{asn: 2038}, routeAttributes{asn: 2039}, false},
		{
			helpers.Mark(),
			routeAttributes{asn: 2038, asPath: []uint32{}},
			routeAttributes{asn: 2038},
			true,
		},
		{
			helpers.Mark(),
			routeAttributes{asn: 2038, asPath: []uint32{}},
			routeAttributes{asn: 2039},
			false,
		},
		{
			helpers.Mark(),
			routeAttributes{asn: 2038, communities: []uint32{}},
			routeAttributes{asn: 2038},
			true,
		},
		{
			helpers.Mark(),
			routeAttributes{asn: 2038, communities: []uint32{}},
			routeAttributes{asn: 2039},
			false,
		},
		{
			helpers.Mark(),
			routeAttributes{asn: 2038, largeCommunities: []bgp.LargeCommunity{}},
			routeAttributes{asn: 2038},
			true,
		},
		{
			helpers.Mark(),
			routeAttributes{asn: 2038, largeCommunities: []bgp.LargeCommunity{}},
			routeAttributes{asn: 2039},
			false,
		},
		{
			helpers.Mark(),
			routeAttributes{asn: 2038, asPath: []uint32{1, 2, 3}},
			routeAttributes{asn: 2038, asPath: []uint32{1, 2, 3}},
			true,
		},
		{
			helpers.Mark(),
			routeAttributes{asn: 2038, asPath: []uint32{1, 2, 3}},
			routeAttributes{asn: 2038, asPath: []uint32{1, 2, 3, 4}},
			false,
		},
		{
			helpers.Mark(),
			routeAttributes{asn: 2038, asPath: []uint32{1, 2, 3}},
			routeAttributes{asn: 2038, asPath: []uint32{1, 2, 3, 0}},
			false,
		},
		{
			helpers.Mark(),
			routeAttributes{asn: 2038, asPath: []uint32{1, 2, 3}},
			routeAttributes{asn: 2038, asPath: []uint32{1, 2, 4}},
			false,
		},
		{
			helpers.Mark(),
			routeAttributes{asn: 2038, asPath: []uint32{1, 2, 3, 4}},
			routeAttributes{asn: 2038, asPath: []uint32{1, 2, 3, 4}},
			true,
		},
		{
			helpers.Mark(),
			routeAttributes{asn: 2038, asPath: []uint32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34}},
			routeAttributes{asn: 2038, asPath: []uint32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34}},
			true,
		},
		{
			helpers.Mark(),
			routeAttributes{asn: 2038, asPath: []uint32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34}},
			routeAttributes{asn: 2038, asPath: []uint32{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 35}},
			false,
		},
		{
			helpers.Mark(),
			routeAttributes{asn: 2038, communities: []uint32{100, 200, 300, 400}},
			routeAttributes{asn: 2038, communities: []uint32{100, 200, 300, 400}},
			true,
		},
		{
			helpers.Mark(),
			routeAttributes{asn: 2038, communities: []uint32{100, 200, 300, 400}},
			routeAttributes{asn: 2038, communities: []uint32{100, 200, 300, 402}},
			false,
		},
		{
			helpers.Mark(),
			routeAttributes{asn: 2038, communities: []uint32{100, 200, 300}},
			routeAttributes{asn: 2038, communities: []uint32{100, 200, 300, 400}},
			false,
		},
		{
			helpers.Mark(),
			routeAttributes{asn: 2038, largeCommunities: []bgp.LargeCommunity{{ASN: 1, LocalData1: 2, LocalData2: 3}, {ASN: 3, LocalData1: 4, LocalData2: 5}, {ASN: 5, LocalData1: 6, LocalData2: 7}}},
			routeAttributes{asn: 2038, largeCommunities: []bgp.LargeCommunity{{ASN: 1, LocalData1: 2, LocalData2: 3}, {ASN: 3, LocalData1: 4, LocalData2: 5}, {ASN: 5, LocalData1: 6, LocalData2: 7}}},
			true,
		},
		{
			helpers.Mark(),
			routeAttributes{asn: 2038, largeCommunities: []bgp.LargeCommunity{{ASN: 1, LocalData1: 2, LocalData2: 3}, {ASN: 3, LocalData1: 4, LocalData2: 5}, {ASN: 5, LocalData1: 6, LocalData2: 7}}},
			routeAttributes{asn: 2038, largeCommunities: []bgp.LargeCommunity{{ASN: 1, LocalData1: 2, LocalData2: 3}, {ASN: 3, LocalData1: 4, LocalData2: 5}, {ASN: 5, LocalData1: 6, LocalData2: 8}}},
			false,
		},
		{
			helpers.Mark(),
			routeAttributes{asn: 2038, largeCommunities: []bgp.LargeCommunity{{ASN: 1, LocalData1: 2, LocalData2: 3}, {ASN: 3, LocalData1: 4, LocalData2: 5}, {ASN: 5, LocalData1: 6, LocalData2: 7}}},
			routeAttributes{asn: 2038, largeCommunities: []bgp.LargeCommunity{{ASN: 1, LocalData1: 2, LocalData2: 4}, {ASN: 3, LocalData1: 4, LocalData2: 5}, {ASN: 5, LocalData1: 6, LocalData2: 7}}},
			false,
		},
		{
			helpers.Mark(),
			routeAttributes{asn: 2038, largeCommunities: []bgp.LargeCommunity{{ASN: 1, LocalData1: 2, LocalData2: 3}, {ASN: 3, LocalData1: 4, LocalData2: 5}}},
			routeAttributes{asn: 2038, largeCommunities: []bgp.LargeCommunity{{ASN: 1, LocalData1: 2, LocalData2: 3}, {ASN: 3, LocalData1: 4, LocalData2: 5}, {ASN: 5, LocalData1: 6, LocalData2: 7}}},
			false,
		},
	}

	for _, tc := range cases {
		equal := tc.rta1.Equal(tc.rta2)
		if equal && !tc.equal {
			t.Errorf("%s%+v == %+v", tc.pos, tc.rta1, tc.rta2)
		} else if !equal && tc.equal {
			t.Errorf("%s%+v != %+v", tc.pos, tc.rta1, tc.rta2)
		} else {
			equal := tc.rta1.Hash() == tc.rta2.Hash()
			if equal && !tc.equal {
				t.Errorf("%s%+v.hash == %+v.hash", tc.pos, tc.rta1, tc.rta2)
			} else if !equal && tc.equal {
				t.Errorf("%s%+v.hash != %+v.hash", tc.pos, tc.rta1, tc.rta2)
			}
		}
	}
}

func TestRemoveRoutes(t *testing.T) {
	nr := func(r *rib, peer uint32) route {
		return route{
			peer:    peer,
			nlri:    r.nlris.Put(nlri{family: bgp.RF_IPv4_UC, path: 1}),
			nextHop: r.nextHops.Put(nextHop(netip.MustParseAddr("::ffff:198.51.100.8"))),
			attributes: r.rtas.Put(routeAttributes{
				asn: 65300,
			}),
			prefixLen: 96 + 24,
		}
	}
	t.Run("only route", func(t *testing.T) {
		r := newRIB()
		r.AddPrefix(netip.MustParsePrefix("::ffff:192.168.144.0/120"), nr(r, 10))
		idx, _ := r.tree.Lookup(netip.MustParseAddr("192.168.144.10"))
		count, empty := r.removeRoutes(idx, func(route) bool { return true }, true)
		if !empty {
			t.Error("removeRoutes() should have removed all routes from node")
		}
		if count != 1 {
			t.Error("removeRoutes() should have removed 1 route")
		}
		if diff := helpers.Diff(r.routes, map[routeKey]route{}); diff != "" {
			t.Errorf("removeRoutes() (-got, +want):\n%s", diff)
		}
	})

	t.Run("first route", func(t *testing.T) {
		r := newRIB()
		r1 := nr(r, 10)
		r2 := nr(r, 11)
		r.AddPrefix(netip.MustParsePrefix("::ffff:192.168.144.0/120"), r1)
		r.AddPrefix(netip.MustParsePrefix("::ffff:192.168.144.0/120"), r2)
		idx, _ := r.tree.Lookup(netip.MustParseAddr("192.168.144.10"))
		count, empty := r.removeRoutes(idx, func(r route) bool { return r.peer == 10 }, true)
		if empty {
			t.Error("removeRoutes() should not have removed all routes from node")
		}
		if count != 1 {
			t.Error("removeRoutes() should have removed 1 route")
		}
		if diff := helpers.Diff(r.routes, map[routeKey]route{
			makeRouteKey(idx, 0): r2,
		}); diff != "" {
			t.Errorf("removeRoutes() (-got, +want):\n%s", diff)
		}
	})

	t.Run("second route", func(t *testing.T) {
		r := newRIB()
		r1 := nr(r, 10)
		r2 := nr(r, 11)
		r.AddPrefix(netip.MustParsePrefix("::ffff:192.168.144.0/120"), r1)
		r.AddPrefix(netip.MustParsePrefix("::ffff:192.168.144.0/120"), r2)
		idx, _ := r.tree.Lookup(netip.MustParseAddr("192.168.144.10"))
		count, empty := r.removeRoutes(idx, func(r route) bool { return r.peer == 11 }, true)
		if empty {
			t.Error("removeRoutes() should not have removed all routes from node")
		}
		if count != 1 {
			t.Error("removeRoutes() should have removed 1 route")
		}
		if diff := helpers.Diff(r.routes, map[routeKey]route{
			makeRouteKey(idx, 0): r1,
		}); diff != "" {
			t.Errorf("removeRoutes() (-got, +want):\n%s", diff)
		}
	})
	t.Run("middle route", func(t *testing.T) {
		r := newRIB()
		r1 := nr(r, 10)
		r2 := nr(r, 11)
		r3 := nr(r, 12)
		r.AddPrefix(netip.MustParsePrefix("::ffff:192.168.144.0/120"), r1)
		r.AddPrefix(netip.MustParsePrefix("::ffff:192.168.144.0/120"), r2)
		r.AddPrefix(netip.MustParsePrefix("::ffff:192.168.144.0/120"), r3)
		idx, _ := r.tree.Lookup(netip.MustParseAddr("192.168.144.10"))
		count, empty := r.removeRoutes(idx, func(r route) bool { return r.peer == 11 }, true)
		if empty {
			t.Error("removeRoutes() should not have removed all routes from node")
		}
		if count != 1 {
			t.Error("removeRoutes() should have removed 1 route")
		}
		if diff := helpers.Diff(r.routes, map[routeKey]route{
			makeRouteKey(idx, 0): r1,
			makeRouteKey(idx, 1): r3,
		}); diff != "" {
			t.Errorf("removeRoutes() (-got, +want):\n%s", diff)
		}
	})
	t.Run("one route out of two", func(t *testing.T) {
		r := newRIB()
		r1 := nr(r, 10)
		r2 := nr(r, 11)
		r3 := nr(r, 12)
		r4 := nr(r, 13)
		r5 := nr(r, 14)
		r.AddPrefix(netip.MustParsePrefix("::ffff:192.168.144.0/120"), r1)
		r.AddPrefix(netip.MustParsePrefix("::ffff:192.168.144.0/120"), r2)
		r.AddPrefix(netip.MustParsePrefix("::ffff:192.168.144.0/120"), r3)
		r.AddPrefix(netip.MustParsePrefix("::ffff:192.168.144.0/120"), r4)
		r.AddPrefix(netip.MustParsePrefix("::ffff:192.168.144.0/120"), r5)
		idx, _ := r.tree.Lookup(netip.MustParseAddr("192.168.144.10"))
		count, empty := r.removeRoutes(idx, func(r route) bool { return r.peer%2 == 0 }, false)
		if empty {
			t.Error("removeRoutes() should not have removed all routes from node")
		}
		if count != 3 {
			t.Error("removeRoutes() should have removed 3 route")
		}
		if diff := helpers.Diff(r.routes, map[routeKey]route{
			makeRouteKey(idx, 0): r2,
			makeRouteKey(idx, 1): r4,
		}); diff != "" {
			t.Errorf("removeRoutes() (-got, +want):\n%s", diff)
		}
	})

	t.Run("all routes", func(t *testing.T) {
		r := newRIB()
		r1 := nr(r, 10)
		r2 := nr(r, 11)
		r3 := nr(r, 12)
		r4 := nr(r, 13)
		r5 := nr(r, 14)
		r.AddPrefix(netip.MustParsePrefix("::ffff:192.168.144.0/120"), r1)
		r.AddPrefix(netip.MustParsePrefix("::ffff:192.168.144.0/120"), r2)
		r.AddPrefix(netip.MustParsePrefix("::ffff:192.168.144.0/120"), r3)
		r.AddPrefix(netip.MustParsePrefix("::ffff:192.168.144.0/120"), r4)
		r.AddPrefix(netip.MustParsePrefix("::ffff:192.168.144.0/120"), r5)
		idx, _ := r.tree.Lookup(netip.MustParseAddr("192.168.144.10"))
		count, empty := r.removeRoutes(idx, func(route) bool { return true }, false)
		if !empty {
			t.Error("removeRoutes() should have removed all routes from node")
		}
		if count != 5 {
			t.Error("removeRoutes() should have removed 5 route")
		}
		if diff := helpers.Diff(r.routes, map[routeKey]route{}); diff != "" {
			t.Errorf("removeRoutes() (-got, +want):\n%s", diff)
		}
	})
}

func TestRIBHarness(t *testing.T) {
	for run := range 5 {
		random := rand.New(rand.NewPCG(uint64(run), 0))
		run++

		// Ramp up the test
		totalExporters := run
		peerPerExporter := 1 + run/2
		maxInitialRoutePerPeer := 500 * run
		maxRemovedRoutePerPeer := 100 * run
		maxReaddedRoutePerPeer := 50 * run
		t.Logf("Run %d. Exporters=%d, Peers=%d, Initial=max %d, Removed=max %d, Readded=max %d",
			run, totalExporters, peerPerExporter,
			maxInitialRoutePerPeer, maxRemovedRoutePerPeer, maxReaddedRoutePerPeer)

		r := newRIB()
		type lookup struct {
			peer    uint32
			addr    netip.Addr
			nextHop netip.Addr
			rd      RD
			asn     uint32
			removed bool
			comment string
		}
		// We store all lookups that should succeed
		lookups := []lookup{}
		removeLookup := func(lookup lookup, comment string) {
			for idx := range lookups {
				if lookups[idx].peer != lookup.peer {
					continue
				}
				if lookups[idx].addr != lookup.addr || lookups[idx].rd != lookup.rd {
					continue
				}
				if lookups[idx].removed {
					continue
				}
				lookups[idx].removed = true
				lookups[idx].comment = fmt.Sprintf("%s; %s", lookups[idx].comment, comment)
				break
			}
		}

		peers := []uint32{}
		for i := range totalExporters {
			for j := range peerPerExporter {
				peer := uint32((i << 16) + int(j))
				peers = append(peers, peer)
				toAdd := random.IntN(maxInitialRoutePerPeer)
				added := 0
				for range toAdd {
					lookup := lookup{
						peer: peer,
						addr: netip.MustParseAddr(fmt.Sprintf("2001:db8:f:%x::",
							random.IntN(300))),
						nextHop: netip.MustParseAddr(
							fmt.Sprintf("2001:db8:c::%x", random.IntN(500))),
						rd:      RD(random.IntN(3)),
						asn:     uint32(random.IntN(1000)),
						comment: "added during first pass",
					}
					added += r.AddPrefix(netip.PrefixFrom(lookup.addr, 64),
						route{
							peer:    peer,
							nlri:    r.nlris.Put(nlri{rd: lookup.rd}),
							nextHop: r.nextHops.Put(nextHop(lookup.nextHop)),
							attributes: r.rtas.Put(routeAttributes{
								asn: lookup.asn,
							}),
						})
					removeLookup(lookup, fmt.Sprintf("erased by NH: %s, ASN: %d", lookup.nextHop, lookup.asn))
					lookups = append(lookups, lookup)
				}
				t.Logf("Run %d: added = %d/%d", run, added, toAdd)

				toRemove := random.IntN(maxRemovedRoutePerPeer)
				removed := 0
				for range toRemove {
					prefix := netip.MustParseAddr(fmt.Sprintf("2001:db8:f:%x::",
						random.IntN(300)))
					rd := RD(random.IntN(4))
					if nlriRef, ok := r.nlris.Ref(nlri{
						rd: rd,
					}); ok {
						removed += r.RemovePrefix(netip.PrefixFrom(prefix, 64),
							route{
								peer: peer,
								nlri: nlriRef,
							})
						removeLookup(lookup{
							peer: peer,
							addr: prefix,
							rd:   rd,
						}, "removed during second pass")
					}
				}
				t.Logf("Run %d: removed = %d/%d", run, removed, toRemove)

				toAdd = random.IntN(maxReaddedRoutePerPeer)
				added = 0
				for range toAdd {
					lookup := lookup{
						peer: peer,
						addr: netip.MustParseAddr(fmt.Sprintf("2001:db8:f:%x::",
							random.IntN(300))),
						nextHop: netip.MustParseAddr(
							fmt.Sprintf("2001:db8:c::%x", random.Uint32()%500)),
						asn:     uint32(random.IntN(1010)),
						comment: "added during third pass",
					}
					added += r.AddPrefix(netip.PrefixFrom(lookup.addr, 64),
						route{
							peer:    peer,
							nlri:    r.nlris.Put(nlri{}),
							nextHop: r.nextHops.Put(nextHop(lookup.nextHop)),
							attributes: r.rtas.Put(routeAttributes{
								asn: lookup.asn,
							}),
						})
					removeLookup(lookup, fmt.Sprintf("erased by NH: %s, ASN: %d", lookup.nextHop, lookup.asn))
					lookups = append(lookups, lookup)
				}
				t.Logf("Run %d: readedd = %d/%d", run, added, toAdd)
			}
		}

		removed := 0
		for _, lookup := range lookups {
			if lookup.removed {
				removed++
				continue
			}
			// Find prefix in tree
			prefixIdx, ok := r.tree.Lookup(lookup.addr)
			if !ok {
				t.Errorf("cannot find %s for %d",
					lookup.addr, lookup.peer)
				continue
			}

			// Check if routes exist for this prefix
			found := false
			routeFound := false

			for route := range r.iterateRoutesForPrefixIndex(prefixIdx) {
				routeFound = true // At least one route exists
				if r.nextHops.Get(route.nextHop) != nextHop(lookup.nextHop) || r.nlris.Get(route.nlri).rd != lookup.rd {
					continue
				}
				if r.rtas.Get(route.attributes).asn != lookup.asn {
					continue
				}
				found = true
				break
			}

			if !routeFound {
				t.Errorf("no routes found for %s for %d",
					lookup.addr, lookup.peer)
				continue
			}

			if !found {
				t.Errorf("cannot find %s for peer %d; NH: %s, RD: %s, ASN: %d, comment: %s",
					lookup.addr, lookup.peer,
					lookup.nextHop, lookup.rd, lookup.asn, lookup.comment)
				t.Logf("> available routes in tree for %s:", lookup.addr)
				for route := range r.iterateRoutesForPrefixIndex(prefixIdx) {
					t.Logf("  peer %d, NH: %s, RD: %s, ASN: %d",
						route.peer,
						netip.Addr(r.nextHops.Get(route.nextHop)),
						r.nlris.Get(route.nlri).rd, r.rtas.Get(route.attributes).asn)
				}
				t.Logf("> route history for prefix %s:", lookup.addr)
				for _, olookup := range lookups {
					if lookup.addr != olookup.addr {
						continue
					}
					t.Logf("  peer: %d, NH: %s, RD: %s, ASN: %d, comment: %s",
						olookup.peer, olookup.nextHop, olookup.rd, olookup.asn, olookup.comment)
				}
				if run == 1 {
					if testing.Verbose() {
						t.Log("> complete history:")
						for _, olookup := range lookups {
							t.Logf("  prefix: %s, peer: %d, NH: %s, RD: %s, ASN: %d comment: %s",
								olookup.addr,
								olookup.peer, olookup.nextHop, olookup.rd, olookup.asn, olookup.comment)
						}
					} else {
						t.Log("> complete history available in verbose mode")
					}
				}
			}
		}
		if removed < 5 {
			t.Error("did not remove more than 5 prefixes, suspicious...")
		}

		// Remove everything
		for _, peer := range peers {
			r.FlushPeer(peer)
		}

		// Check for leak of interned values
		if r.nlris.Len() > 0 {
			t.Errorf("%d NLRIs have leaked", r.nlris.Len())
		}
		if r.nextHops.Len() > 0 {
			t.Errorf("%d next hops have leaked", r.nextHops.Len())
		}
		if r.rtas.Len() > 0 {
			t.Errorf("%d route attributes have leaked", r.rtas.Len())
		}

		if t.Failed() {
			break
		}
	}
}

func BenchmarkRTAHash(b *testing.B) {
	rta := routeAttributes{
		asn:    2038,
		asPath: []uint32{1, 2, 3, 4, 5, 6, 7},
	}
	for b.Loop() {
		rta.Hash()
	}
}

func BenchmarkRTAEqual(b *testing.B) {
	rta := routeAttributes{
		asn:    2038,
		asPath: []uint32{1, 2, 3, 4, 5, 6, 7},
	}
	for b.Loop() {
		rta.Equal(rta)
	}
}
