// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0

package resourcepools

import (
	"sort"
	"testing"
	"time"

	"github.com/go-logr/logr"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"

	capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
)

// ---------- helpers ----------

func q(s string) resource.Quantity { return resource.MustParse(s) }

func rl(m map[corev1.ResourceName]string) corev1.ResourceList {
	out := corev1.ResourceList{}
	for k, v := range m {
		out[k] = q(v)
	}
	return out
}

func claim(t *testing.T, uid, ns, name string, ts time.Time, req corev1.ResourceList) capsulev1beta2.ResourcePoolClaim {
	t.Helper()

	return capsulev1beta2.ResourcePoolClaim{
		ObjectMeta: metav1.ObjectMeta{
			UID:               types.UID(uid),
			Namespace:         ns,
			Name:              name,
			CreationTimestamp: metav1.NewTime(ts),
		},
		Spec: capsulev1beta2.ResourcePoolClaimSpec{
			ResourceClaims: req,
		},
	}
}

func setEq(got map[string]struct{}, want []string) bool {
	if len(got) != len(want) {
		return false
	}
	for _, w := range want {
		if _, ok := got[w]; !ok {
			return false
		}
	}
	return true
}

func poolWithClaims(hard corev1.ResourceList, claims ...capsulev1beta2.ResourcePoolClaim) *capsulev1beta2.ResourcePool {
	pool := &capsulev1beta2.ResourcePool{
		Status: capsulev1beta2.ResourcePoolStatus{
			Allocation: capsulev1beta2.ResourcePoolQuotaStatus{
				Hard: hard,
			},
		},
	}

	for i := range claims {
		pool.AddClaimToStatus(&claims[i])
	}

	return pool
}

// ---------- filterResourceListByKeys tests ----------

func TestFilterResourceListByKeys(t *testing.T) {
	t.Parallel()

	t.Run("empty keys returns empty", func(t *testing.T) {
		t.Parallel()

		in := rl(map[corev1.ResourceName]string{
			corev1.ResourceCPU:    "100m",
			corev1.ResourceMemory: "128Mi",
		})
		keys := corev1.ResourceList{}

		got := filterResourceListByKeys(in, keys)
		if len(got) != 0 {
			t.Fatalf("expected empty result, got=%v", got)
		}
	})

	t.Run("filters by intersection", func(t *testing.T) {
		t.Parallel()

		in := rl(map[corev1.ResourceName]string{
			corev1.ResourceCPU:    "250m",
			corev1.ResourceMemory: "512Mi",
			corev1.ResourcePods:   "10",
		})
		keys := rl(map[corev1.ResourceName]string{
			corev1.ResourceCPU:  "0", // values ignored, only keys matter
			corev1.ResourcePods: "0",
		})

		got := filterResourceListByKeys(in, keys)
		if len(got) != 2 {
			t.Fatalf("expected 2 keys, got=%v", got)
		}
		cpu := got[corev1.ResourceCPU]
		if cpu.Cmp(q("250m")) != 0 {
			t.Fatalf("expected cpu=250m, got=%v", cpu.String())
		}

		pods := got[corev1.ResourcePods]
		if pods.Cmp(q("10")) != 0 {
			t.Fatalf("expected pods=10, got=%v", pods.String())
		}
		if _, ok := got[corev1.ResourceMemory]; ok {
			t.Fatalf("did not expect memory in result, got=%v", got)
		}
	})

	t.Run("keys not present in input are ignored", func(t *testing.T) {
		t.Parallel()

		in := rl(map[corev1.ResourceName]string{
			corev1.ResourceCPU: "100m",
		})
		keys := rl(map[corev1.ResourceName]string{
			corev1.ResourceCPU:    "0",
			corev1.ResourceMemory: "0",
		})

		got := filterResourceListByKeys(in, keys)
		if len(got) != 1 {
			t.Fatalf("expected 1 key, got=%v", got)
		}
		if _, ok := got[corev1.ResourceCPU]; !ok {
			t.Fatalf("expected cpu key present")
		}
		if _, ok := got[corev1.ResourceMemory]; ok {
			t.Fatalf("did not expect memory key present")
		}
	})
}

func TestCanClaimWithinPoolExcludingClaim(t *testing.T) {
	t.Parallel()

	base := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)

	t.Run("allows resize when new claim fits after excluding previous allocation", func(t *testing.T) {
		t.Parallel()

		existing := claim(t, "u1", "ns", "claim", base, rl(map[corev1.ResourceName]string{corev1.ResourceCPU: "5"}))
		pool := poolWithClaims(
			rl(map[corev1.ResourceName]string{corev1.ResourceCPU: "10"}),
			existing,
		)

		resized := existing
		resized.Spec.ResourceClaims = rl(map[corev1.ResourceName]string{corev1.ResourceCPU: "10"})

		got := canClaimWithinPoolExcludingClaim(logr.Discard(), pool, &resized, pool.GetClaimFromStatus(&existing))
		if len(got) != 0 {
			t.Fatalf("expected resize to fit, got exhaustions=%v", got)
		}
	})

	t.Run("rejects resize that would exceed pool capacity", func(t *testing.T) {
		t.Parallel()

		existing := claim(t, "u1", "ns", "claim", base, rl(map[corev1.ResourceName]string{corev1.ResourceCPU: "5"}))
		pool := poolWithClaims(
			rl(map[corev1.ResourceName]string{corev1.ResourceCPU: "10"}),
			existing,
		)

		resized := existing
		resized.Spec.ResourceClaims = rl(map[corev1.ResourceName]string{corev1.ResourceCPU: "55"})

		got := canClaimWithinPoolExcludingClaim(logr.Discard(), pool, &resized, pool.GetClaimFromStatus(&existing))
		if len(got) != 1 {
			t.Fatalf("expected one exhaustion, got=%v", got)
		}

		exhaustion := got[string(corev1.ResourceCPU)]
		if exhaustion.Available.Cmp(q("10")) != 0 {
			t.Fatalf("expected available=10, got=%s", exhaustion.Available.String())
		}
		if exhaustion.Requesting.Cmp(q("55")) != 0 {
			t.Fatalf("expected requesting=55, got=%s", exhaustion.Requesting.String())
		}
	})

	t.Run("accounts for other claims in the pool", func(t *testing.T) {
		t.Parallel()

		existing := claim(t, "u1", "ns", "claim", base, rl(map[corev1.ResourceName]string{corev1.ResourceCPU: "5"}))
		other := claim(t, "u2", "ns", "other", base.Add(time.Second), rl(map[corev1.ResourceName]string{corev1.ResourceCPU: "4"}))
		pool := poolWithClaims(
			rl(map[corev1.ResourceName]string{corev1.ResourceCPU: "10"}),
			existing,
			other,
		)

		resized := existing
		resized.Spec.ResourceClaims = rl(map[corev1.ResourceName]string{corev1.ResourceCPU: "7"})

		got := canClaimWithinPoolExcludingClaim(logr.Discard(), pool, &resized, pool.GetClaimFromStatus(&existing))
		if len(got) != 1 {
			t.Fatalf("expected resize to exhaust remaining capacity, got=%v", got)
		}

		exhaustion := got[string(corev1.ResourceCPU)]
		if exhaustion.Available.Cmp(q("6")) != 0 {
			t.Fatalf("expected available=6, got=%s", exhaustion.Available.String())
		}
	})
}

// ---------- resourceListAllZero tests ----------

func TestResourceListAllZero(t *testing.T) {
	t.Parallel()

	t.Run("nil list is all zero", func(t *testing.T) {
		t.Parallel()
		if !resourceListAllZero(nil) {
			t.Fatalf("expected true")
		}
	})

	t.Run("empty list is all zero", func(t *testing.T) {
		t.Parallel()
		if !resourceListAllZero(corev1.ResourceList{}) {
			t.Fatalf("expected true")
		}
	})

	t.Run("positive quantity returns false", func(t *testing.T) {
		t.Parallel()
		rl := rl(map[corev1.ResourceName]string{
			corev1.ResourceCPU: "1",
		})
		if resourceListAllZero(rl) {
			t.Fatalf("expected false")
		}
	})

	t.Run("explicit zero quantities returns true", func(t *testing.T) {
		t.Parallel()
		rl := rl(map[corev1.ResourceName]string{
			corev1.ResourceCPU:    "0",
			corev1.ResourceMemory: "0",
		})
		if !resourceListAllZero(rl) {
			t.Fatalf("expected true")
		}
	})
}

// ---------- claimCoverageScore tests ----------

func TestClaimCoverageScore(t *testing.T) {
	t.Parallel()

	t.Run("no overlap yields zero score", func(t *testing.T) {
		t.Parallel()

		remaining := rl(map[corev1.ResourceName]string{
			corev1.ResourceCPU: "500m",
		})
		reqs := rl(map[corev1.ResourceName]string{
			corev1.ResourceMemory: "128Mi",
		})

		if got := claimCoverageScore(remaining, reqs); got != 0 {
			t.Fatalf("expected score 0, got=%v", got)
		}
	})

	t.Run("caps at remaining (min(rem, req))", func(t *testing.T) {
		t.Parallel()

		remaining := rl(map[corev1.ResourceName]string{
			corev1.ResourceCPU: "500m",
		})
		reqs := rl(map[corev1.ResourceName]string{
			corev1.ResourceCPU: "2",
		})

		// covered = remaining (500m) -> score > 0
		got := claimCoverageScore(remaining, reqs)
		if got <= 0 {
			t.Fatalf("expected positive score, got=%v", got)
		}

		// Sanity: if remaining is smaller, score should not increase.
		remaining2 := rl(map[corev1.ResourceName]string{
			corev1.ResourceCPU: "250m",
		})
		got2 := claimCoverageScore(remaining2, reqs)
		if got2 <= 0 || got2 >= got {
			t.Fatalf("expected smaller positive score; got=%v got2=%v", got, got2)
		}
	})

	t.Run("ignores zero remaining", func(t *testing.T) {
		t.Parallel()

		remaining := rl(map[corev1.ResourceName]string{
			corev1.ResourceCPU: "0",
		})
		reqs := rl(map[corev1.ResourceName]string{
			corev1.ResourceCPU: "1",
		})

		if got := claimCoverageScore(remaining, reqs); got != 0 {
			t.Fatalf("expected 0, got=%v", got)
		}
	})

	t.Run("sums across resources", func(t *testing.T) {
		t.Parallel()

		remaining := rl(map[corev1.ResourceName]string{
			corev1.ResourceCPU:    "500m",
			corev1.ResourceMemory: "1Gi",
		})
		reqs := rl(map[corev1.ResourceName]string{
			corev1.ResourceCPU:    "250m",
			corev1.ResourceMemory: "512Mi",
		})

		got := claimCoverageScore(remaining, reqs)
		if got <= 0 {
			t.Fatalf("expected positive score, got=%v", got)
		}
	})
}

// ---------- selectClaimsCoveringUsageGreedy tests ----------

func TestSelectClaimsCoveringUsageGreedy(t *testing.T) {
	t.Parallel()

	base := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)

	t.Run("empty used returns empty selection", func(t *testing.T) {
		t.Parallel()

		used := corev1.ResourceList{}
		claims := []capsulev1beta2.ResourcePoolClaim{
			claim(t, "u1", "ns", "c1", base, rl(map[corev1.ResourceName]string{corev1.ResourceCPU: "1"})),
		}

		got := selectClaimsCoveringUsageGreedy(used, claims)
		if len(got) != 0 {
			t.Fatalf("expected empty, got=%v", got)
		}
	})

	t.Run("no claims returns empty selection", func(t *testing.T) {
		t.Parallel()

		used := rl(map[corev1.ResourceName]string{corev1.ResourceCPU: "1"})
		got := selectClaimsCoveringUsageGreedy(used, nil)
		if len(got) != 0 {
			t.Fatalf("expected empty, got=%v", got)
		}
	})

	t.Run("selects single best claim that covers usage", func(t *testing.T) {
		t.Parallel()

		used := rl(map[corev1.ResourceName]string{
			corev1.ResourceCPU: "900m",
		})

		claims := []capsulev1beta2.ResourcePoolClaim{
			claim(t, "u1", "ns", "small", base.Add(1*time.Second), rl(map[corev1.ResourceName]string{corev1.ResourceCPU: "500m"})),
			claim(t, "u2", "ns", "big", base, rl(map[corev1.ResourceName]string{corev1.ResourceCPU: "2"})),
		}

		got := selectClaimsCoveringUsageGreedy(used, claims)
		if !setEq(got, []string{"u2"}) {
			t.Fatalf("expected [u2], got=%v", got)
		}
	})

	t.Run("selects multiple claims to cover usage (clamps remaining at zero)", func(t *testing.T) {
		t.Parallel()

		used := rl(map[corev1.ResourceName]string{
			corev1.ResourceCPU: "1500m",
		})

		claims := []capsulev1beta2.ResourcePoolClaim{
			claim(t, "u1", "ns", "c1", base, rl(map[corev1.ResourceName]string{corev1.ResourceCPU: "1"})), // 1000m
			claim(t, "u2", "ns", "c2", base.Add(1*time.Second), rl(map[corev1.ResourceName]string{corev1.ResourceCPU: "1"})),
			claim(t, "u3", "ns", "c3", base.Add(2*time.Second), rl(map[corev1.ResourceName]string{corev1.ResourceCPU: "250m"})),
		}

		got := selectClaimsCoveringUsageGreedy(used, claims)

		// Greedy should take u1 (covers 1000m), then u2 (covers remaining 500m).
		if !setEq(got, []string{"u1", "u2"}) {
			t.Fatalf("expected [u1 u2], got=%v", got)
		}
	})

	t.Run("handles used resources not present in any claim (stops when cannot improve)", func(t *testing.T) {
		t.Parallel()

		used := rl(map[corev1.ResourceName]string{
			corev1.ResourceCPU:    "500m",
			corev1.ResourceMemory: "256Mi",
		})

		claims := []capsulev1beta2.ResourcePoolClaim{
			claim(t, "u1", "ns", "cpu-only", base, rl(map[corev1.ResourceName]string{corev1.ResourceCPU: "1"})),
		}

		got := selectClaimsCoveringUsageGreedy(used, claims)
		// It can reduce CPU remaining, but cannot cover memory at all; algorithm should select cpu-only then stop.
		if !setEq(got, []string{"u1"}) {
			t.Fatalf("expected [u1], got=%v", got)
		}
	})

	t.Run("deterministic tie-break: equal score chooses oldest, then name, then namespace", func(t *testing.T) {
		t.Parallel()

		used := rl(map[corev1.ResourceName]string{
			corev1.ResourceCPU: "500m",
		})

		// Three identical claims by resources; selection should be deterministic by the sort.
		cA := claim(t, "uA", "ns-b", "a", base, rl(map[corev1.ResourceName]string{corev1.ResourceCPU: "1"}))
		cB := claim(t, "uB", "ns-a", "b", base, rl(map[corev1.ResourceName]string{corev1.ResourceCPU: "1"}))
		cC := claim(t, "uC", "ns-a", "a", base, rl(map[corev1.ResourceName]string{corev1.ResourceCPU: "1"}))

		claims := []capsulev1beta2.ResourcePoolClaim{cA, cB, cC}

		// Shuffle input order to ensure determinism isn't accidental
		sort.Slice(claims, func(i, j int) bool { return i > j })

		got := selectClaimsCoveringUsageGreedy(used, claims)

		// After sorting: same ts -> name asc -> namespace asc
		// Name "a" beats "b". For name "a", namespace "ns-a" beats "ns-b".
		// That means uC (ns-a, a) should be selected.
		if !setEq(got, []string{"uC"}) {
			t.Fatalf("expected [uC], got=%v", got)
		}
	})
}
