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

package selectors_test

import (
	"context"
	"reflect"
	"testing"

	"github.com/projectcapsule/capsule/pkg/runtime/selectors"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

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

	tests := []struct {
		name      string
		labels    labels.Set
		selectors []metav1.LabelSelector
		want      bool
	}{
		{
			name: "empty selectors match",
			want: true,
		},
		{
			name:   "matching selector returns true",
			labels: labels.Set{"env": "prod"},
			selectors: []metav1.LabelSelector{{
				MatchLabels: map[string]string{"env": "prod"},
			}},
			want: true,
		},
		{
			name:   "any matching selector returns true",
			labels: labels.Set{"env": "prod"},
			selectors: []metav1.LabelSelector{
				{MatchLabels: map[string]string{"env": "dev"}},
				{MatchLabels: map[string]string{"env": "prod"}},
			},
			want: true,
		},
		{
			name:   "invalid selector is skipped",
			labels: labels.Set{"env": "prod"},
			selectors: []metav1.LabelSelector{
				{MatchExpressions: []metav1.LabelSelectorRequirement{{Key: "env", Operator: "invalid"}}},
				{MatchLabels: map[string]string{"env": "prod"}},
			},
			want: true,
		},
		{
			name:   "no selector matches",
			labels: labels.Set{"env": "prod"},
			selectors: []metav1.LabelSelector{{
				MatchLabels: map[string]string{"env": "dev"},
			}},
			want: false,
		},
	}

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

			if got := selectors.MatchesSelectors(tt.labels, tt.selectors); got != tt.want {
				t.Fatalf("MatchesSelectors() = %v, want %v", got, tt.want)
			}
		})
	}
}

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

	t.Run("matches", func(t *testing.T) {
		t.Parallel()

		got, err := selectors.MatchesSelector(labels.Set{"tier": "frontend"}, metav1.LabelSelector{
			MatchLabels: map[string]string{"tier": "frontend"},
		})
		if err != nil {
			t.Fatalf("MatchesSelector() unexpected error: %v", err)
		}
		if !got {
			t.Fatalf("MatchesSelector() = false, want true")
		}
	})

	t.Run("does not match", func(t *testing.T) {
		t.Parallel()

		got, err := selectors.MatchesSelector(labels.Set{"tier": "backend"}, metav1.LabelSelector{
			MatchLabels: map[string]string{"tier": "frontend"},
		})
		if err != nil {
			t.Fatalf("MatchesSelector() unexpected error: %v", err)
		}
		if got {
			t.Fatalf("MatchesSelector() = true, want false")
		}
	})

	t.Run("invalid selector returns error", func(t *testing.T) {
		t.Parallel()

		got, err := selectors.MatchesSelector(labels.Set{"tier": "backend"}, metav1.LabelSelector{
			MatchExpressions: []metav1.LabelSelectorRequirement{{Key: "tier", Operator: "invalid"}},
		})
		if err == nil {
			t.Fatalf("MatchesSelector() expected error")
		}
		if got {
			t.Fatalf("MatchesSelector() = true, want false on error")
		}
	})
}

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

	ctx := context.Background()
	cl := fakeClient(
		namespace("alpha", map[string]string{"team": "a"}),
		namespace("beta", map[string]string{"team": "b"}),
		namespace("gamma", nil),
	)

	t.Run("nil selector returns nil", func(t *testing.T) {
		t.Parallel()

		got, err := (&selectors.NamespaceSelector{}).GetMatchingNamespaces(ctx, cl)
		if err != nil {
			t.Fatalf("GetMatchingNamespaces() unexpected error: %v", err)
		}
		if got != nil {
			t.Fatalf("GetMatchingNamespaces() = %#v, want nil", got)
		}
	})

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

		got, err := (&selectors.NamespaceSelector{
			LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"team": "a"}},
		}).GetMatchingNamespaces(ctx, cl)
		if err != nil {
			t.Fatalf("GetMatchingNamespaces() unexpected error: %v", err)
		}

		if names := namespaceNames(got); !reflect.DeepEqual(names, []string{"alpha"}) {
			t.Fatalf("GetMatchingNamespaces() names = %#v, want alpha", names)
		}
	})

	t.Run("invalid selector returns error", func(t *testing.T) {
		t.Parallel()

		_, err := (&selectors.NamespaceSelector{
			LabelSelector: &metav1.LabelSelector{
				MatchExpressions: []metav1.LabelSelectorRequirement{{Key: "team", Operator: "invalid"}},
			},
		}).GetMatchingNamespaces(ctx, cl)
		if err == nil {
			t.Fatalf("GetMatchingNamespaces() expected error")
		}
	})
}

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

	ctx := context.Background()
	cl := fakeClient(
		namespace("zeta", map[string]string{"team": "a", "region": "eu"}),
		namespace("alpha", map[string]string{"team": "a", "region": "us"}),
		namespace("beta", map[string]string{"team": "b", "region": "eu"}),
	)
	counting := &listCountingReader{Reader: cl}

	got, err := selectors.GetNamespacesMatchingSelectors(ctx, counting, []selectors.NamespaceSelector{
		{LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"team": "a"}}},
		{LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"region": "eu"}}},
	})
	if err != nil {
		t.Fatalf("GetNamespacesMatchingSelectors() unexpected error: %v", err)
	}

	if names := namespaceNames(got); !reflect.DeepEqual(names, []string{"alpha", "beta", "zeta"}) {
		t.Fatalf("GetNamespacesMatchingSelectors() names = %#v, want sorted unique names", names)
	}
	if counting.listCalls != 1 {
		t.Fatalf("GetNamespacesMatchingSelectors() list calls = %d, want 1", counting.listCalls)
	}

	gotNames, err := selectors.GetNamespacesMatchingSelectorsStrings(ctx, cl, []selectors.NamespaceSelector{
		{LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"team": "a"}}},
	})
	if err != nil {
		t.Fatalf("GetNamespacesMatchingSelectorsStrings() unexpected error: %v", err)
	}
	if !reflect.DeepEqual(gotNames, []string{"alpha", "zeta"}) {
		t.Fatalf("GetNamespacesMatchingSelectorsStrings() = %#v, want alpha and zeta", gotNames)
	}

	empty, err := selectors.GetNamespacesMatchingSelectors(ctx, cl, nil)
	if err != nil {
		t.Fatalf("GetNamespacesMatchingSelectors() unexpected error for nil selectors: %v", err)
	}
	if empty != nil {
		t.Fatalf("GetNamespacesMatchingSelectors() = %#v, want nil for no selectors", empty)
	}
}

type listCountingReader struct {
	client.Reader

	listCalls int
}

func (r *listCountingReader) List(
	ctx context.Context,
	list client.ObjectList,
	opts ...client.ListOption,
) error {
	r.listCalls++

	return r.Reader.List(ctx, list, opts...)
}

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

	ctx := context.Background()
	cl := fakeClient(
		namespace("allowed", map[string]string{"tenant": "one"}),
		namespace("denied", map[string]string{"tenant": "two"}),
	)
	objects := []metav1.Object{
		configMap("allowed", "match", map[string]string{"app": "api"}),
		configMap("allowed", "skip-label", map[string]string{"app": "worker"}),
		configMap("denied", "skip-namespace", map[string]string{"app": "api"}),
	}

	got, err := (&selectors.SelectorWithNamespaceSelector{
		LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "api"}},
		NamespaceSelector: &selectors.NamespaceSelector{
			LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"tenant": "one"}},
		},
	}).MatchObjects(ctx, cl, objects)
	if err != nil {
		t.Fatalf("MatchObjects() unexpected error: %v", err)
	}

	if names := objectNames(got); !reflect.DeepEqual(names, []string{"match"}) {
		t.Fatalf("MatchObjects() names = %#v, want match", names)
	}

	noNamespaceFilter, err := (&selectors.SelectorWithNamespaceSelector{
		LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "api"}},
	}).MatchObjects(ctx, cl, objects)
	if err != nil {
		t.Fatalf("MatchObjects() unexpected error without namespace selector: %v", err)
	}
	if names := objectNames(noNamespaceFilter); !reflect.DeepEqual(names, []string{"match", "skip-namespace"}) {
		t.Fatalf("MatchObjects() names = %#v, want both app=api objects", names)
	}

	nilSelector, err := (*selectors.SelectorWithNamespaceSelector)(nil).MatchObjects(ctx, cl, objects)
	if err != nil {
		t.Fatalf("MatchObjects() unexpected error for nil selector: %v", err)
	}
	if nilSelector != nil {
		t.Fatalf("MatchObjects() = %#v, want nil for nil receiver", nilSelector)
	}
}

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

	ctx := context.Background()
	cl := fakeClient(
		configMap("ns-a", "one", map[string]string{"app": "api"}),
		configMap("ns-a", "two", map[string]string{"app": "worker"}),
		configMap("ns-b", "three", nil),
	)

	got, err := selectors.ListBySelectors[*corev1.ConfigMap](ctx, cl, &corev1.ConfigMapList{}, []*metav1.LabelSelector{
		{MatchLabels: map[string]string{"app": "api"}},
		{MatchLabels: map[string]string{"app": "worker"}},
	})
	if err != nil {
		t.Fatalf("ListBySelectors() unexpected error: %v", err)
	}
	if names := configMapKeys(got); !reflect.DeepEqual(names, []string{"ns-a/one", "ns-a/two"}) {
		t.Fatalf("ListBySelectors() = %#v, want matching config maps", names)
	}

	empty, err := selectors.ListBySelectors[*corev1.ConfigMap](ctx, cl, &corev1.ConfigMapList{}, nil)
	if err != nil {
		t.Fatalf("ListBySelectors() unexpected error for nil selectors: %v", err)
	}
	if empty != nil {
		t.Fatalf("ListBySelectors() = %#v, want nil for nil selectors", empty)
	}

	empty, err = selectors.ListBySelectors[*corev1.ConfigMap](ctx, cl, &corev1.ConfigMapList{}, []*metav1.LabelSelector{nil})
	if err != nil {
		t.Fatalf("ListBySelectors() unexpected error for nil selector entry: %v", err)
	}
	if empty != nil {
		t.Fatalf("ListBySelectors() = %#v, want nil when all selector entries are nil", empty)
	}

	if _, err = selectors.ListBySelectors[*corev1.ConfigMap](ctx, cl, nil, []*metav1.LabelSelector{{}}); err == nil {
		t.Fatalf("ListBySelectors() expected error for nil list")
	}

	if _, err = selectors.ListBySelectors[*corev1.ConfigMap](ctx, cl, &corev1.ConfigMapList{}, []*metav1.LabelSelector{{
		MatchExpressions: []metav1.LabelSelectorRequirement{{Key: "app", Operator: "invalid"}},
	}}); err == nil {
		t.Fatalf("ListBySelectors() expected error for invalid selector")
	}
}

func fakeClient(objects ...client.Object) client.Client {
	scheme := runtime.NewScheme()
	if err := corev1.AddToScheme(scheme); err != nil {
		panic(err)
	}

	return fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build()
}

func namespace(name string, lbls map[string]string) *corev1.Namespace {
	return &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: name, Labels: lbls}}
}

func configMap(namespace, name string, lbls map[string]string) *corev1.ConfigMap {
	return &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: name, Labels: lbls}}
}

func namespaceNames(namespaces []corev1.Namespace) []string {
	names := make([]string, 0, len(namespaces))
	for _, ns := range namespaces {
		names = append(names, ns.Name)
	}

	return names
}

func objectNames(objects []metav1.Object) []string {
	names := make([]string, 0, len(objects))
	for _, obj := range objects {
		names = append(names, obj.GetName())
	}

	return names
}

func configMapKeys(configMaps []*corev1.ConfigMap) []string {
	names := make([]string, 0, len(configMaps))
	for _, configMap := range configMaps {
		names = append(names, configMap.Namespace+"/"+configMap.Name)
	}

	return names
}
