package agentbuild

import (
	"context"
	"reflect"
	"sort"
	"testing"
	"time"

	vmoperatorv1beta1 "github.com/VictoriaMetrics/operator/api/operator/v1beta1"
	ec2v1alpha1 "github.com/crossplane-contrib/provider-aws/apis/ec2/v1alpha1"
	eksv1alpha1 "github.com/crossplane-contrib/provider-aws/apis/eks/manualv1alpha1"
	gkecontainerv1beta1 "github.com/crossplane-contrib/provider-gcp/apis/container/v1beta1"
	"github.com/stretchr/testify/assert"
	corev1 "k8s.io/api/core/v1"
	rbacv1 "k8s.io/api/rbac/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/observability/agent-control-server/pkg/testkits"
)

type deleteTestCase struct {
	name           string
	setupResources []client.Object
	target         string
	namespace      string
	wantErr        bool
	verify         func(t *testing.T, cli client.Client)
}

func runDeleteTests(
	t *testing.T,
	deleteFunc func(ctx context.Context, cli client.Client, name, namespace string) error,
	cases []deleteTestCase,
) {
	for _, tt := range cases {
		t.Run(tt.name, func(t *testing.T) {
			fakeCli := testkits.BuildClient(tt.setupResources...)
			err := deleteFunc(context.Background(), fakeCli, tt.target, tt.namespace)
			if tt.wantErr {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
			}
			if tt.verify != nil {
				tt.verify(t, fakeCli)
			}
		})
	}
}

type deleteWithLabelsTestCase struct {
	name           string
	setupResources []client.Object
	namespace      string
	labels         map[string]string
	wantErr        bool
	verify         func(t *testing.T, cli client.Client)
}

func runDeleteWithLabelsTests(
	t *testing.T,
	deleteFunc func(ctx context.Context, cli client.Client, namespace string, labels map[string]string) error,
	cases []deleteWithLabelsTestCase,
) {
	for _, tt := range cases {
		t.Run(tt.name, func(t *testing.T) {
			fakeCli := testkits.BuildClient(tt.setupResources...)
			err := deleteFunc(context.Background(), fakeCli, tt.namespace, tt.labels)
			if tt.wantErr {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
			}
			if tt.verify != nil {
				tt.verify(t, fakeCli)
			}
		})
	}
}

func getStandardDeleteTestCases[T client.Object](
	newObj func(name, namespace string) T,
	getFunc func(ctx context.Context, cli client.Client, name, namespace string) error,
	namespace string,
) []deleteTestCase {
	return []deleteTestCase{
		{
			name:           "delete existing resource",
			setupResources: []client.Object{newObj("test-resource", namespace)},
			target:         "test-resource",
			namespace:      namespace,
			wantErr:        false,
			verify: func(t *testing.T, cli client.Client) {
				err := getFunc(context.Background(), cli, "test-resource", namespace)
				assert.True(t, apierrors.IsNotFound(err))
			},
		},
		{
			name:           "delete non-existent resource",
			setupResources: []client.Object{},
			target:         "non-existent",
			namespace:      namespace,
			wantErr:        false,
		},
	}
}

func getStandardDeleteWithLabelsTestCases[T client.ObjectList](
	createObj func(name, namespace string, labels map[string]string) client.Object,
	namespace string,
	listFunc func(ctx context.Context, cli client.Client, namespace string, labels map[string]string) (T, error),
) []deleteWithLabelsTestCase {
	return []deleteWithLabelsTestCase{
		{
			name:           "delete resources with matching labels",
			setupResources: []client.Object{createObj("resource-1", namespace, map[string]string{"app": "test"})},
			namespace:      namespace,
			labels:         map[string]string{"app": "test"},
			wantErr:        false,
			verify: func(t *testing.T, cli client.Client) {
				list, err := listFunc(context.Background(), cli, namespace, map[string]string{"app": "test"})
				assert.NoError(t, err)
				assert.Zero(t, getListLen(list))
			},
		},
		{
			name:           "no resources with matching labels",
			setupResources: []client.Object{createObj("resource-1", namespace, map[string]string{"app": "other"})},
			namespace:      namespace,
			labels:         map[string]string{"app": "test"},
			wantErr:        false,
			verify: func(t *testing.T, cli client.Client) {
				list, err := listFunc(context.Background(), cli, namespace, map[string]string{"app": "other"})
				assert.NoError(t, err)
				assert.Equal(t, 1, getListLen(list))
			},
		},
		{
			name:           "empty namespace",
			setupResources: []client.Object{},
			namespace:      namespace,
			labels:         map[string]string{"app": "test"},
			wantErr:        false,
		},
		{
			name: "delete multiple resources with matching labels",
			setupResources: []client.Object{
				createObj("resource-1", namespace, map[string]string{"app": "test"}),
				createObj("resource-2", namespace, map[string]string{"app": "test"}),
			},
			namespace: namespace,
			labels:    map[string]string{"app": "test"},
			wantErr:   false,
			verify: func(t *testing.T, cli client.Client) {
				list, err := listFunc(context.Background(), cli, namespace, map[string]string{"app": "test"})
				assert.NoError(t, err)
				assert.Zero(t, getListLen(list))
			},
		},
	}
}

func getListLen(list client.ObjectList) int {
	return reflect.ValueOf(list).Elem().FieldByName("Items").Len()
}

func TestDeleteVMAgent(t *testing.T) {
	runDeleteTests(t, DeleteVMAgent, getStandardDeleteTestCases(
		func(name, namespace string) *vmoperatorv1beta1.VMAgent {
			return &vmoperatorv1beta1.VMAgent{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace}}
		},
		func(ctx context.Context, cli client.Client, name, namespace string) error {
			return cli.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, &vmoperatorv1beta1.VMAgent{})
		},
		"default",
	))
}

func TestDeleteVMPodScrape(t *testing.T) {
	runDeleteTests(t, DeleteVMPodScrape, getStandardDeleteTestCases(
		func(name, namespace string) *vmoperatorv1beta1.VMPodScrape {
			return &vmoperatorv1beta1.VMPodScrape{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace}}
		},
		func(ctx context.Context, cli client.Client, name, namespace string) error {
			return cli.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, &vmoperatorv1beta1.VMPodScrape{})
		},
		"default",
	))
}

func TestDeleteVMServiceScrape(t *testing.T) {
	runDeleteTests(t, DeleteVMServiceScrape, getStandardDeleteTestCases(
		func(name, namespace string) *vmoperatorv1beta1.VMServiceScrape {
			return &vmoperatorv1beta1.VMServiceScrape{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace}}
		},
		func(ctx context.Context, cli client.Client, name, namespace string) error {
			return cli.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, &vmoperatorv1beta1.VMServiceScrape{})
		},
		"default",
	))
}

func TestDeleteRole(t *testing.T) {
	runDeleteTests(t, DeleteRole, getStandardDeleteTestCases(
		func(name, namespace string) *rbacv1.Role {
			return &rbacv1.Role{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace}}
		},
		func(ctx context.Context, cli client.Client, name, namespace string) error {
			return cli.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, &rbacv1.Role{})
		},
		"default",
	))
}

func TestDeleteRoleBinding(t *testing.T) {
	runDeleteTests(t, DeleteRoleBinding, getStandardDeleteTestCases(
		func(name, namespace string) *rbacv1.RoleBinding {
			return &rbacv1.RoleBinding{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace}}
		},
		func(ctx context.Context, cli client.Client, name, namespace string) error {
			return cli.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, &rbacv1.RoleBinding{})
		},
		"default",
	))
}

func TestDeletePVC(t *testing.T) {
	runDeleteTests(t, DeletePVC, getStandardDeleteTestCases(
		func(name, namespace string) *corev1.PersistentVolumeClaim {
			return &corev1.PersistentVolumeClaim{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace}}
		},
		func(ctx context.Context, cli client.Client, name, namespace string) error {
			return cli.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, &corev1.PersistentVolumeClaim{})
		},
		"default",
	))
}

func TestDeletePVCWithLabels(t *testing.T) {
	runDeleteWithLabelsTests(t, func(ctx context.Context, cli client.Client, namespace string, labels map[string]string) error {
		return DeletePVCWithLabels(ctx, cli, labels, namespace)
	}, getStandardDeleteWithLabelsTestCases(
		func(name, namespace string, labels map[string]string) client.Object {
			return &corev1.PersistentVolumeClaim{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace, Labels: labels}}
		},
		"default",
		func(ctx context.Context, cli client.Client, namespace string, labels map[string]string) (*corev1.PersistentVolumeClaimList, error) {
			var list corev1.PersistentVolumeClaimList
			err := cli.List(ctx, &list, client.InNamespace(namespace), client.MatchingLabels(labels))
			return &list, err
		},
	))
}

func TestDeleteNodeGroup(t *testing.T) {
	runDeleteTests(t, func(ctx context.Context, cli client.Client, name, namespace string) error {
		return DeleteNodeGroup(ctx, cli, name)
	}, getStandardDeleteTestCases(
		func(name, namespace string) *eksv1alpha1.NodeGroup {
			return &eksv1alpha1.NodeGroup{ObjectMeta: metav1.ObjectMeta{Name: name}}
		},
		func(ctx context.Context, cli client.Client, name, namespace string) error {
			return cli.Get(ctx, types.NamespacedName{Name: name}, &eksv1alpha1.NodeGroup{})
		},
		"",
	))
}

func TestDeleteNodePool(t *testing.T) {
	runDeleteTests(t, func(ctx context.Context, cli client.Client, name, namespace string) error {
		return DeleteNodePool(ctx, cli, name)
	}, getStandardDeleteTestCases(
		func(name, namespace string) *gkecontainerv1beta1.NodePool {
			return &gkecontainerv1beta1.NodePool{ObjectMeta: metav1.ObjectMeta{Name: name}}
		},
		func(ctx context.Context, cli client.Client, name, namespace string) error {
			return cli.Get(ctx, types.NamespacedName{Name: name}, &gkecontainerv1beta1.NodePool{})
		},
		"",
	))
}

func TestDeleteNodeGroupWithLabels(t *testing.T) {
	runDeleteWithLabelsTests(t, func(ctx context.Context, cli client.Client, namespace string, labels map[string]string) error {
		_, err := DeleteNodeGroupWithLabels(ctx, cli, labels)
		return err
	}, getStandardDeleteWithLabelsTestCases(
		func(name, namespace string, labels map[string]string) client.Object {
			return &eksv1alpha1.NodeGroup{ObjectMeta: metav1.ObjectMeta{Name: name, Labels: labels}}
		},
		"",
		func(ctx context.Context, cli client.Client, namespace string, labels map[string]string) (*eksv1alpha1.NodeGroupList, error) {
			var list eksv1alpha1.NodeGroupList
			err := cli.List(ctx, &list, client.MatchingLabels(labels))
			return &list, err
		},
	))
}

func TestDeleteNodeGroupWithLabelsExcludeNames(t *testing.T) {
	labels := map[string]string{"app": "test"}
	keep := &eksv1alpha1.NodeGroup{ObjectMeta: metav1.ObjectMeta{Name: "ng-keep", Labels: labels}}
	stale := &eksv1alpha1.NodeGroup{ObjectMeta: metav1.ObjectMeta{Name: "ng-stale", Labels: labels}}
	deleting := &eksv1alpha1.NodeGroup{
		ObjectMeta: metav1.ObjectMeta{
			Name:              "ng-deleting",
			Labels:            labels,
			DeletionTimestamp: &metav1.Time{Time: time.Now()},
			Finalizers:        []string{"fake-finalizer"},
		},
	}
	cli := testkits.BuildClient(keep, stale, deleting)

	deleted, err := DeleteNodeGroupWithLabels(context.Background(), cli, labels, "ng-keep")
	assert.NoError(t, err)
	assert.Equal(t, []string{"ng-stale"}, deleted)

	var got eksv1alpha1.NodeGroupList
	assert.NoError(t, cli.List(context.Background(), &got, client.MatchingLabels(labels)))
	names := make([]string, 0, len(got.Items))
	for _, ng := range got.Items {
		names = append(names, ng.Name)
	}
	sort.Strings(names)
	assert.Equal(t, []string{"ng-deleting", "ng-keep"}, names)
}

func TestDeleteNodePoolWithLabels(t *testing.T) {
	runDeleteWithLabelsTests(t, func(ctx context.Context, cli client.Client, namespace string, labels map[string]string) error {
		return DeleteNodePoolWithLabels(ctx, cli, labels)
	}, getStandardDeleteWithLabelsTestCases(
		func(name, namespace string, labels map[string]string) client.Object {
			return &gkecontainerv1beta1.NodePool{ObjectMeta: metav1.ObjectMeta{Name: name, Labels: labels}}
		},
		"",
		func(ctx context.Context, cli client.Client, namespace string, labels map[string]string) (*gkecontainerv1beta1.NodePoolList, error) {
			var list gkecontainerv1beta1.NodePoolList
			err := cli.List(ctx, &list, client.MatchingLabels(labels))
			return &list, err
		},
	))
}

func TestDeleteAwsVPCEndpoint(t *testing.T) {
	runDeleteTests(t, func(ctx context.Context, cli client.Client, name, namespace string) error {
		return DeleteAwsVPCEndpoint(ctx, cli, name)
	}, getStandardDeleteTestCases(
		func(name, namespace string) *ec2v1alpha1.VPCEndpoint {
			return &ec2v1alpha1.VPCEndpoint{ObjectMeta: metav1.ObjectMeta{Name: name}}
		},
		func(ctx context.Context, cli client.Client, name, namespace string) error {
			return cli.Get(ctx, types.NamespacedName{Name: name}, &ec2v1alpha1.VPCEndpoint{})
		},
		"",
	))
}

func TestDeleteObject(t *testing.T) {
	tests := []struct {
		name    string
		obj     client.Object
		wantErr bool
	}{
		{
			name: "delete existing object",
			obj: &corev1.ConfigMap{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-cm",
					Namespace: "default",
				},
			},
			wantErr: false,
		},
		{
			name: "delete non-existent object returns no error due to IgnoreNotFound",
			obj: &corev1.ConfigMap{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "non-existent",
					Namespace: "default",
				},
			},
			wantErr: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			fakeCli := testkits.BuildClient()
			err := DeleteObject(context.Background(), fakeCli, tt.obj)
			if tt.wantErr {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
			}
		})
	}
}
