package task

import (
	"context"
	"testing"
	"text/template"

	vmoperatorv1beta1 "github.com/VictoriaMetrics/operator/api/operator/v1beta1"
	"github.com/stretchr/testify/assert"
	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"

	agentv2 "github.com/tidbcloud/observability/agent-control-server/api/v2"
	"github.com/tidbcloud/observability/agent-control-server/pkg/config"
	"github.com/tidbcloud/observability/agent-control-server/pkg/consts"
	"github.com/tidbcloud/observability/agent-control-server/pkg/reconciler"
	"github.com/tidbcloud/observability/agent-control-server/pkg/testkits"
)

func makeVMAgentTask(agent *agentv2.KubernetesAgent, opts testkits.TaskOptions) *VMAgentTask {
	objs := []client.Object{agent}
	if len(opts.LocalObjects) > 0 {
		objs = append(objs, opts.LocalObjects...)
	}

	localCli := testkits.BuildClient(objs...)
	ctx := &reconciler.Context[*agentv2.KubernetesAgent]{
		Agent:     agent,
		Local:     localCli,
		Remote:    testkits.NewClientGetterWithOpts(opts),
		Config:    &opts.Config,
		Templates: opts.Templates,
	}
	return NewVMAgentTask(ctx)
}

func baseVMAgentConfig() config.RootConfig {
	return config.RootConfig{
		O11yConfig: config.ObservabilityInfra{
			Vendor: "aws",
			CustomerConfigsV2: config.CustomerConfigsV2{
				NextGen: config.CustomerNextGen{
					K8s: config.CustomerNextGenK8s{
						Default: config.CustomerAgents{
							VMAgent: &config.VMAgentConfig{
								Image: "victoriametrics/vmagent:latest",
								StatefulStorage: &vmoperatorv1beta1.StorageSpec{
									VolumeClaimTemplate: vmoperatorv1beta1.EmbeddedPersistentVolumeClaim{
										Spec: corev1.PersistentVolumeClaimSpec{},
									},
								},
							},
						},
						Specs: config.CustomerAgentSpecs{
							{
								Name:    "default",
								VMAgent: &corev1.ResourceRequirements{},
							},
						},
					},
				},
			},
		},
	}
}

func TestVMAgentTask(t *testing.T) {
	tests := []struct {
		name   string
		verify func(t *testing.T)
	}{
		{
			name: "Name",
			verify: func(t *testing.T) {
				task := &VMAgentTask{}
				assert.Equal(t, "VMAgent", task.Name())
			},
		},
		{
			name: "ClusterMeta",
			verify: func(t *testing.T) {
				agent := testkits.NewKubernetesAgentBuilder().Build()
				task := makeVMAgentTask(agent, testkits.TaskOptions{})
				meta := task.ClusterMeta()

				assert.Equal(t, consts.AgentKubernetes, meta.Agent)
				assert.Equal(t, "test-cluster", meta.K8sClusterName)
				assert.Equal(t, "tenant-1", meta.TenantID)
				assert.Equal(t, "app-1", meta.ApplicationID)
				assert.Equal(t, "tidbcloud", meta.BizType)
			},
		},
		{
			name: "buildNamespace normal mode",
			verify: func(t *testing.T) {
				agent := testkits.NewKubernetesAgentBuilder().Build()
				task := makeVMAgentTask(agent, testkits.TaskOptions{Config: baseVMAgentConfig()})
				assert.Equal(t, consts.O11ySystemNamespace, task.buildNamespace())
			},
		},
		{
			name: "buildNamespace serverless mode",
			verify: func(t *testing.T) {
				cfg := baseVMAgentConfig()
				cfg.Mode = config.ModeServerless
				agent := testkits.NewKubernetesAgentBuilder().Build()
				task := makeVMAgentTask(agent, testkits.TaskOptions{Config: cfg})
				assert.Equal(t, consts.ServerlessObservabilityNamespace, task.buildNamespace())
			},
		},
		{
			name: "vmagentLabels",
			verify: func(t *testing.T) {
				agent := testkits.NewKubernetesAgentBuilder().Build()
				task := makeVMAgentTask(agent, testkits.TaskOptions{})
				labels := task.vmagentLabels()

				assert.Equal(t, consts.AgentKubernetes, labels["controller/agent"])
				assert.Equal(t, consts.ComponentVMAgent, labels["controller/component"])
				assert.Equal(t, "default", labels[consts.AgentKeyRefNamespace])
				assert.Equal(t, "test-agent", labels[consts.AgentKeyRefName])
			},
		},
		{
			name: "statefulsetLabels",
			verify: func(t *testing.T) {
				agent := testkits.NewKubernetesAgentBuilder().Build()
				task := makeVMAgentTask(agent, testkits.TaskOptions{Config: baseVMAgentConfig()})
				labels := task.statefulsetLabels()

				assert.Equal(t, consts.O11yNameVMAgent, labels["app.kubernetes.io/instance"])
				assert.Equal(t, "vmagent", labels["app.kubernetes.io/name"])
			},
		},
	}

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

func TestVMAgentTask_Create(t *testing.T) {
	tmpl, err := template.New("test").Parse(`
metadata:
  name: {{.Name}}
  namespace: {{.Namespace}}
spec:
  image: {{.ImageRepo}}:{{.ImageTag}}
`)
	assert.NoError(t, err)

	tests := []struct {
		name    string
		agent   *agentv2.KubernetesAgent
		opts    testkits.TaskOptions
		wantErr bool
	}{
		{
			name:    "create fails with remote client error",
			agent:   testkits.NewKubernetesAgentBuilder().Build(),
			opts:    testkits.TaskOptions{ClientError: true, Config: baseVMAgentConfig()},
			wantErr: true,
		},
		{
			name: "create fails with missing template",
			agent: testkits.NewKubernetesAgentBuilder().
				WithAnnotations(map[string]string{
					consts.AnnotationKeyO11yVMAccountID:    "vm-acc-1",
					consts.AnnotationKeyO11yVMProjectID:    "vm-proj-1",
					consts.AnnotationKeyO11yServiceDNSName: "o11y-service.example.com",
				}).
				Build(),
			opts:    testkits.TaskOptions{Client: testkits.BuildClient(), Config: baseVMAgentConfig()},
			wantErr: true,
		},
		{
			name: "create fails when spec not found",
			agent: testkits.NewKubernetesAgentBuilder().
				WithAnnotations(map[string]string{consts.AnnotationKeyO11yDesiredSpec: "nonexistent"}).
				Build(),
			opts: func() testkits.TaskOptions {
				cfg := baseVMAgentConfig()
				cfg.O11yConfig.CustomerConfigsV2.NextGen.K8s.Specs = nil
				return testkits.TaskOptions{Client: testkits.BuildClient(), Config: cfg}
			}(),
			wantErr: true,
		},
		{
			name: "create succeeds with valid config and template",
			agent: testkits.NewKubernetesAgentBuilder().
				WithAnnotations(map[string]string{
					consts.AnnotationKeyO11yVMAccountID:    "vm-acc-1",
					consts.AnnotationKeyO11yVMProjectID:    "vm-proj-1",
					consts.AnnotationKeyO11yServiceDNSName: "o11y-service.example.com",
				}).
				Build(),
			opts: testkits.TaskOptions{
				Client:    testkits.BuildClient(),
				Config:    baseVMAgentConfig(),
				Templates: config.Templates{"k8s/vmagent.yaml": tmpl},
			},
			wantErr: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			task := makeVMAgentTask(tt.agent, tt.opts)
			result := task.Create(context.Background())
			if tt.wantErr {
				assert.Error(t, result.Error())
			} else {
				assert.NoError(t, result.Error())
			}
		})
	}
}

func TestVMAgentTask_PostCreate(t *testing.T) {
	tests := []struct {
		name    string
		agent   *agentv2.KubernetesAgent
		opts    testkits.TaskOptions
		wantErr bool
	}{
		{
			name:    "post create fails with remote client error",
			agent:   testkits.NewKubernetesAgentBuilder().Build(),
			opts:    testkits.TaskOptions{ClientError: true},
			wantErr: true,
		},
		{
			name:  "post create succeeds when no matching statefulset",
			agent: testkits.NewKubernetesAgentBuilder().Build(),
			opts: testkits.TaskOptions{
				Client: testkits.BuildClient(),
			},
			wantErr: false,
		},
		{
			name:  "post create succeeds when statefulset ready",
			agent: testkits.NewKubernetesAgentBuilder().Build(),
			opts: testkits.TaskOptions{
				Client: testkits.BuildClient(
					&appsv1.StatefulSet{
						ObjectMeta: metav1.ObjectMeta{
							Name:      "vmagent-" + consts.O11yNameVMAgent,
							Namespace: consts.O11ySystemNamespace,
							Labels: map[string]string{
								"app.kubernetes.io/instance": consts.O11yNameVMAgent,
								"app.kubernetes.io/name":     "vmagent",
							},
						},
						Spec:   appsv1.StatefulSetSpec{Replicas: ptr.To(int32(1))},
						Status: appsv1.StatefulSetStatus{AvailableReplicas: 1},
					},
				),
			},
			wantErr: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			task := makeVMAgentTask(tt.agent, tt.opts)
			result := task.PostCreate(context.Background())
			if tt.wantErr {
				assert.Error(t, result.Error())
			} else {
				assert.NoError(t, result.Error())
			}
		})
	}
}

func TestVMAgentTask_Delete(t *testing.T) {
	tests := []struct {
		name    string
		agent   *agentv2.KubernetesAgent
		opts    testkits.TaskOptions
		wantErr bool
	}{
		{
			name:  "delete fails with remote client error",
			agent: testkits.NewKubernetesAgentBuilder().Build(),
			opts: testkits.TaskOptions{
				ClientError: true,
				Config:      baseVMAgentConfig(),
			},
			wantErr: true,
		},
		{
			name:  "delete succeeds with valid remote client",
			agent: testkits.NewKubernetesAgentBuilder().Build(),
			opts: testkits.TaskOptions{
				Client: testkits.BuildClient(),
				Config: baseVMAgentConfig(),
			},
			wantErr: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			task := makeVMAgentTask(tt.agent, tt.opts)
			result := task.Delete(context.Background())
			if tt.wantErr {
				assert.Error(t, result.Error())
			} else {
				assert.NoError(t, result.Error())
			}
		})
	}
}

func TestVMAgentTask_PostDelete(t *testing.T) {
	tests := []struct {
		name    string
		agent   *agentv2.KubernetesAgent
		opts    testkits.TaskOptions
		wantErr bool
	}{
		{
			name:    "post delete fails with remote client error",
			agent:   testkits.NewKubernetesAgentBuilder().Build(),
			opts:    testkits.TaskOptions{ClientError: true},
			wantErr: true,
		},
		{
			name:    "post delete succeeds when vmagent deleted",
			agent:   testkits.NewKubernetesAgentBuilder().Build(),
			opts:    testkits.TaskOptions{Client: testkits.BuildClient()},
			wantErr: false,
		},
		{
			name:  "post delete fails when vmagent still exists",
			agent: testkits.NewKubernetesAgentBuilder().Build(),
			opts: testkits.TaskOptions{
				Client: testkits.BuildClient(
					&vmoperatorv1beta1.VMAgent{
						ObjectMeta: metav1.ObjectMeta{
							Name:      consts.O11yNameVMAgent,
							Namespace: consts.O11ySystemNamespace,
						},
					},
				),
			},
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			task := makeVMAgentTask(tt.agent, tt.opts)
			result := task.PostDelete(context.Background())
			if tt.wantErr {
				assert.Error(t, result.Error())
			} else {
				assert.NoError(t, result.Error())
			}
		})
	}
}

func TestVMAgentTask_build(t *testing.T) {
	tmpl, err := template.New("test").Parse(`
metadata:
  name: {{.Name}}
  namespace: {{.Namespace}}
spec:
  image:
    repository: {{.ImageRepo}}
    tag: {{.ImageTag}}
`)
	assert.NoError(t, err)

	tests := []struct {
		name     string
		agent    *agentv2.KubernetesAgent
		opts     testkits.TaskOptions
		wantErr  bool
		errMsg   string
		wantName string
	}{
		{
			name: "build succeeds with valid config and template",
			agent: testkits.NewKubernetesAgentBuilder().
				WithAnnotations(map[string]string{
					consts.AnnotationKeyO11yVMAccountID:    "vm-acc-1",
					consts.AnnotationKeyO11yVMProjectID:    "vm-proj-1",
					consts.AnnotationKeyO11yServiceDNSName: "o11y-service.example.com",
				}).
				Build(),
			opts:     testkits.TaskOptions{Config: baseVMAgentConfig(), Templates: config.Templates{"k8s/vmagent.yaml": tmpl}},
			wantErr:  false,
			wantName: consts.O11yNameVMAgent,
		},
		{
			name:    "build fails with missing template",
			agent:   testkits.NewKubernetesAgentBuilder().Build(),
			opts:    testkits.TaskOptions{Config: baseVMAgentConfig()},
			wantErr: true,
		},
		{
			name: "build fails when spec not found",
			agent: testkits.NewKubernetesAgentBuilder().
				WithAnnotations(map[string]string{consts.AnnotationKeyO11yDesiredSpec: "nonexistent"}).
				Build(),
			opts: func() testkits.TaskOptions {
				cfg := baseVMAgentConfig()
				cfg.O11yConfig.CustomerConfigsV2.NextGen.K8s.Specs = nil
				return testkits.TaskOptions{Config: cfg, Templates: config.Templates{"k8s/vmagent.yaml": tmpl}}
			}(),
			wantErr: true,
			errMsg:  "desired spec not found",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			task := makeVMAgentTask(tt.agent, tt.opts)
			result, err := task.build()
			if tt.wantErr {
				assert.Error(t, err)
				if tt.errMsg != "" {
					assert.Contains(t, err.Error(), tt.errMsg)
				}
				return
			}
			assert.NoError(t, err)
			assert.NotNil(t, result)
			if tt.wantName != "" {
				assert.Equal(t, tt.wantName, result.Name)
			}
		})
	}
}
