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

package metadata

import (
	"errors"
	"fmt"
	"io/fs"
	"math"
	"math/rand/v2"
	"net/netip"
	"path/filepath"
	"slices"
	"strconv"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	"akvorado/common/helpers"
	"akvorado/common/reporter"
	"akvorado/outlet/metadata/provider"
)

func setupTestCache(t *testing.T) (*reporter.Reporter, *metadataCache) {
	t.Helper()
	r := reporter.NewMock(t)
	sc := newMetadataCache(r)
	return r, sc
}

func expectCacheLookup(t *testing.T, sc *metadataCache, exporterIP string, ifIndex uint, expected provider.Answer) {
	t.Helper()
	ip := netip.MustParseAddr(exporterIP)
	ip = helpers.AddrTo6(ip)
	got, ok := sc.Lookup(time.Time{}, provider.Query{
		ExporterIP: ip,
		IfIndex:    ifIndex,
	})
	if ok && (got == provider.Answer{}) {
		t.Error("Lookup() returned an empty result")
	} else if !ok && (got != provider.Answer{}) {
		t.Error("Lookup() returned a non-empty result")
	}
	if diff := helpers.Diff(got, expected); diff != "" {
		t.Errorf("Lookup() (-got, +want):\n%s", diff)
	}
}

func TestGetEmpty(t *testing.T) {
	r, sc := setupTestCache(t)
	expectCacheLookup(t, sc, "127.0.0.1", 676, provider.Answer{})

	gotMetrics := r.GetMetrics("akvorado_outlet_metadata_cache_")
	expectedMetrics := map[string]string{
		`expired_entries_total`: "0",
		`hits_total`:            "0",
		`misses_total`:          "1",
		`size_entries`:          "0",
	}
	if diff := helpers.Diff(gotMetrics, expectedMetrics); diff != "" {
		t.Fatalf("Metrics (-got, +want):\n%s", diff)
	}
}

func TestSimpleLookup(t *testing.T) {
	r, sc := setupTestCache(t)
	sc.Put(time.Now(),
		provider.Query{
			ExporterIP: netip.MustParseAddr("::ffff:127.0.0.1"),
			IfIndex:    676,
		},
		provider.Answer{
			Exporter:  provider.Exporter{Name: "localhost"},
			Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "Transit", Speed: 1000},
		})
	expectCacheLookup(t, sc, "127.0.0.1", 676, provider.Answer{
		Exporter:  provider.Exporter{Name: "localhost"},
		Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "Transit", Speed: 1000},
	})
	expectCacheLookup(t, sc, "127.0.0.1", 787, provider.Answer{})
	expectCacheLookup(t, sc, "127.0.0.2", 676, provider.Answer{})

	gotMetrics := r.GetMetrics("akvorado_outlet_metadata_cache_")
	expectedMetrics := map[string]string{
		`expired_entries_total`: "0",
		`hits_total`:            "1",
		`misses_total`:          "2",
		`size_entries`:          "1",
	}
	if diff := helpers.Diff(gotMetrics, expectedMetrics); diff != "" {
		t.Fatalf("Metrics (-got, +want):\n%s", diff)
	}
}

func TestExpire(t *testing.T) {
	r, sc := setupTestCache(t)
	now := time.Now()
	sc.Put(now,
		provider.Query{
			ExporterIP: netip.MustParseAddr("::ffff:127.0.0.1"),
			IfIndex:    676,
		},
		provider.Answer{
			Exporter:  provider.Exporter{Name: "localhost"},
			Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "Transit"},
		})
	now = now.Add(10 * time.Minute)
	sc.Put(now,
		provider.Query{
			ExporterIP: netip.MustParseAddr("::ffff:127.0.0.1"),
			IfIndex:    678,
		},
		provider.Answer{
			Exporter:  provider.Exporter{Name: "localhost2"},
			Interface: provider.Interface{Name: "Gi0/0/0/2", Description: "Peering"},
		})
	now = now.Add(10 * time.Minute)
	sc.Put(now,
		provider.Query{
			ExporterIP: netip.MustParseAddr("::ffff:127.0.0.2"),
			IfIndex:    678,
		},
		provider.Answer{
			Exporter:  provider.Exporter{Name: "localhost3"},
			Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "IX"},
		})
	now = now.Add(10 * time.Minute)
	sc.Expire(now.Add(-time.Hour))
	expectCacheLookup(t, sc, "127.0.0.1", 676, provider.Answer{
		Exporter:  provider.Exporter{Name: "localhost"},
		Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "Transit"},
	})
	expectCacheLookup(t, sc, "127.0.0.1", 678, provider.Answer{
		Exporter:  provider.Exporter{Name: "localhost2"},
		Interface: provider.Interface{Name: "Gi0/0/0/2", Description: "Peering"},
	})
	expectCacheLookup(t, sc, "127.0.0.2", 678, provider.Answer{
		Exporter:  provider.Exporter{Name: "localhost3"},
		Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "IX"},
	})
	sc.Expire(now.Add(-29 * time.Minute))
	expectCacheLookup(t, sc, "127.0.0.1", 676, provider.Answer{})
	expectCacheLookup(t, sc, "127.0.0.1", 678, provider.Answer{
		Exporter:  provider.Exporter{Name: "localhost2"},
		Interface: provider.Interface{Name: "Gi0/0/0/2", Description: "Peering"},
	})
	expectCacheLookup(t, sc, "127.0.0.2", 678, provider.Answer{
		Exporter:  provider.Exporter{Name: "localhost3"},
		Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "IX"},
	})
	sc.Expire(now.Add(-19 * time.Minute))
	expectCacheLookup(t, sc, "127.0.0.1", 676, provider.Answer{})
	expectCacheLookup(t, sc, "127.0.0.1", 678, provider.Answer{})
	expectCacheLookup(t, sc, "127.0.0.2", 678, provider.Answer{
		Exporter:  provider.Exporter{Name: "localhost3"},
		Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "IX"},
	})
	sc.Expire(now.Add(-9 * time.Minute))
	expectCacheLookup(t, sc, "127.0.0.1", 676, provider.Answer{})
	expectCacheLookup(t, sc, "127.0.0.1", 678, provider.Answer{})
	expectCacheLookup(t, sc, "127.0.0.2", 678, provider.Answer{})
	sc.Put(now,
		provider.Query{
			ExporterIP: netip.MustParseAddr("::ffff:127.0.0.1"),
			IfIndex:    676,
		},
		provider.Answer{
			Exporter:  provider.Exporter{Name: "localhost"},
			Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "Transit"},
		})
	now = now.Add(10 * time.Minute)
	sc.Expire(now.Add(-19 * time.Minute))
	expectCacheLookup(t, sc, "127.0.0.1", 676, provider.Answer{
		Exporter:  provider.Exporter{Name: "localhost"},
		Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "Transit"},
	})

	gotMetrics := r.GetMetrics("akvorado_outlet_metadata_cache_")
	expectedMetrics := map[string]string{
		`expired_entries_total`: "3",
		`hits_total`:            "7",
		`misses_total`:          "6",
		`size_entries`:          "1",
	}
	if diff := helpers.Diff(gotMetrics, expectedMetrics); diff != "" {
		t.Fatalf("Metrics (-got, +want):\n%s", diff)
	}
}

func TestExpireRefresh(t *testing.T) {
	_, sc := setupTestCache(t)
	now := time.Now()
	sc.Put(now,
		provider.Query{
			ExporterIP: netip.MustParseAddr("::ffff:127.0.0.1"),
			IfIndex:    676,
		},
		provider.Answer{
			Exporter:  provider.Exporter{Name: "localhost"},
			Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "Transit"},
		})
	now = now.Add(10 * time.Minute)
	sc.Put(now,
		provider.Query{
			ExporterIP: netip.MustParseAddr("::ffff:127.0.0.1"),
			IfIndex:    678,
		},
		provider.Answer{
			Exporter:  provider.Exporter{Name: "localhost"},
			Interface: provider.Interface{Name: "Gi0/0/0/2", Description: "Peering"},
		})
	now = now.Add(10 * time.Minute)
	sc.Put(now,
		provider.Query{
			ExporterIP: netip.MustParseAddr("::ffff:127.0.0.2"),
			IfIndex:    678,
		},
		provider.Answer{
			Exporter:  provider.Exporter{Name: "localhost2"},
			Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "IX"},
		})
	now = now.Add(10 * time.Minute)

	// Refresh first entry
	sc.Lookup(now, provider.Query{
		ExporterIP: netip.MustParseAddr("::ffff:127.0.0.1"),
		IfIndex:    676,
	})
	now = now.Add(10 * time.Minute)

	sc.Expire(now.Add(-29 * time.Minute))
	expectCacheLookup(t, sc, "127.0.0.1", 676, provider.Answer{
		Exporter:  provider.Exporter{Name: "localhost"},
		Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "Transit"},
	})
	expectCacheLookup(t, sc, "127.0.0.1", 678, provider.Answer{})
	expectCacheLookup(t, sc, "127.0.0.2", 678, provider.Answer{
		Exporter:  provider.Exporter{Name: "localhost2"},
		Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "IX"},
	})
}

func TestNeedUpdates(t *testing.T) {
	_, sc := setupTestCache(t)
	now := time.Now()
	sc.Put(now,
		provider.Query{
			ExporterIP: netip.MustParseAddr("::ffff:127.0.0.1"),
			IfIndex:    676,
		},
		provider.Answer{
			Exporter:  provider.Exporter{Name: "localhost"},
			Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "Transit"},
		})
	now = now.Add(10 * time.Minute)
	sc.Put(now,
		provider.Query{
			ExporterIP: netip.MustParseAddr("::ffff:127.0.0.1"),
			IfIndex:    678,
		},
		provider.Answer{
			Exporter:  provider.Exporter{Name: "localhost"},
			Interface: provider.Interface{Name: "Gi0/0/0/2", Description: "Peering"},
		})
	now = now.Add(10 * time.Minute)
	sc.Put(now,
		provider.Query{
			ExporterIP: netip.MustParseAddr("::ffff:127.0.0.2"),
			IfIndex:    678,
		},
		provider.Answer{
			Exporter:  provider.Exporter{Name: "localhost2"},
			Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "IX"},
		})
	now = now.Add(10 * time.Minute)
	// Refresh
	sc.Put(now,
		provider.Query{
			ExporterIP: netip.MustParseAddr("::ffff:127.0.0.1"),
			IfIndex:    676,
		},
		provider.Answer{
			Exporter:  provider.Exporter{Name: "localhost1"},
			Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "Transit"},
		})
	now = now.Add(10 * time.Minute)

	cases := []struct {
		Minutes  time.Duration
		Expected map[netip.Addr][]uint
	}{
		{9, map[netip.Addr][]uint{
			netip.MustParseAddr("::ffff:127.0.0.1"): {
				676,
				678,
			},
			netip.MustParseAddr("::ffff:127.0.0.2"): {
				678,
			},
		}},
		{19, map[netip.Addr][]uint{
			netip.MustParseAddr("::ffff:127.0.0.1"): {
				678,
			},
			netip.MustParseAddr("::ffff:127.0.0.2"): {
				678,
			},
		}},
		{29, map[netip.Addr][]uint{
			netip.MustParseAddr("::ffff:127.0.0.1"): {
				678,
			},
		}},
		{39, map[netip.Addr][]uint{}},
	}
	for _, tc := range cases {
		t.Run(fmt.Sprintf("%d minutes", tc.Minutes), func(t *testing.T) {
			got := sc.NeedUpdates(now.Add(-tc.Minutes * time.Minute))
			// sort output for comparison purposes
			for _, v := range got {
				slices.Sort(v)
			}
			if diff := helpers.Diff(got, tc.Expected); diff != "" {
				t.Fatalf("WouldExpire(%d minutes) (-got, +want):\n%s", tc.Minutes, diff)
			}
		})
	}
}

func TestLoadNotExist(t *testing.T) {
	_, sc := setupTestCache(t)
	err := sc.Load("/i/do/not/exist")
	if !errors.Is(err, fs.ErrNotExist) {
		t.Fatalf("sc.Load() error:\n%s", err)
	}
}

func TestSaveLoad(t *testing.T) {
	_, sc := setupTestCache(t)
	now := time.Now()
	sc.Put(now,
		provider.Query{
			ExporterIP: netip.MustParseAddr("::ffff:127.0.0.1"),
			IfIndex:    676,
		},
		provider.Answer{
			Exporter:  provider.Exporter{Name: "localhost"},
			Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "Transit"},
		})
	now = now.Add(10 * time.Minute)
	sc.Put(now,
		provider.Query{
			ExporterIP: netip.MustParseAddr("::ffff:127.0.0.1"),
			IfIndex:    678,
		},
		provider.Answer{
			Exporter:  provider.Exporter{Name: "localhost"},
			Interface: provider.Interface{Name: "Gi0/0/0/2", Description: "Peering"},
		})
	now = now.Add(10 * time.Minute)
	sc.Put(now,
		provider.Query{
			ExporterIP: netip.MustParseAddr("::ffff:127.0.0.2"),
			IfIndex:    678,
		},
		provider.Answer{
			Exporter:  provider.Exporter{Name: "localhost2"},
			Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "IX", Speed: 1000},
		})

	target := filepath.Join(t.TempDir(), "cache")
	if err := sc.Save(target); err != nil {
		t.Fatalf("sc.Save() error:\n%s", err)
	}

	_, sc = setupTestCache(t)
	now = now.Add(10 * time.Minute)
	if err := sc.Load(target); err != nil {
		t.Fatalf("sc.Load() error:\n%s", err)
	}

	sc.Expire(now.Add(-29 * time.Minute))
	expectCacheLookup(t, sc, "127.0.0.1", 676, provider.Answer{})
	expectCacheLookup(t, sc, "127.0.0.1", 678, provider.Answer{
		Exporter:  provider.Exporter{Name: "localhost"},
		Interface: provider.Interface{Name: "Gi0/0/0/2", Description: "Peering"},
	})
	expectCacheLookup(t, sc, "127.0.0.2", 678, provider.Answer{
		Exporter:  provider.Exporter{Name: "localhost2"},
		Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "IX", Speed: 1000},
	})
}

func TestConcurrentOperations(t *testing.T) {
	r, sc := setupTestCache(t)
	now := time.Now()
	done := make(chan bool)
	var wg sync.WaitGroup
	var nowLock sync.RWMutex

	// Make the clock go forward
	wg.Go(func() {
		for {
			nowLock.Lock()
			now = now.Add(1 * time.Minute)
			nowLock.Unlock()
			select {
			case <-done:
				return
			case <-time.After(1 * time.Millisecond):
			}
		}
	})
	for range 5 {
		wg.Go(func() {
			for {
				nowLock.RLock()
				now := now
				nowLock.RUnlock()
				ip := rand.IntN(10)
				iface := rand.IntN(100)
				sc.Put(now, provider.Query{
					ExporterIP: netip.MustParseAddr(fmt.Sprintf("::ffff:127.0.0.%d", ip)),
					IfIndex:    uint(iface),
				}, provider.Answer{
					Exporter:  provider.Exporter{Name: fmt.Sprintf("localhost%d", ip)},
					Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "Transit"},
				})
				select {
				case <-done:
					return
				default:
				}
			}
		})
	}
	var lookups int64
	for range 20 {
		wg.Go(func() {
			for {
				nowLock.RLock()
				now := now
				nowLock.RUnlock()
				ip := rand.IntN(10)
				iface := rand.IntN(100)
				sc.Lookup(now, provider.Query{
					ExporterIP: netip.MustParseAddr(fmt.Sprintf("::ffff:127.0.0.%d", ip)),
					IfIndex:    uint(iface),
				})
				for {
					current := atomic.LoadInt64(&lookups)
					if current == math.MaxInt64 {
						return
					}
					if atomic.CompareAndSwapInt64(&lookups, current, current+1) {
						break
					}
				}
				select {
				case <-done:
					return
				default:
				}
			}
		})
	}
	time.Sleep(30 * time.Millisecond)
	nowLock.RLock()
	sc.Expire(now.Add(-5 * time.Minute))
	nowLock.RUnlock()
	time.Sleep(30 * time.Millisecond)
	close(done)
	wg.Wait()

	for remaining := 20; remaining >= 0; remaining-- {
		gotMetrics := r.GetMetrics("akvorado_outlet_metadata_cache_")
		hits, err := strconv.ParseFloat(gotMetrics["hits_total"], 64)
		if err != nil {
			t.Fatalf("strconv.ParseFloat() error:\n%+v", err)
		}
		misses, err := strconv.ParseFloat(gotMetrics["misses_total"], 64)
		if err != nil {
			t.Fatalf("strconv.ParseFloat() error:\n%+v", err)
		}
		got := int64(hits + misses)
		expected := atomic.LoadInt64(&lookups)
		if got == expected {
			break
		} else if remaining == 0 {
			t.Errorf("hit + miss = %d, expected %d", got, expected)
		}
		time.Sleep(30 * time.Millisecond)
	}
}
