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

package globalresourcequotas

import (
	"context"
	"testing"
	"time"

	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"

	capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
	"github.com/projectcapsule/capsule/pkg/runtime/selectors"
)

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

	scheme := runtime.NewScheme()
	if err := corev1.AddToScheme(scheme); err != nil {
		t.Fatal(err)
	}
	if err := capsulev1beta2.AddToScheme(scheme); err != nil {
		t.Fatal(err)
	}

	hard := corev1.ResourceList{
		corev1.ResourceRequestsCPU:    resource.MustParse("8"),
		corev1.ResourceRequestsMemory: resource.MustParse("16Gi"),
	}
	quota := &capsulev1beta2.GlobalResourceQuota{
		ObjectMeta: metav1.ObjectMeta{Name: "shared", UID: types.UID("quota-uid")},
		Spec: capsulev1beta2.GlobalResourceQuotaSpec{
			Quota: corev1.ResourceQuotaSpec{Hard: hard},
		},
	}
	namespaceA := corev1.Namespace{
		ObjectMeta: metav1.ObjectMeta{Name: "a"},
		Status:     corev1.NamespaceStatus{Phase: corev1.NamespaceActive},
	}
	namespaceB := corev1.Namespace{
		ObjectMeta: metav1.ObjectMeta{Name: "b"},
		Status:     corev1.NamespaceStatus{Phase: corev1.NamespaceActive},
	}
	resourceQuotaA := observedResourceQuota(quota, namespaceA.Name, corev1.ResourceList{
		corev1.ResourceRequestsCPU:    resource.MustParse("2"),
		corev1.ResourceRequestsMemory: resource.MustParse("3Gi"),
	})
	resourceQuotaB := observedResourceQuota(quota, namespaceB.Name, corev1.ResourceList{
		corev1.ResourceRequestsCPU:    resource.MustParse("1"),
		corev1.ResourceRequestsMemory: resource.MustParse("4Gi"),
	})

	cl := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(quota, resourceQuotaA, resourceQuotaB).
		Build()
	controller := &Controller{Client: cl, reader: cl}

	status, initialized, err := controller.observeUsage(
		context.Background(),
		quota,
		[]corev1.Namespace{namespaceB, namespaceA},
	)
	if err != nil {
		t.Fatalf("observeUsage() error = %v", err)
	}
	if !initialized {
		t.Fatal("observeUsage() initialized = false, want true")
	}
	if status.NamespaceSize != 2 || len(status.NamespaceUsage) != 2 {
		t.Fatalf("namespace status = size %d usage %#v", status.NamespaceSize, status.NamespaceUsage)
	}
	if status.Namespaces[0] != "a" || status.Namespaces[1] != "b" {
		t.Fatalf("ordered namespaces = %#v, want [a b]", status.Namespaces)
	}
	assertResource(t, status.Total.Used, corev1.ResourceRequestsCPU, "3")
	assertResource(t, status.Total.Used, corev1.ResourceRequestsMemory, "7Gi")
	assertResource(t, status.Total.Available, corev1.ResourceRequestsCPU, "5")
	assertResource(t, status.NamespaceUsage["b"].Used, corev1.ResourceRequestsMemory, "4Gi")
}

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

	now := metav1.Now()
	expires := metav1.NewTime(now.Add(time.Minute))
	current := &capsulev1beta2.QuantityLedgerResourceQuotaStatus{
		Used: corev1.ResourceList{corev1.ResourceRequestsCPU: resource.MustParse("0")},
		Reservations: []capsulev1beta2.QuantityLedgerResourceQuotaReservation{{
			ID:        "request",
			Delta:     corev1.ResourceList{corev1.ResourceRequestsCPU: resource.MustParse("2")},
			ExpiresAt: &expires,
		}},
	}

	next := reconcileLedgerStatus(
		current,
		3,
		[]string{"a", "b"},
		corev1.ResourceList{corev1.ResourceRequestsCPU: resource.MustParse("1")},
		true,
		now,
	)

	if len(next.Reservations) != 1 {
		t.Fatalf("reservations = %d, want 1", len(next.Reservations))
	}
	assertResource(t, next.Reservations[0].Delta, corev1.ResourceRequestsCPU, "1")
	assertResource(t, next.Used, corev1.ResourceRequestsCPU, "1")
	assertResource(t, next.Allocated, corev1.ResourceRequestsCPU, "2")
	if next.ObservedGeneration != 3 || len(next.Namespaces) != 2 {
		t.Fatalf("ledger snapshot = generation %d, namespaces %#v", next.ObservedGeneration, next.Namespaces)
	}
}

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

	quota := &capsulev1beta2.GlobalResourceQuota{
		Spec: capsulev1beta2.GlobalResourceQuotaSpec{
			NamespaceSelectors: []selectors.NamespaceSelector{
				{LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"team": "a"}}},
				{LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"team": "b"}}},
			},
		},
	}
	namespace := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{
		Name: "b", Labels: map[string]string{"team": "b"},
	}}

	cl := fake.NewClientBuilder().WithObjects(namespace).Build()
	matched, err := selectors.GetNamespacesMatchingSelectors(
		context.Background(),
		cl,
		quota.Spec.NamespaceSelectors,
	)
	if err != nil {
		t.Fatal(err)
	}
	if len(matched) != 1 || matched[0].Name != namespace.Name {
		t.Fatalf("matched namespaces = %#v, want b", matched)
	}
}

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

	quota := corev1.ResourceQuotaSpec{
		Hard: corev1.ResourceList{
			corev1.ResourceRequestsCPU:    resource.MustParse("10"),
			corev1.ResourceRequestsMemory: resource.MustParse("4Gi"),
		},
		Scopes: []corev1.ResourceQuotaScope{corev1.ResourceQuotaScopeNotTerminating},
	}
	status := &capsulev1beta2.GlobalResourceQuotaStatus{
		Total: capsulev1beta2.GlobalResourceQuotaUsage{
			Available: corev1.ResourceList{
				corev1.ResourceRequestsCPU:    resource.MustParse("4"),
				corev1.ResourceRequestsMemory: resource.MustParse("0"),
			},
		},
		NamespaceUsage: capsulev1beta2.GlobalResourceQuotaNamespaceUsage{
			"team-a": {
				Used: corev1.ResourceList{
					corev1.ResourceRequestsCPU:    resource.MustParse("2"),
					corev1.ResourceRequestsMemory: resource.MustParse("5Gi"),
				},
			},
		},
	}

	projected := projectedResourceQuotaSpec(quota, status, "team-a")

	assertResource(t, projected.Hard, corev1.ResourceRequestsCPU, "6")
	assertResource(t, projected.Hard, corev1.ResourceRequestsMemory, "5Gi")
	if len(projected.Scopes) != 1 || projected.Scopes[0] != corev1.ResourceQuotaScopeNotTerminating {
		t.Fatalf("projected scopes = %#v, want NotTerminating", projected.Scopes)
	}

	// The desired shared limit remains immutable while projecting a namespace's
	// native hard values.
	assertResource(t, quota.Hard, corev1.ResourceRequestsCPU, "10")
	assertResource(t, quota.Hard, corev1.ResourceRequestsMemory, "4Gi")
}

func observedResourceQuota(
	quota *capsulev1beta2.GlobalResourceQuota,
	namespace string,
	used corev1.ResourceList,
) *corev1.ResourceQuota {
	return &corev1.ResourceQuota{
		ObjectMeta: metav1.ObjectMeta{
			Name:      quota.GetResourceQuotaName(),
			Namespace: namespace,
		},
		Status: corev1.ResourceQuotaStatus{
			Hard: quota.Spec.Quota.Hard.DeepCopy(),
			Used: used.DeepCopy(),
		},
	}
}

func assertResource(
	t *testing.T,
	resources corev1.ResourceList,
	name corev1.ResourceName,
	want string,
) {
	t.Helper()

	got := resources[name]
	if got.Cmp(resource.MustParse(want)) != 0 {
		t.Fatalf("%s = %s, want %s", name, got.String(), want)
	}
}
