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

package v1beta2_test

import (
	"testing"

	"github.com/stretchr/testify/assert"
	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"
	"github.com/projectcapsule/capsule/pkg/api/meta"
)

func TestGetClaimFromStatus(t *testing.T) {
	ns := "test-namespace"
	testUID := types.UID("test-uid")
	otherUID := types.UID("wrong-uid")

	claim := &capsulev1beta2.ResourcePoolClaim{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "claim-a",
			Namespace: ns,
			UID:       testUID,
		},
	}

	pool := &capsulev1beta2.ResourcePool{
		Status: capsulev1beta2.ResourcePoolStatus{
			Claims: capsulev1beta2.ResourcePoolNamespaceClaimsStatus{
				ns: {
					&capsulev1beta2.ResourcePoolClaimsItem{
						NamespacedRFC1123ObjectReferenceWithNamespaceWithUID: meta.NamespacedRFC1123ObjectReferenceWithNamespaceWithUID{
							UID: testUID,
						},
						Claims: corev1.ResourceList{
							corev1.ResourceCPU:    resource.MustParse("500m"),
							corev1.ResourceMemory: resource.MustParse("256Mi"),
						},
					},
				},
			},
		},
	}

	t.Run("returns matching claim", func(t *testing.T) {
		found := pool.GetClaimFromStatus(claim)
		assert.NotNil(t, found)
		assert.Equal(t, testUID, found.UID)
	})

	t.Run("returns nil if UID doesn't match", func(t *testing.T) {
		claimWrongUID := *claim
		claimWrongUID.UID = otherUID

		found := pool.GetClaimFromStatus(&claimWrongUID)
		assert.Nil(t, found)
	})

	t.Run("returns nil if namespace has no claims", func(t *testing.T) {
		claimWrongNS := *claim
		claimWrongNS.Namespace = "other-ns"

		found := pool.GetClaimFromStatus(&claimWrongNS)
		assert.Nil(t, found)
	})
}

func makeResourceList(cpu, memory string) corev1.ResourceList {
	return corev1.ResourceList{
		corev1.ResourceLimitsCPU:    resource.MustParse(cpu),
		corev1.ResourceLimitsMemory: resource.MustParse(memory),
	}
}

func makeClaim(name, ns string, uid types.UID, res corev1.ResourceList) *capsulev1beta2.ResourcePoolClaim {
	return &capsulev1beta2.ResourcePoolClaim{
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: ns,
			UID:       uid,
		},
		Spec: capsulev1beta2.ResourcePoolClaimSpec{
			ResourceClaims: res,
		},
	}
}

func TestAssignNamespaces(t *testing.T) {
	pool := &capsulev1beta2.ResourcePool{}

	namespaces := []corev1.Namespace{
		{ObjectMeta: metav1.ObjectMeta{Name: "active-ns"}, Status: corev1.NamespaceStatus{Phase: corev1.NamespaceActive}},
		{ObjectMeta: metav1.ObjectMeta{Name: "terminating-ns", DeletionTimestamp: &metav1.Time{}}, Status: corev1.NamespaceStatus{Phase: corev1.NamespaceTerminating}},
	}

	pool.AssignNamespaces(namespaces)

	assert.Equal(t, uint(1), pool.Status.NamespaceSize)
	assert.Equal(t, []string{"active-ns"}, pool.Status.Namespaces)
}

func TestAssignClaims(t *testing.T) {
	pool := &capsulev1beta2.ResourcePool{
		Status: capsulev1beta2.ResourcePoolStatus{
			Claims: capsulev1beta2.ResourcePoolNamespaceClaimsStatus{
				"ns": {
					&capsulev1beta2.ResourcePoolClaimsItem{},
					&capsulev1beta2.ResourcePoolClaimsItem{},
				},
			},
		},
	}
	pool.AssignClaims()

	assert.Equal(t, uint(2), pool.Status.ClaimSize)
}

func TestAddRemoveClaimToStatus(t *testing.T) {
	pool := &capsulev1beta2.ResourcePool{}

	claim := makeClaim("claim-1", "ns", "uid-1", makeResourceList("1", "1Gi"))
	pool.AddClaimToStatus(claim)

	stored := pool.GetClaimFromStatus(claim)

	assert.NotNil(t, stored)
	assert.Equal(t, meta.RFC1123Name("claim-1"), stored.Name)

	pool.RemoveClaimFromStatus(claim)
	assert.Nil(t, pool.GetClaimFromStatus(claim))
}

func TestCalculateResources(t *testing.T) {
	pool := &capsulev1beta2.ResourcePool{
		Status: capsulev1beta2.ResourcePoolStatus{
			Allocation: capsulev1beta2.ResourcePoolQuotaStatus{
				Hard: corev1.ResourceList{
					corev1.ResourceLimitsCPU: resource.MustParse("2"),
				},
			},
			Claims: capsulev1beta2.ResourcePoolNamespaceClaimsStatus{
				"ns": {
					&capsulev1beta2.ResourcePoolClaimsItem{
						Claims: corev1.ResourceList{
							corev1.ResourceLimitsCPU: resource.MustParse("1"),
						},
					},
				},
			},
		},
	}

	pool.CalculateClaimedResources()

	actualClaimed := pool.Status.Allocation.Claimed[corev1.ResourceLimitsCPU]
	actualAvailable := pool.Status.Allocation.Available[corev1.ResourceLimitsCPU]

	assert.Equal(t, 0, (&actualClaimed).Cmp(resource.MustParse("1")))
	assert.Equal(t, 0, (&actualAvailable).Cmp(resource.MustParse("1")))
}

func TestCanClaimFromPool(t *testing.T) {
	pool := &capsulev1beta2.ResourcePool{
		Status: capsulev1beta2.ResourcePoolStatus{
			Allocation: capsulev1beta2.ResourcePoolQuotaStatus{
				Hard: corev1.ResourceList{
					corev1.ResourceLimitsMemory: resource.MustParse("1Gi"),
				},
				Claimed: corev1.ResourceList{
					corev1.ResourceLimitsMemory: resource.MustParse("512Mi"),
				},
			},
		},
	}

	errs := pool.CanClaimFromPool(corev1.ResourceList{
		corev1.ResourceLimitsMemory: resource.MustParse("1Gi"),
	})
	assert.Len(t, errs, 1)

	errs = pool.CanClaimFromPool(corev1.ResourceList{
		corev1.ResourceLimitsMemory: resource.MustParse("500Mi"),
	})
	assert.Len(t, errs, 0)
}

func TestGetResourceQuotaHardResources(t *testing.T) {
	pool := &capsulev1beta2.ResourcePool{
		Spec: capsulev1beta2.ResourcePoolSpec{
			Defaults: corev1.ResourceList{
				corev1.ResourceLimitsCPU: resource.MustParse("1"),
			},
		},
		Status: capsulev1beta2.ResourcePoolStatus{
			Claims: capsulev1beta2.ResourcePoolNamespaceClaimsStatus{
				"ns": {
					&capsulev1beta2.ResourcePoolClaimsItem{
						Claims: corev1.ResourceList{
							corev1.ResourceLimitsCPU: resource.MustParse("1"),
						},
					},
				},
			},
		},
	}

	res := pool.GetResourceQuotaHardResources("ns")
	actual := res[corev1.ResourceLimitsCPU]
	assert.Equal(t, 0, (&actual).Cmp(resource.MustParse("2")))
}

func TestGetNamespaceClaims(t *testing.T) {
	pool := &capsulev1beta2.ResourcePool{
		Status: capsulev1beta2.ResourcePoolStatus{
			Claims: capsulev1beta2.ResourcePoolNamespaceClaimsStatus{
				"ns": {
					&capsulev1beta2.ResourcePoolClaimsItem{
						NamespacedRFC1123ObjectReferenceWithNamespaceWithUID: meta.NamespacedRFC1123ObjectReferenceWithNamespaceWithUID{UID: "uid1"},
						Claims: corev1.ResourceList{
							corev1.ResourceLimitsCPU: resource.MustParse("1"),
						},
					},
				},
			},
		},
	}

	claims, res := pool.GetNamespaceClaims("ns")
	assert.Contains(t, claims, "uid1")
	actual := res[corev1.ResourceLimitsCPU]
	assert.Equal(t, 0, (&actual).Cmp(resource.MustParse("1")))
}

func TestGetClaimedByNamespaceClaims(t *testing.T) {
	pool := &capsulev1beta2.ResourcePool{
		Status: capsulev1beta2.ResourcePoolStatus{
			Claims: capsulev1beta2.ResourcePoolNamespaceClaimsStatus{
				"ns1": {
					&capsulev1beta2.ResourcePoolClaimsItem{
						Claims: makeResourceList("1", "1Gi"),
					},
				},
			},
		},
	}

	result := pool.GetClaimedByNamespaceClaims()
	actualCPU := result["ns1"][corev1.ResourceLimitsCPU]
	actualMem := result["ns1"][corev1.ResourceLimitsMemory]

	assert.Equal(t, 0, (&actualCPU).Cmp(resource.MustParse("1")))
	assert.Equal(t, 0, (&actualMem).Cmp(resource.MustParse("1Gi")))
}

func TestIsBoundToResourcePool_2(t *testing.T) {
	t.Run("bound to resource pool (Assigned=True)", func(t *testing.T) {
		claim := &capsulev1beta2.ResourcePoolClaim{
			Status: capsulev1beta2.ResourcePoolClaimStatus{
				Conditions: meta.ConditionList{},
			},
		}

		assert.Equal(t, false, claim.IsBoundInResourcePool())
	})

	t.Run("not bound - wrong condition type", func(t *testing.T) {
		claim := &capsulev1beta2.ResourcePoolClaim{
			Status: capsulev1beta2.ResourcePoolClaimStatus{
				Conditions: meta.ConditionList{
					meta.Condition{},
				},
			},
		}

		cond := meta.NewAssignedCondition(claim)
		cond.Status = metav1.ConditionFalse
		claim.Status.Conditions.UpdateConditionByType(cond)

		assert.Equal(t, false, claim.IsBoundInResourcePool())
	})

	t.Run("not bound - condition not true", func(t *testing.T) {
		claim := &capsulev1beta2.ResourcePoolClaim{
			Status: capsulev1beta2.ResourcePoolClaimStatus{
				Conditions: meta.ConditionList{
					meta.Condition{},
				},
			},
		}

		cond := meta.NewBoundCondition(claim)
		cond.Status = metav1.ConditionFalse
		claim.Status.Conditions.UpdateConditionByType(cond)

		assert.Equal(t, false, claim.IsBoundInResourcePool())
	})

	t.Run("not bound - condition not true", func(t *testing.T) {
		claim := &capsulev1beta2.ResourcePoolClaim{
			Status: capsulev1beta2.ResourcePoolClaimStatus{
				Conditions: meta.ConditionList{
					meta.Condition{},
				},
			},
		}

		cond := meta.NewBoundCondition(claim)
		cond.Status = metav1.ConditionTrue
		claim.Status.Conditions.UpdateConditionByType(cond)

		assert.Equal(t, true, claim.IsBoundInResourcePool())
	})

}
