package task

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

	vmoperatorv1beta1 "github.com/VictoriaMetrics/operator/api/operator/v1beta1"
	"github.com/stretchr/testify/assert"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"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/deploy"
	"github.com/tidbcloud/observability/agent-control-server/pkg/reconciler"
	"github.com/tidbcloud/observability/agent-control-server/pkg/testkits"
)

func sharedTiproxyVMPodScrapeTmpl() *template.Template {
	tmpl, err := template.New("tiproxy").Parse(`
apiVersion: operator.victoriametrics.com/v1beta1
kind: VMPodScrape
metadata:
  name: db-{{.InstanceID}}-tiproxy
  namespace: {{.Namespace}}
  labels:
    scrape-by: {{.LabelScrapeBy}}
    db-instance-id: {{.InstanceID}}
    component-level: sharedpool
spec:
  podMetricsEndpoints:
    - port: api
  selector:
    matchLabels:
      app.kubernetes.io/component: tiproxy
      app.kubernetes.io/instance: db-{{.InstanceID}}
`)
	if err != nil {
		panic(err)
	}
	return tmpl
}

func sharedPoolTiproxyTmpls() config.Templates {
	return config.Templates{
		"sharedpool/jobs/tiproxy.yaml": sharedTiproxyVMPodScrapeTmpl(),
	}
}

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

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

func TestVMPodScrapeTask(t *testing.T) {
	tests := []struct {
		name   string
		verify func(t *testing.T)
	}{
		{
			name: "Name",
			verify: func(t *testing.T) {
				task := &VMPodScrapeTask{}
				assert.Equal(t, "VMPodScrape", task.Name())
			},
		},
		{
			name: "ClusterMeta",
			verify: func(t *testing.T) {
				agent := testkits.NewSharedPoolAgentBuilder().Build()
				task := makeVMPodScrapeTask(agent, testkits.TaskOptions{})
				meta := task.ClusterMeta()

				assert.Equal(t, consts.AgentSharedPool, 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",
			verify: func(t *testing.T) {
				agent := testkits.NewSharedPoolAgentBuilder().Build()
				task := makeVMPodScrapeTask(agent, testkits.TaskOptions{})
				assert.Equal(t, "tidbpool-456", task.buildNamespace())
			},
		},
		{
			name: "buildLabelScrapeBy",
			verify: func(t *testing.T) {
				agent := testkits.NewSharedPoolAgentBuilder().Build()
				task := makeVMPodScrapeTask(agent, testkits.TaskOptions{})
				assert.Equal(t, "tidbpool-456-o11y-vmagent", task.buildLabelScrapeBy())
			},
		},
	}

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

func TestVMPodScrapeTask_Create(t *testing.T) {
	tests := []struct {
		name    string
		agent   *agentv2.SharedPoolAgent
		opts    testkits.TaskOptions
		wantErr bool
		wantRes deploy.Result
	}{
		{
			name:    "create fails with remote client error",
			agent:   testkits.NewSharedPoolAgentBuilder().Build(),
			opts:    testkits.TaskOptions{ClientError: true},
			wantErr: true,
		},
		{
			name:    "create succeeds with valid remote client",
			agent:   testkits.NewSharedPoolAgentBuilder().Build(),
			opts:    testkits.TaskOptions{Client: testkits.BuildClient()},
			wantErr: false,
			wantRes: deploy.Done(),
		},
	}

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

func TestVMPodScrapeTask_Create_TiProxy(t *testing.T) {
	pdPods := []client.Object{
		&corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "db-inst123-pd-0", Namespace: "tidbpool-456", Labels: map[string]string{"pingcap.com/component": "pd"}}},
	}

	tests := []struct {
		name       string
		agent      *agentv2.SharedPoolAgent
		opts       testkits.TaskOptions
		wantErr    bool
		wantCreate bool
	}{
		{
			name:  "create includes tiproxy when annotation is shared_tiproxy",
			agent: testkits.NewSharedPoolAgentBuilder().WithAnnotations(map[string]string{consts.AnnotationKeyO11yTiProxyDeployType: "shared_tiproxy"}).Build(),
			opts: testkits.TaskOptions{
				Client:    testkits.BuildClient(pdPods...),
				Templates: sharedPoolTiproxyTmpls(),
			},
			wantErr:    false,
			wantCreate: true,
		},
		{
			name:  "create excludes tiproxy when annotation value is not shared_tiproxy",
			agent: testkits.NewSharedPoolAgentBuilder().Build(),
			opts: testkits.TaskOptions{
				Client:    testkits.BuildClient(pdPods...),
				Templates: sharedPoolTiproxyTmpls(),
			},
			wantErr:    false,
			wantCreate: false,
		},
	}

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

			scrape := &vmoperatorv1beta1.VMPodScrape{}
			err := tt.opts.Client.Get(context.Background(), types.NamespacedName{
				Name:      "db-inst123-tiproxy",
				Namespace: "tidbpool-456",
			}, scrape)
			if tt.wantCreate {
				assert.NoError(t, err)
			} else {
				assert.Error(t, err)
			}
		})
	}
}

func TestVMPodScrapeTask_PostCreate(t *testing.T) {
	tests := []struct {
		name    string
		agent   *agentv2.SharedPoolAgent
		opts    testkits.TaskOptions
		names   []string
		wantErr bool
	}{
		{
			name:    "post create fails with remote client error",
			agent:   testkits.NewSharedPoolAgentBuilder().Build(),
			opts:    testkits.TaskOptions{ClientError: true},
			names:   []string{"test-scrape"},
			wantErr: true,
		},
		{
			name:    "post create fails when vmpodscrape not ready",
			agent:   testkits.NewSharedPoolAgentBuilder().Build(),
			opts:    testkits.TaskOptions{Client: testkits.BuildClient()},
			names:   []string{"test-scrape"},
			wantErr: true,
		},
		{
			name:  "post create succeeds when vmpodscrape ready",
			agent: testkits.NewSharedPoolAgentBuilder().Build(),
			opts: testkits.TaskOptions{Client: testkits.BuildClient(
				&vmoperatorv1beta1.VMPodScrape{
					ObjectMeta: metav1.ObjectMeta{Name: "test-scrape", Namespace: "tidbpool-456"},
					Status: vmoperatorv1beta1.ScrapeObjectStatus{
						StatusMetadata: vmoperatorv1beta1.StatusMetadata{UpdateStatus: vmoperatorv1beta1.UpdateStatusOperational},
					},
				},
			)},
			names:   []string{"test-scrape"},
			wantErr: false,
		},
	}

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

func TestVMPodScrapeTask_Delete(t *testing.T) {
	tests := []struct {
		name    string
		agent   *agentv2.SharedPoolAgent
		opts    testkits.TaskOptions
		wantErr bool
		verify  func(t *testing.T, opts testkits.TaskOptions)
	}{
		{
			name:    "delete fails with remote client error",
			agent:   testkits.NewSharedPoolAgentBuilder().Build(),
			opts:    testkits.TaskOptions{ClientError: true},
			wantErr: true,
		},
		{
			name:    "delete succeeds with valid remote client",
			agent:   testkits.NewSharedPoolAgentBuilder().Build(),
			opts:    testkits.TaskOptions{Client: testkits.BuildClient()},
			wantErr: false,
		},
		{
			name:  "delete succeeds when vmpodscrapes exist",
			agent: testkits.NewSharedPoolAgentBuilder().Build(),
			opts: testkits.TaskOptions{
				Client: testkits.BuildClient(
					&vmoperatorv1beta1.VMPodScrape{
						ObjectMeta: metav1.ObjectMeta{
							Name:      "test-scrape",
							Namespace: "tidbpool-456",
							Labels: map[string]string{
								"scrape-by": "tidbpool-456-o11y-vmagent",
							},
						},
					},
				),
			},
			wantErr: false,
			verify: func(t *testing.T, opts testkits.TaskOptions) {
				scrape := &vmoperatorv1beta1.VMPodScrape{}
				err := opts.Client.Get(context.Background(), types.NamespacedName{
					Name:      "test-scrape",
					Namespace: "tidbpool-456",
				}, scrape)
				assert.Error(t, err)
			},
		},
	}

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

func TestVMPodScrapeTask_PostDelete(t *testing.T) {
	tests := []struct {
		name    string
		agent   *agentv2.SharedPoolAgent
		opts    testkits.TaskOptions
		wantErr bool
	}{
		{
			name:    "post delete fails with remote client error",
			agent:   testkits.NewSharedPoolAgentBuilder().Build(),
			opts:    testkits.TaskOptions{ClientError: true},
			wantErr: true,
		},
		{
			name:    "post delete succeeds when vmpodscrapes deleted",
			agent:   testkits.NewSharedPoolAgentBuilder().Build(),
			opts:    testkits.TaskOptions{Client: testkits.BuildClient()},
			wantErr: false,
		},
		{
			name:  "post delete fails when vmpodscrape still exists",
			agent: testkits.NewSharedPoolAgentBuilder().Build(),
			opts: testkits.TaskOptions{Client: testkits.BuildClient(
				&vmoperatorv1beta1.VMPodScrape{ObjectMeta: metav1.ObjectMeta{
					Name:      "test-scrape",
					Namespace: "tidbpool-456",
					Labels:    map[string]string{"scrape-by": "tidbpool-456-o11y-vmagent"},
				}},
			)},
			wantErr: true,
		},
	}

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

func TestVMPodScrapeTask_buildInstanceIDs(t *testing.T) {
	tests := []struct {
		name             string
		remoteClient     client.Client
		wantInstanceIDs  []string
		wantInstanceSize int
	}{
		{
			name: "extracts instanceIDs from PD pods",
			remoteClient: testkits.BuildClient(
				&corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "db-instance123-pd-0", Namespace: "tidbpool-456", Labels: map[string]string{"pingcap.com/component": "pd"}}},
				&corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "db-instance456-pd-0", Namespace: "tidbpool-456", Labels: map[string]string{"pingcap.com/component": "pd"}}},
				&corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "tikv-0", Namespace: "tidbpool-456", Labels: map[string]string{"pingcap.com/component": "tikv"}}},
			),
			wantInstanceIDs:  []string{"instance123", "instance456"},
			wantInstanceSize: 2,
		},
		{
			name:             "returns empty when no PD pods",
			remoteClient:     testkits.BuildClient(),
			wantInstanceIDs:  nil,
			wantInstanceSize: 0,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			agent := testkits.NewSharedPoolAgentBuilder().Build()
			task := makeVMPodScrapeTask(agent, testkits.TaskOptions{Client: tt.remoteClient})
			instanceIDs, err := task.buildInstanceIDs(context.Background(), tt.remoteClient)
			assert.NoError(t, err)
			assert.Len(t, instanceIDs, tt.wantInstanceSize)
			for _, instanceID := range tt.wantInstanceIDs {
				assert.Contains(t, instanceIDs, instanceID)
			}
		})
	}
}

func TestVMPodScrapeTask_buildMetricRelabelConfigs(t *testing.T) {
	tests := []struct {
		name         string
		localObjects []client.Object
		wantLen      int
		verify       func(t *testing.T, configs []*vmoperatorv1beta1.RelabelConfig)
	}{
		{
			name: "builds relabel configs from TiDBGroupAgents",
			localObjects: []client.Object{&agentv2.TiDBGroupAgent{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "tidb-group-1",
					Namespace: "default",
					Annotations: map[string]string{
						consts.AnnotationKeyO11yVMAccountID: "vm-acc-1",
						consts.AnnotationKeyO11yVMProjectID: "vm-proj-1",
					},
				},
				Spec: agentv2.TiDBGroupAgentSpec{SharedPoolID: "pool-456", ClusterID: "cluster-1"},
			}},
			wantLen: 3,
			verify: func(t *testing.T, configs []*vmoperatorv1beta1.RelabelConfig) {
				assert.Equal(t, "replace", configs[0].Action)
				assert.Equal(t, []string{"keyspace_name"}, configs[0].SourceLabels)
				assert.Equal(t, "tidb_cluster_id", configs[0].TargetLabel)
			},
		},
		{
			name: "returns empty when no matching TiDBGroupAgents",
			localObjects: []client.Object{&agentv2.TiDBGroupAgent{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "tidb-group-1",
					Namespace: "default",
					Annotations: map[string]string{
						consts.AnnotationKeyO11yVMAccountID: "vm-acc-1",
						consts.AnnotationKeyO11yVMProjectID: "vm-proj-1",
					},
				},
				Spec: agentv2.TiDBGroupAgentSpec{SharedPoolID: "different-pool", ClusterID: "cluster-1"},
			}},
			wantLen: 0,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			agent := testkits.NewSharedPoolAgentBuilder().Build()
			task := makeVMPodScrapeTask(agent, testkits.TaskOptions{LocalObjects: tt.localObjects})
			configs, err := task.buildMetricRelabelConfigs(context.Background())
			assert.NoError(t, err)
			assert.Len(t, configs, tt.wantLen)
			if tt.verify != nil {
				tt.verify(t, configs)
			}
		})
	}
}
