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

package errors_test

import (
	stderrors "errors"
	"strings"
	"testing"

	"github.com/projectcapsule/capsule/pkg/api"
	apierrors "github.com/projectcapsule/capsule/pkg/api/errors"
	"github.com/projectcapsule/capsule/pkg/runtime/events"
	corev1 "k8s.io/api/core/v1"
	k8sapierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime/schema"
	gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
)

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

	allowed := api.SelectorAllowedListSpec{
		AllowedListSpec: api.AllowedListSpec{
			Exact: []string{"fast", "slow"},
			Regex: "premium-.*",
		},
		LabelSelector: metav1.LabelSelector{MatchLabels: map[string]string{"class": "gold"}},
	}

	got := apierrors.AllowedValuesErrorMessage(allowed, "prefix: ")
	for _, want := range []string{
		"prefix:",
		"use one from the following list (fast, slow)",
		"use one matching the following regex (premium-.*)",
		"matching the label selector defined in the Tenant",
	} {
		if !strings.Contains(got, want) {
			t.Fatalf("AllowedValuesErrorMessage() = %q, missing %q", got, want)
		}
	}

	defaultMsg := apierrors.DefaultAllowedValuesErrorMessage(api.DefaultAllowedListSpec{SelectorAllowedListSpec: allowed}, "default: ")
	if !strings.Contains(defaultMsg, "premium-.*") {
		t.Fatalf("DefaultAllowedValuesErrorMessage() = %q, want regex detail", defaultMsg)
	}

	selectionMsg := apierrors.SelectionListWithDefaultErrorMessage(api.SelectionListWithDefaultSpec{
		SelectionListWithSpec: api.SelectionListWithSpec{
			LabelSelector: metav1.LabelSelector{MatchLabels: map[string]string{"class": "gold"}},
		},
	}, "selection: ")
	if !strings.Contains(selectionMsg, "matching the label selector defined in the Tenant") {
		t.Fatalf("SelectionListWithDefaultErrorMessage() = %q", selectionMsg)
	}
}

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

	allowed := api.DefaultAllowedListSpec{SelectorAllowedListSpec: api.SelectorAllowedListSpec{
		AllowedListSpec: api.AllowedListSpec{Exact: []string{"allowed"}, Regex: "allowed-.*"},
	}}
	selectorAllowed := api.SelectorAllowedListSpec{AllowedListSpec: api.AllowedListSpec{Exact: []string{"allowed"}}}

	tests := []struct {
		name string
		err  error
		want string
	}{
		{name: "custom quota", err: apierrors.NewCustomResourceQuotaError("pods.v1", 3), want: "pods.v1"},
		{name: "device forbidden", err: apierrors.NewDeviceClassForbidden("gpu", selectorAllowed), want: "Device Class gpu is forbidden"},
		{name: "device undefined", err: apierrors.NewDeviceClassUndefined(selectorAllowed), want: "Selected DeviceClass is forbidden"},
		{name: "gateway class", err: apierrors.NewGatewayClassError("public", stderrors.New("missing")), want: "Failed to resolve Gateway Class public"},
		{name: "gateway", err: apierrors.NewGatewayError(gatewayv1.ObjectName("gw"), stderrors.New("missing")), want: "Failed to resolve Gateway gw"},
		{name: "gateway forbidden", err: apierrors.NewGatewayClassForbidden("public", allowed), want: "Gateway Class public is forbidden"},
		{name: "gateway undefined", err: apierrors.NewGatewayClassUndefined(allowed), want: "No gateway Class is forbidden"},
		{name: "ingress class", err: apierrors.NewIngressClassError("nginx", stderrors.New("missing")), want: "Failed to resolve Ingress Class nginx"},
		{name: "ingress forbidden", err: apierrors.NewIngressClassForbidden("nginx", allowed), want: "Ingress Class nginx is forbidden"},
		{name: "ingress collision", err: apierrors.NewIngressHostnameCollision("example.com"), want: "example.com is already used"},
		{name: "empty ingress hostname", err: apierrors.NewEmptyIngressHostname(api.AllowedListSpec{Exact: []string{"example.com"}, Regex: ".*\\.example\\.com"}), want: "empty hostname is not allowed"},
		{name: "ingress hostnames invalid", err: apierrors.NewIngressHostnamesNotValid([]string{"bad_host"}, api.AllowedListSpec{Exact: []string{"example.com"}}), want: "Hostnames [bad_host] are not valid"},
		{name: "ingress undefined", err: apierrors.NewIngressClassUndefined(allowed), want: "No Ingress Class is forbidden"},
		{name: "ingress not valid", err: apierrors.NewIngressClassNotValid("nginx", allowed), want: "Ingress Class nginx is forbidden"},
		{name: "namespace quota", err: apierrors.NewNamespaceQuotaExceededError(), want: "Cannot exceed Namespace quota"},
		{name: "node labels", err: apierrors.NewNodeLabelForbiddenError(&api.ForbiddenListSpec{Exact: []string{"node-role"}, Regex: "forbidden.*"}), want: "some labels are marked as forbidden"},
		{name: "node annotations", err: apierrors.NewNodeAnnotationForbiddenError(&api.ForbiddenListSpec{Exact: []string{"internal"}}), want: "some annotations are marked as forbidden"},
		{name: "priority class", err: apierrors.NewPriorityClassError("high", stderrors.New("missing")), want: "Failed to resolve Priority Class high"},
		{name: "pod metadata", err: apierrors.NewNoPodMetadata("pod"), want: "Skipping labels sync for pod"},
		{name: "missing registry", err: apierrors.NewMissingContainerRegistryError("nginx"), want: "missing repository"},
		{name: "registry forbidden", err: apierrors.NewContainerRegistryForbidden("docker.io/nginx", api.AllowedListSpec{Exact: []string{"ghcr.io"}, Regex: "registry.*"}), want: "registry is forbidden"},
		{name: "pull policy", err: apierrors.NewImagePullPolicyForbidden("Always", "app", []string{"IfNotPresent"}), want: "ImagePullPolicy Always"},
		{name: "pod priority forbidden", err: apierrors.NewPodPriorityClassForbidden("high", allowed), want: "Pod Priority Class high is forbidden"},
		{name: "pod runtime forbidden", err: apierrors.NewPodRuntimeClassForbidden("kata", allowed), want: "Pod Runtime Class kata is forbidden"},
		{name: "services metadata", err: apierrors.NewNoServicesMetadata("service"), want: "Skipping labels sync for service"},
		{name: "external service IP forbidden empty", err: apierrors.NewExternalServiceIPForbidden(nil), want: "does not allow the use of Service with external IPs"},
		{name: "external service IP forbidden cidr", err: apierrors.NewExternalServiceIPForbidden([]api.AllowedIP{"10.0.0.0/8"}), want: "10.0.0.0/8"},
		{name: "nodeport disabled", err: apierrors.NewNodePortDisabledError(), want: "NodePort service types are forbidden"},
		{name: "external name disabled", err: apierrors.NewExternalNameDisabledError(), want: "ExternalName service types are forbidden"},
		{name: "loadbalancer disabled", err: apierrors.NewLoadBalancerDisabled(), want: "LoadBalancer service types are forbidden"},
		{name: "storage class", err: apierrors.NewStorageClassError("fast", stderrors.New("missing")), want: "Failed to resolve Storage Class fast"},
		{name: "storage class not valid", err: apierrors.NewStorageClassNotValid(allowed), want: "A valid Storage Class must be used"},
		{name: "storage forbidden", err: apierrors.NewStorageClassForbidden("slow", allowed), want: "Storage Class slow is forbidden"},
		{name: "tenant object", err: apierrors.NewNonTenantObject("obj"), want: "doesn't belong to tenant"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			if got := tt.err.Error(); !strings.Contains(got, tt.want) {
				t.Fatalf("Error() = %q, want substring %q", got, tt.want)
			}
		})
	}
}

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

	tests := []error{
		apierrors.NewMissingTenantPVLabelsError("pv-a", events.ActionValidationDenied),
		apierrors.NewCrossTenantPVMountError("pv-a", events.ActionValidationDenied),
		apierrors.NewPVSelectorError(events.ActionValidationDenied),
		apierrors.NewPvNotFoundError("pv-a", events.ActionValidationDenied),
	}

	for _, err := range tests {
		evented, ok := err.(apierrors.EventedError)
		if !ok {
			t.Fatalf("%T does not implement EventedError", err)
		}
		if evented.Reason() != events.ReasonCrossTenantReference {
			t.Fatalf("Reason() = %q, want %q", evented.Reason(), events.ReasonCrossTenantReference)
		}
		if evented.Action() != events.ActionValidationDenied {
			t.Fatalf("Action() = %q, want %q", evented.Action(), events.ActionValidationDenied)
		}
		if err.Error() == "" {
			t.Fatalf("Error() is empty")
		}
	}
}

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

	for _, err := range []error{
		apierrors.RunningInOutOfClusterModeError{},
		apierrors.CaNotYetValidError{},
		apierrors.CaExpiredError{},
	} {
		if err.Error() == "" {
			t.Fatalf("%T Error() is empty", err)
		}
	}
}

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

	if !apierrors.IgnoreGone(nil) {
		t.Fatalf("IgnoreGone(nil) = false, want true")
	}
	if !apierrors.IgnoreGone(k8sapierrors.NewNotFound(schema.GroupResource{Group: "", Resource: "pods"}, "missing")) {
		t.Fatalf("IgnoreGone(NotFound) = false, want true")
	}
	if !apierrors.IgnoreGone(&k8sapierrors.StatusError{ErrStatus: metav1.Status{
		Reason: metav1.StatusReasonForbidden,
		Details: &metav1.StatusDetails{Causes: []metav1.StatusCause{{
			Type: corev1.NamespaceTerminatingCause,
		}}},
	}}) {
		t.Fatalf("IgnoreGone(namespace terminating) = false, want true")
	}
	if !apierrors.IgnoreGone(stderrors.New("widget not found")) {
		t.Fatalf("IgnoreGone(string not found) = false, want true")
	}
	if apierrors.IgnoreGone(stderrors.New("boom")) {
		t.Fatalf("IgnoreGone(other error) = true, want false")
	}
}
