package agentbuild

import (
	"context"
	"errors"
	"testing"

	vmoperatorv1beta1 "github.com/VictoriaMetrics/operator/api/operator/v1beta1"
	"github.com/stretchr/testify/assert"
	rbacv1 "k8s.io/api/rbac/v1"
	schedulingv1 "k8s.io/api/scheduling/v1"
	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 genericCreateOrUpdateTestCase[T client.Object] struct {
	name           string
	setupResources []client.Object
	builder        func() (T, error)
	wantErr        bool
	errMsg         string
	verify         func(t *testing.T, cli client.Client)
}

func runGenericCreateOrUpdateTests[T client.Object](
	t *testing.T,
	createOrUpdateFunc func(context.Context, client.Client, func() (T, error)) error,
	cases []genericCreateOrUpdateTestCase[T],
) {
	for _, tt := range cases {
		t.Run(tt.name, func(t *testing.T) {
			fakeCli := testkits.BuildClient(tt.setupResources...)
			err := createOrUpdateFunc(context.Background(), fakeCli, tt.builder)
			if tt.wantErr {
				assert.Error(t, err)
				assert.Contains(t, err.Error(), tt.errMsg)
			} else {
				assert.NoError(t, err)
			}
			if tt.verify != nil {
				tt.verify(t, fakeCli)
			}
		})
	}
}

func getStandardCreateOrUpdateTestCases[T client.Object](
	newObj func() T,
	updateExistingObj func() T,
	updateNewObj func() T,
	verifyCreate func(t *testing.T, cli client.Client),
	verifyUpdate func(t *testing.T, cli client.Client),
) []genericCreateOrUpdateTestCase[T] {
	return []genericCreateOrUpdateTestCase[T]{
		{
			name:    "builder returns error",
			builder: func() (T, error) { var zero T; return zero, errors.New("builder error") },
			wantErr: true,
			errMsg:  "builder error",
		},
		{
			name:           "create new resource",
			setupResources: []client.Object{},
			builder:        func() (T, error) { return newObj(), nil },
			wantErr:        false,
			verify:         verifyCreate,
		},
		{
			name:           "update existing resource",
			setupResources: []client.Object{updateExistingObj()},
			builder:        func() (T, error) { return updateNewObj(), nil },
			wantErr:        false,
			verify:         verifyUpdate,
		},
	}
}

func TestCreateOrUpdateVmagent(t *testing.T) {
	cases := getStandardCreateOrUpdateTestCases(
		func() *vmoperatorv1beta1.VMAgent {
			return &vmoperatorv1beta1.VMAgent{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-vmagent",
					Namespace: "default",
					Labels:    map[string]string{"app": "test"},
				},
			}
		},
		func() *vmoperatorv1beta1.VMAgent {
			return &vmoperatorv1beta1.VMAgent{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-vmagent",
					Namespace: "default",
					Labels:    map[string]string{"old": "label"},
				},
			}
		},
		func() *vmoperatorv1beta1.VMAgent {
			return &vmoperatorv1beta1.VMAgent{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-vmagent",
					Namespace: "default",
					Labels:    map[string]string{"new": "label"},
				},
			}
		},
		func(t *testing.T, cli client.Client) {
			vmagent := &vmoperatorv1beta1.VMAgent{}
			err := cli.Get(context.Background(), types.NamespacedName{Name: "test-vmagent", Namespace: "default"}, vmagent)
			assert.NoError(t, err)
			assert.Equal(t, "test", vmagent.Labels["app"])
		},
		func(t *testing.T, cli client.Client) {
			vmagent := &vmoperatorv1beta1.VMAgent{}
			err := cli.Get(context.Background(), types.NamespacedName{Name: "test-vmagent", Namespace: "default"}, vmagent)
			assert.NoError(t, err)
			assert.Equal(t, "label", vmagent.Labels["new"])
			assert.Equal(t, "label", vmagent.Labels["old"])
		},
	)
	runGenericCreateOrUpdateTests(t, CreateOrUpdateVMAgent, cases)
}

func TestCreateOrUpdateVMPodScrape(t *testing.T) {
	cases := getStandardCreateOrUpdateTestCases(
		func() *vmoperatorv1beta1.VMPodScrape {
			return &vmoperatorv1beta1.VMPodScrape{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-scrape",
					Namespace: "default",
					Labels:    map[string]string{"app": "test"},
				},
			}
		},
		func() *vmoperatorv1beta1.VMPodScrape {
			return &vmoperatorv1beta1.VMPodScrape{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-scrape",
					Namespace: "default",
					Labels:    map[string]string{"old": "label"},
				},
			}
		},
		func() *vmoperatorv1beta1.VMPodScrape {
			return &vmoperatorv1beta1.VMPodScrape{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-scrape",
					Namespace: "default",
					Labels:    map[string]string{"new": "label"},
				},
			}
		},
		func(t *testing.T, cli client.Client) {
			scrape := &vmoperatorv1beta1.VMPodScrape{}
			err := cli.Get(context.Background(), types.NamespacedName{Name: "test-scrape", Namespace: "default"}, scrape)
			assert.NoError(t, err)
			assert.Equal(t, "test", scrape.Labels["app"])
		},
		func(t *testing.T, cli client.Client) {
			scrape := &vmoperatorv1beta1.VMPodScrape{}
			err := cli.Get(context.Background(), types.NamespacedName{Name: "test-scrape", Namespace: "default"}, scrape)
			assert.NoError(t, err)
			assert.Equal(t, "label", scrape.Labels["new"])
			assert.Equal(t, "label", scrape.Labels["old"])
		},
	)
	runGenericCreateOrUpdateTests(t, CreateOrUpdateVMPodScrape, cases)
}

func TestCreateOrUpdateVMServiceScrape(t *testing.T) {
	cases := getStandardCreateOrUpdateTestCases(
		func() *vmoperatorv1beta1.VMServiceScrape {
			return &vmoperatorv1beta1.VMServiceScrape{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-svc-scrape",
					Namespace: "default",
					Labels:    map[string]string{"app": "test"},
				},
			}
		},
		func() *vmoperatorv1beta1.VMServiceScrape {
			return &vmoperatorv1beta1.VMServiceScrape{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-svc-scrape",
					Namespace: "default",
					Labels:    map[string]string{"old": "label"},
				},
			}
		},
		func() *vmoperatorv1beta1.VMServiceScrape {
			return &vmoperatorv1beta1.VMServiceScrape{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-svc-scrape",
					Namespace: "default",
					Labels:    map[string]string{"new": "label"},
				},
			}
		},
		func(t *testing.T, cli client.Client) {
			scrape := &vmoperatorv1beta1.VMServiceScrape{}
			err := cli.Get(context.Background(), types.NamespacedName{Name: "test-svc-scrape", Namespace: "default"}, scrape)
			assert.NoError(t, err)
			assert.Equal(t, "test", scrape.Labels["app"])
		},
		func(t *testing.T, cli client.Client) {
			scrape := &vmoperatorv1beta1.VMServiceScrape{}
			err := cli.Get(context.Background(), types.NamespacedName{Name: "test-svc-scrape", Namespace: "default"}, scrape)
			assert.NoError(t, err)
			assert.Equal(t, "label", scrape.Labels["new"])
			assert.Equal(t, "label", scrape.Labels["old"])
		},
	)
	runGenericCreateOrUpdateTests(t, CreateOrUpdateVMServiceScrape, cases)
}

func TestCreateOrUpdateRole(t *testing.T) {
	cases := getStandardCreateOrUpdateTestCases(
		func() *rbacv1.Role {
			return &rbacv1.Role{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-role",
					Namespace: "default",
					Labels:    map[string]string{"app": "test"},
				},
				Rules: []rbacv1.PolicyRule{
					{
						APIGroups: []string{""},
						Resources: []string{"pods"},
						Verbs:     []string{"get", "list"},
					},
				},
			}
		},
		func() *rbacv1.Role {
			return &rbacv1.Role{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-role",
					Namespace: "default",
					Labels:    map[string]string{"old": "label"},
				},
				Rules: []rbacv1.PolicyRule{
					{
						APIGroups: []string{""},
						Resources: []string{"pods"},
						Verbs:     []string{"get"},
					},
				},
			}
		},
		func() *rbacv1.Role {
			return &rbacv1.Role{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-role",
					Namespace: "default",
					Labels:    map[string]string{"new": "label"},
				},
				Rules: []rbacv1.PolicyRule{
					{
						APIGroups: []string{""},
						Resources: []string{"pods"},
						Verbs:     []string{"get", "list", "watch"},
					},
				},
			}
		},
		func(t *testing.T, cli client.Client) {
			role := &rbacv1.Role{}
			err := cli.Get(context.Background(), types.NamespacedName{Name: "test-role", Namespace: "default"}, role)
			assert.NoError(t, err)
			assert.Equal(t, "pods", role.Rules[0].Resources[0])
			assert.Equal(t, "test", role.Labels["app"])
		},
		func(t *testing.T, cli client.Client) {
			role := &rbacv1.Role{}
			err := cli.Get(context.Background(), types.NamespacedName{Name: "test-role", Namespace: "default"}, role)
			assert.NoError(t, err)
			assert.Equal(t, 3, len(role.Rules[0].Verbs))
			assert.Equal(t, "label", role.Labels["new"])
			assert.Equal(t, "label", role.Labels["old"])
		},
	)
	runGenericCreateOrUpdateTests(t, CreateOrUpdateRole, cases)
}

func TestCreateOrUpdateRoleBinding(t *testing.T) {
	cases := getStandardCreateOrUpdateTestCases(
		func() *rbacv1.RoleBinding {
			return &rbacv1.RoleBinding{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-rolebinding",
					Namespace: "default",
					Labels:    map[string]string{"app": "test"},
				},
				Subjects: []rbacv1.Subject{
					{
						Kind:      "ServiceAccount",
						Name:      "test-sa",
						Namespace: "default",
					},
				},
				RoleRef: rbacv1.RoleRef{
					Kind: "Role",
					Name: "test-role",
				},
			}
		},
		func() *rbacv1.RoleBinding {
			return &rbacv1.RoleBinding{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-rolebinding",
					Namespace: "default",
					Labels:    map[string]string{"old": "label"},
				},
				Subjects: []rbacv1.Subject{
					{
						Kind:      "ServiceAccount",
						Name:      "old-sa",
						Namespace: "default",
					},
				},
				RoleRef: rbacv1.RoleRef{
					Kind: "Role",
					Name: "old-role",
				},
			}
		},
		func() *rbacv1.RoleBinding {
			return &rbacv1.RoleBinding{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-rolebinding",
					Namespace: "default",
					Labels:    map[string]string{"new": "label"},
				},
				Subjects: []rbacv1.Subject{
					{
						Kind:      "ServiceAccount",
						Name:      "new-sa",
						Namespace: "default",
					},
				},
				RoleRef: rbacv1.RoleRef{
					Kind: "Role",
					Name: "new-role",
				},
			}
		},
		func(t *testing.T, cli client.Client) {
			binding := &rbacv1.RoleBinding{}
			err := cli.Get(context.Background(), types.NamespacedName{Name: "test-rolebinding", Namespace: "default"}, binding)
			assert.NoError(t, err)
			assert.Equal(t, "test-sa", binding.Subjects[0].Name)
			assert.Equal(t, "test-role", binding.RoleRef.Name)
			assert.Equal(t, "test", binding.Labels["app"])
		},
		func(t *testing.T, cli client.Client) {
			binding := &rbacv1.RoleBinding{}
			err := cli.Get(context.Background(), types.NamespacedName{Name: "test-rolebinding", Namespace: "default"}, binding)
			assert.NoError(t, err)
			assert.Equal(t, "new-sa", binding.Subjects[0].Name)
			assert.Equal(t, "new-role", binding.RoleRef.Name)
			assert.Equal(t, "label", binding.Labels["new"])
			assert.Equal(t, "label", binding.Labels["old"])
		},
	)
	runGenericCreateOrUpdateTests(t, CreateOrUpdateRoleBinding, cases)
}

type priorityClassTestCase struct {
	name           string
	setupResources []client.Object
	priorityName   string
	value          int32
	wantErr        bool
	errMsg         string
	verify         func(t *testing.T, cli client.Client)
}

func runPriorityClassTests(
	t *testing.T,
	createFunc func(context.Context, client.Client, string, int32) error,
	cases []priorityClassTestCase,
) {
	for _, tt := range cases {
		t.Run(tt.name, func(t *testing.T) {
			fakeCli := testkits.BuildClient(tt.setupResources...)
			err := createFunc(context.Background(), fakeCli, tt.priorityName, tt.value)
			if tt.wantErr {
				assert.Error(t, err)
				assert.Contains(t, err.Error(), tt.errMsg)
			} else {
				assert.NoError(t, err)
			}
			if tt.verify != nil {
				tt.verify(t, fakeCli)
			}
		})
	}
}

func TestCreateAgentPriorityClassWithValue(t *testing.T) {
	cases := []priorityClassTestCase{
		{
			name:           "create new priority class with custom value",
			setupResources: []client.Object{},
			priorityName:   "test-priority",
			value:          500000000,
			wantErr:        false,
			verify: func(t *testing.T, cli client.Client) {
				pc := &schedulingv1.PriorityClass{}
				err := cli.Get(context.Background(), types.NamespacedName{Name: "test-priority"}, pc)
				assert.NoError(t, err)
				assert.Equal(t, int32(500000000), pc.Value)
				assert.Equal(t, false, pc.GlobalDefault)
				assert.Equal(t, "Priority class for o11y agent", pc.Description)
			},
		},
		{
			name: "update existing priority class with custom value (no changes applied)",
			setupResources: []client.Object{
				&schedulingv1.PriorityClass{
					ObjectMeta: metav1.ObjectMeta{
						Name: "test-priority",
					},
					Value:         100,
					GlobalDefault: true,
				},
			},
			priorityName: "test-priority",
			value:        999999999,
			wantErr:      false,
			verify: func(t *testing.T, cli client.Client) {
				pc := &schedulingv1.PriorityClass{}
				err := cli.Get(context.Background(), types.NamespacedName{Name: "test-priority"}, pc)
				assert.NoError(t, err)
				assert.Equal(t, int32(100), pc.Value)
				assert.Equal(t, true, pc.GlobalDefault)
			},
		},
	}
	runPriorityClassTests(t, CreateAgentPriorityClassWithValue, cases)
}
