package k8s

import (
	"context"
	"errors"
	"testing"

	. "github.com/onsi/gomega"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

// Using ConfigMap as a simple test resource since it's a standard Kubernetes type
// and the reconciliation logic is generic and works for any client.Object
func newTestConfigMap(name string, labels map[string]string) *corev1.ConfigMap {
	return &corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:   name,
			Labels: labels,
		},
		Data: map[string]string{
			"test": "data",
		},
	}
}

func TestListOpts_ReconcileByDesiredNames(t *testing.T) {
	ctx := context.Background()
	ownerLabel := map[string]string{"owner": "test-owner"}

	tests := []struct {
		name              string
		existingResources []*corev1.ConfigMap
		desiredNames      map[string]bool
		matchLabels       map[string]string
		expectedRemain    []string // Names of resources that should remain
		wantErr           bool
	}{
		{
			name: "delete stale resources",
			existingResources: []*corev1.ConfigMap{
				newTestConfigMap("resource-a", ownerLabel),
				newTestConfigMap("resource-b", ownerLabel),
				newTestConfigMap("resource-c", ownerLabel),
			},
			desiredNames: map[string]bool{
				"resource-a": true,
				"resource-b": true,
			},
			matchLabels:    ownerLabel,
			expectedRemain: []string{"resource-a", "resource-b"},
			wantErr:        false,
		},
		{
			name: "no cleanup needed - all resources in desired set",
			existingResources: []*corev1.ConfigMap{
				newTestConfigMap("resource-a", ownerLabel),
				newTestConfigMap("resource-b", ownerLabel),
			},
			desiredNames: map[string]bool{
				"resource-a": true,
				"resource-b": true,
			},
			matchLabels:    ownerLabel,
			expectedRemain: []string{"resource-a", "resource-b"},
			wantErr:        false,
		},
		{
			name: "cleanup all resources - empty desired set",
			existingResources: []*corev1.ConfigMap{
				newTestConfigMap("resource-a", ownerLabel),
				newTestConfigMap("resource-b", ownerLabel),
			},
			desiredNames:   map[string]bool{},
			matchLabels:    ownerLabel,
			expectedRemain: []string{},
			wantErr:        false,
		},
		{
			name: "label selector filters correctly",
			existingResources: []*corev1.ConfigMap{
				newTestConfigMap("resource-a", ownerLabel),
				newTestConfigMap("resource-b", ownerLabel),
				newTestConfigMap("resource-c", map[string]string{"owner": "other-owner"}),
			},
			desiredNames: map[string]bool{
				"resource-a": true,
			},
			matchLabels:    ownerLabel,
			expectedRemain: []string{"resource-a", "resource-c"}, // resource-c should remain (different label)
			wantErr:        false,
		},
		{
			name: "mixed scenario - some new, some existing, some deleted",
			existingResources: []*corev1.ConfigMap{
				newTestConfigMap("resource-old-1", ownerLabel),
				newTestConfigMap("resource-old-2", ownerLabel),
				newTestConfigMap("resource-keep", ownerLabel),
			},
			desiredNames: map[string]bool{
				"resource-keep": true,
				"resource-new":  true, // This would be created separately, not tested here
			},
			matchLabels:    ownerLabel,
			expectedRemain: []string{"resource-keep"},
			wantErr:        false,
		},
		{
			name:              "no existing resources",
			existingResources: []*corev1.ConfigMap{},
			desiredNames:      map[string]bool{"resource-a": true},
			matchLabels:       ownerLabel,
			expectedRemain:    []string{},
			wantErr:           false,
		},
	}

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

			// Setup fake client with existing resources
			s := runtime.NewScheme()
			g.Expect(corev1.AddToScheme(s)).Should(Succeed())

			objs := make([]client.Object, 0, len(tt.existingResources))
			for _, resource := range tt.existingResources {
				objs = append(objs, resource)
			}
			cli := fake.NewClientBuilder().WithScheme(s).WithObjects(objs...).Build()

			// Create ListOpts instance
			configMapList := &corev1.ConfigMapList{}
			opts := ListOpts[*corev1.ConfigMap]{
				ObjectList: configMapList,
				Unpack: func() []*corev1.ConfigMap {
					return MapPtr(configMapList.Items)
				},
			}

			// Execute reconciliation
			err := opts.ReconcileByDesiredNames(
				ctx,
				cli,
				tt.desiredNames,
				client.MatchingLabels(tt.matchLabels),
			)

			// Verify error expectation
			if tt.wantErr {
				g.Expect(err).Should(HaveOccurred())
				return
			}
			g.Expect(err).ShouldNot(HaveOccurred())

			// Verify remaining resources
			remainingList := &corev1.ConfigMapList{}
			g.Expect(cli.List(ctx, remainingList)).Should(Succeed())

			remainingNames := make([]string, 0, len(remainingList.Items))
			for _, resource := range remainingList.Items {
				remainingNames = append(remainingNames, resource.Name)
			}

			g.Expect(remainingNames).Should(ConsistOf(tt.expectedRemain))
		})
	}
}

func TestListOpts_ReconcileByDesiredNamesWithDelete(t *testing.T) {
	g := NewWithT(t)
	ctx := context.Background()

	s := runtime.NewScheme()
	g.Expect(corev1.AddToScheme(s)).Should(Succeed())

	ownerLabel := map[string]string{"owner": "test-owner"}
	cli := fake.NewClientBuilder().WithScheme(s).WithObjects(
		newTestConfigMap("resource-a", ownerLabel),
		newTestConfigMap("resource-b", ownerLabel),
		newTestConfigMap("resource-c", ownerLabel),
		newTestConfigMap("resource-other", map[string]string{"owner": "other-owner"}),
	).Build()

	configMapList := &corev1.ConfigMapList{}
	opts := ListOpts[*corev1.ConfigMap]{
		ObjectList: configMapList,
		Unpack: func() []*corev1.ConfigMap {
			return MapPtr(configMapList.Items)
		},
	}

	deletedNames := make([]string, 0, 2)
	err := opts.ReconcileByDesiredNamesWithDelete(
		ctx,
		cli,
		map[string]bool{"resource-a": true},
		func(ctx context.Context, cli client.Client, obj client.Object) error {
			deletedNames = append(deletedNames, obj.GetName())
			return DeleteObject(ctx, cli, obj)
		},
		client.MatchingLabels(ownerLabel),
	)
	g.Expect(err).ShouldNot(HaveOccurred())
	g.Expect(deletedNames).Should(ConsistOf("resource-b", "resource-c"))

	remainingList := &corev1.ConfigMapList{}
	g.Expect(cli.List(ctx, remainingList)).Should(Succeed())
	remainingNames := make([]string, 0, len(remainingList.Items))
	for _, resource := range remainingList.Items {
		remainingNames = append(remainingNames, resource.Name)
	}
	g.Expect(remainingNames).Should(ConsistOf("resource-a", "resource-other"))
}

func TestListOpts_ReconcileByDesiredNamesWithDelete_ReturnsDeleteError(t *testing.T) {
	g := NewWithT(t)
	ctx := context.Background()

	s := runtime.NewScheme()
	g.Expect(corev1.AddToScheme(s)).Should(Succeed())

	ownerLabel := map[string]string{"owner": "test-owner"}
	cli := fake.NewClientBuilder().WithScheme(s).WithObjects(
		newTestConfigMap("resource-a", ownerLabel),
		newTestConfigMap("resource-b", ownerLabel),
	).Build()

	configMapList := &corev1.ConfigMapList{}
	opts := ListOpts[*corev1.ConfigMap]{
		ObjectList: configMapList,
		Unpack: func() []*corev1.ConfigMap {
			return MapPtr(configMapList.Items)
		},
	}

	deleteErr := errors.New("delete failed")
	err := opts.ReconcileByDesiredNamesWithDelete(
		ctx,
		cli,
		map[string]bool{"resource-a": true},
		func(context.Context, client.Client, client.Object) error {
			return deleteErr
		},
		client.MatchingLabels(ownerLabel),
	)
	g.Expect(err).Should(MatchError(deleteErr))
}

// TestListOpts_ReconcileByDesiredNames_Integration tests the integration with real List and Delete operations
// This test simulates a real scenario where security group rules (represented by ConfigMaps) need to be reconciled
func TestListOpts_ReconcileByDesiredNames_Integration(t *testing.T) {
	g := NewWithT(t)
	ctx := context.Background()

	// Setup
	s := runtime.NewScheme()
	g.Expect(corev1.AddToScheme(s)).Should(Succeed())

	ownerLabel := map[string]string{"sg-ref": "test-sg-123"}
	cli := fake.NewClientBuilder().WithScheme(s).Build()

	// Create initial resources simulating security group rules for two IPs and a deny-all rule
	initialResources := []*corev1.ConfigMap{
		newTestConfigMap("rule-ip-1-2-3-4-80", ownerLabel),
		newTestConfigMap("rule-ip-1-2-3-4-443", ownerLabel),
		newTestConfigMap("rule-ip-5-6-7-8-80", ownerLabel),
		newTestConfigMap("rule-ip-5-6-7-8-443", ownerLabel),
		newTestConfigMap("deny-all", ownerLabel),
	}

	for _, resource := range initialResources {
		g.Expect(cli.Create(ctx, resource)).Should(Succeed())
	}

	// Verify initial state
	resourceList := &corev1.ConfigMapList{}
	g.Expect(cli.List(ctx, resourceList, client.MatchingLabels(ownerLabel))).Should(Succeed())
	g.Expect(resourceList.Items).Should(HaveLen(5))

	// Simulate configuration change: remove IP 5.6.7.8, keep 1.2.3.4 and deny-all
	desiredNames := map[string]bool{
		"rule-ip-1-2-3-4-80":  true,
		"rule-ip-1-2-3-4-443": true,
		"deny-all":            true,
	}

	// Execute reconciliation
	opts := ListOpts[*corev1.ConfigMap]{
		ObjectList: resourceList,
		Unpack: func() []*corev1.ConfigMap {
			return MapPtr(resourceList.Items)
		},
	}

	err := opts.ReconcileByDesiredNames(ctx, cli, desiredNames, client.MatchingLabels(ownerLabel))
	g.Expect(err).ShouldNot(HaveOccurred())

	// Verify final state - should have removed rule-ip-5-6-7-8-80 and rule-ip-5-6-7-8-443
	finalList := &corev1.ConfigMapList{}
	g.Expect(cli.List(ctx, finalList, client.MatchingLabels(ownerLabel))).Should(Succeed())
	g.Expect(finalList.Items).Should(HaveLen(3))

	finalNames := make([]string, 0, len(finalList.Items))
	for _, resource := range finalList.Items {
		finalNames = append(finalNames, resource.Name)
	}
	g.Expect(finalNames).Should(ConsistOf("rule-ip-1-2-3-4-80", "rule-ip-1-2-3-4-443", "deny-all"))
}
