// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

package flowtrack

import (
	"encoding/json"
	"net/netip"
	"testing"

	"tailscale.com/tstest"
	"tailscale.com/types/ipproto"
)

func TestCache(t *testing.T) {
	c := &Cache[int]{MaxEntries: 2}

	k1 := MakeTuple(0, netip.MustParseAddrPort("1.1.1.1:1"), netip.MustParseAddrPort("1.1.1.1:1"))
	k2 := MakeTuple(0, netip.MustParseAddrPort("1.1.1.1:1"), netip.MustParseAddrPort("2.2.2.2:2"))
	k3 := MakeTuple(0, netip.MustParseAddrPort("1.1.1.1:1"), netip.MustParseAddrPort("3.3.3.3:3"))
	k4 := MakeTuple(0, netip.MustParseAddrPort("1.1.1.1:1"), netip.MustParseAddrPort("4.4.4.4:4"))

	wantLen := func(want int) {
		t.Helper()
		if got := c.Len(); got != want {
			t.Fatalf("Len = %d; want %d", got, want)
		}
	}
	wantVal := func(key Tuple, want int) {
		t.Helper()
		got, ok := c.Get(key)
		if !ok {
			t.Fatalf("Get(%q) failed; want value %v", key, want)
		}
		if *got != want {
			t.Fatalf("Get(%q) = %v; want %v", key, got, want)
		}
	}
	wantMissing := func(key Tuple) {
		t.Helper()
		if got, ok := c.Get(key); ok {
			t.Fatalf("Get(%q) = %v; want absent from cache", key, got)
		}
	}

	wantLen(0)
	c.RemoveOldest() // shouldn't panic
	c.Remove(k4)     // shouldn't panic

	c.Add(k1, 1)
	wantLen(1)
	c.Add(k2, 2)
	wantLen(2)
	c.Add(k3, 3)
	wantLen(2) // hit the max

	wantMissing(k1)
	c.Remove(k1)
	wantLen(2) // no change; k1 should've been the deleted one per LRU

	wantVal(k3, 3)

	wantVal(k2, 2)
	c.Remove(k2)
	wantLen(1)
	wantMissing(k2)

	c.Add(k3, 30)
	wantVal(k3, 30)
	wantLen(1)

	err := tstest.MinAllocsPerRun(t, 0, func() {
		got, ok := c.Get(k3)
		if !ok {
			t.Fatal("missing k3")
		}
		if *got != 30 {
			t.Fatalf("got = %d; want 30", got)
		}
	})
	if err != nil {
		t.Error(err)
	}
}

func BenchmarkMapKeys(b *testing.B) {
	b.Run("typed", func(b *testing.B) {
		c := &Cache[struct{}]{MaxEntries: 1000}
		var t Tuple
		for proto := range 20 {
			t = Tuple{proto: ipproto.Proto(proto), src: netip.MustParseAddr("1.1.1.1").As16(), srcPort: 1, dst: netip.MustParseAddr("1.1.1.1").As16(), dstPort: 1}
			c.Add(t, struct{}{})
		}
		for i := 0; i < b.N; i++ {
			_, ok := c.Get(t)
			if !ok {
				b.Fatal("missing key")
			}
		}
	})
}

func TestStringJSON(t *testing.T) {
	v := MakeTuple(123,
		netip.MustParseAddrPort("1.2.3.4:5"),
		netip.MustParseAddrPort("6.7.8.9:10"))

	if got, want := v.String(), "(IPProto-123 1.2.3.4:5 => 6.7.8.9:10)"; got != want {
		t.Errorf("String = %q; want %q", got, want)
	}

	got, err := json.Marshal(v)
	if err != nil {
		t.Fatal(err)
	}
	const want = `{"proto":123,"src":"1.2.3.4:5","dst":"6.7.8.9:10"}`
	if string(got) != want {
		t.Errorf("Marshal = %q; want %q", got, want)
	}

	var back Tuple
	if err := json.Unmarshal(got, &back); err != nil {
		t.Fatal(err)
	}
	if back != v {
		t.Errorf("back = %v; want %v", back, v)
	}
}
