package task

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

	vmoperatorv1beta1 "github.com/VictoriaMetrics/operator/api/operator/v1beta1"
	"github.com/stretchr/testify/assert"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"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 makeVMPodScrapeTask(agent *agentv2.KubernetesAgent, opts testkits.TaskOptions) *VMPodScrapeTask {
	ctx := &reconciler.Context[*agentv2.KubernetesAgent]{
		Agent:     agent,
		Local:     testkits.BuildClient(agent),
		Remote:    testkits.NewClientGetterWithOpts(opts),
		Config:    &config.RootConfig{},
		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.NewKubernetesAgentBuilder().Build()
				task := makeVMPodScrapeTask(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)
			},
		},
		{
			name: "buildNamespace returns serverless namespace",
			verify: func(t *testing.T) {
				agent := testkits.NewKubernetesAgentBuilder().Build()
				task := makeVMPodScrapeTask(agent, testkits.TaskOptions{})
				assert.Equal(t, consts.ServerlessObservabilityNamespace, task.buildNamespace())
			},
		},
	}

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

func TestVMPodScrapeTask_Create(t *testing.T) {
	tmpl, err := template.New("test").Parse(`
metadata:
  name: test-podscrape
  namespace: {{.Namespace}}
spec:
  selector: {}
`)
	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},
			wantErr: true,
		},
		{
			name:    "create succeeds with no matching templates",
			agent:   testkits.NewKubernetesAgentBuilder().Build(),
			opts:    testkits.TaskOptions{Client: testkits.BuildClient()},
			wantErr: false,
		},
		{
			name:  "create succeeds with k8s/jobs templates",
			agent: testkits.NewKubernetesAgentBuilder().Build(),
			opts: testkits.TaskOptions{
				Client:    testkits.BuildClient(),
				Templates: config.Templates{"k8s/jobs/test.yaml": tmpl},
			},
			wantErr: false,
		},
		{
			name:  "create skips non-k8s/jobs templates",
			agent: testkits.NewKubernetesAgentBuilder().Build(),
			opts: testkits.TaskOptions{
				Client:    testkits.BuildClient(),
				Templates: config.Templates{"k8s/other.yaml": tmpl},
			},
			wantErr: 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())
			} else {
				assert.NoError(t, result.Error())
			}
		})
	}
}

func TestVMPodScrapeTask_PostCreate(t *testing.T) {
	tests := []struct {
		name             string
		agent            *agentv2.KubernetesAgent
		opts             testkits.TaskOptions
		vmpodscrapeNames []string
		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 with no vmpodscrape names",
			agent:            testkits.NewKubernetesAgentBuilder().Build(),
			opts:             testkits.TaskOptions{Client: testkits.BuildClient()},
			vmpodscrapeNames: nil,
			wantErr:          false,
		},
		{
			name:  "post create succeeds when vmpodscrape is ready",
			agent: testkits.NewKubernetesAgentBuilder().Build(),
			opts: testkits.TaskOptions{
				Client: testkits.BuildClient(
					&vmoperatorv1beta1.VMPodScrape{
						ObjectMeta: metav1.ObjectMeta{
							Name:      "test-podscrape",
							Namespace: consts.ServerlessObservabilityNamespace,
						},
						Status: vmoperatorv1beta1.ScrapeObjectStatus{
							StatusMetadata: vmoperatorv1beta1.StatusMetadata{
								UpdateStatus: vmoperatorv1beta1.UpdateStatusOperational,
							},
						},
					},
				),
			},
			vmpodscrapeNames: []string{"test-podscrape"},
			wantErr:          false,
		},
		{
			name:  "post create fails when vmpodscrape not found",
			agent: testkits.NewKubernetesAgentBuilder().Build(),
			opts: testkits.TaskOptions{
				Client: testkits.BuildClient(),
			},
			vmpodscrapeNames: []string{"missing-podscrape"},
			wantErr:          true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			task := makeVMPodScrapeTask(tt.agent, tt.opts)
			task.vmpodscrapeNames = tt.vmpodscrapeNames
			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.KubernetesAgent
		opts    testkits.TaskOptions
		wantErr bool
	}{
		{
			name:    "delete fails with remote client error",
			agent:   testkits.NewKubernetesAgentBuilder().Build(),
			opts:    testkits.TaskOptions{ClientError: true},
			wantErr: true,
		},
		{
			name:    "delete succeeds with no vmpodscrapes",
			agent:   testkits.NewKubernetesAgentBuilder().Build(),
			opts:    testkits.TaskOptions{Client: testkits.BuildClient()},
			wantErr: false,
		},
		{
			name:  "delete succeeds with existing vmpodscrapes",
			agent: testkits.NewKubernetesAgentBuilder().Build(),
			opts: testkits.TaskOptions{
				Client: testkits.BuildClient(
					&vmoperatorv1beta1.VMPodScrape{
						ObjectMeta: metav1.ObjectMeta{
							Name:      "test-podscrape",
							Namespace: consts.ServerlessObservabilityNamespace,
							Labels: map[string]string{
								consts.LabelKeyVMPodScrapeAgent: consts.AgentKubernetes,
							},
						},
					},
				),
			},
			wantErr: false,
		},
	}

	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())
			}
		})
	}
}

func TestVMPodScrapeTask_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 no vmpodscrapes remain",
			agent:   testkits.NewKubernetesAgentBuilder().Build(),
			opts:    testkits.TaskOptions{Client: testkits.BuildClient()},
			wantErr: false,
		},
		{
			name:  "post delete fails when vmpodscrapes still exist",
			agent: testkits.NewKubernetesAgentBuilder().Build(),
			opts: testkits.TaskOptions{
				Client: testkits.BuildClient(
					&vmoperatorv1beta1.VMPodScrape{
						ObjectMeta: metav1.ObjectMeta{
							Name:      "test-podscrape",
							Namespace: consts.ServerlessObservabilityNamespace,
							Labels: map[string]string{
								consts.LabelKeyVMPodScrapeAgent: consts.AgentKubernetes,
							},
						},
					},
				),
			},
			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_create(t *testing.T) {
	tmpl, err := template.New("test").Parse(`
metadata:
  name: test-podscrape
  namespace: {{.Namespace}}
spec:
  selector: {}
`)
	assert.NoError(t, err)

	tests := []struct {
		name                    string
		agent                   *agentv2.KubernetesAgent
		opts                    testkits.TaskOptions
		wantErr                 bool
		wantVMPodScrapeNamesLen int
	}{
		{
			name:                    "create collects no names with no matching templates",
			agent:                   testkits.NewKubernetesAgentBuilder().Build(),
			opts:                    testkits.TaskOptions{Client: testkits.BuildClient()},
			wantErr:                 false,
			wantVMPodScrapeNamesLen: 0,
		},
		{
			name:  "create collects names from k8s/jobs templates",
			agent: testkits.NewKubernetesAgentBuilder().Build(),
			opts: testkits.TaskOptions{
				Client:    testkits.BuildClient(),
				Templates: config.Templates{"k8s/jobs/test.yaml": tmpl},
			},
			wantErr:                 false,
			wantVMPodScrapeNamesLen: 1,
		},
		{
			name:  "create skips non-k8s/jobs templates",
			agent: testkits.NewKubernetesAgentBuilder().Build(),
			opts: testkits.TaskOptions{
				Client:    testkits.BuildClient(),
				Templates: config.Templates{"k8s/other.yaml": tmpl},
			},
			wantErr:                 false,
			wantVMPodScrapeNamesLen: 0,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cli := testkits.NewClientGetter(tt.opts.Client)
			task := makeVMPodScrapeTask(tt.agent, tt.opts)
			err := task.create(context.Background(), cli)
			if tt.wantErr {
				assert.Error(t, err)
				return
			}
			assert.NoError(t, err)
			assert.Len(t, task.vmpodscrapeNames, tt.wantVMPodScrapeNamesLen)
		})
	}
}

func TestVMPodScrapeTask_listVmpodscrapes(t *testing.T) {
	errList := errors.New("list error")

	tests := []struct {
		name    string
		agent   *agentv2.KubernetesAgent
		cli     client.Client
		wantLen int
		wantErr bool
	}{
		{
			name:    "list returns empty when no vmpodscrapes",
			agent:   testkits.NewKubernetesAgentBuilder().Build(),
			cli:     testkits.BuildClient(),
			wantLen: 0,
			wantErr: false,
		},
		{
			name:  "list returns existing vmpodscrapes in namespace",
			agent: testkits.NewKubernetesAgentBuilder().Build(),
			cli: testkits.BuildClient(
				&vmoperatorv1beta1.VMPodScrape{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "test-podscrape-1",
						Namespace: consts.ServerlessObservabilityNamespace,
						Labels: map[string]string{
							consts.LabelKeyVMPodScrapeAgent: consts.AgentKubernetes,
						},
					},
				},
				&vmoperatorv1beta1.VMPodScrape{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "test-podscrape-2",
						Namespace: consts.ServerlessObservabilityNamespace,
						Labels: map[string]string{
							consts.LabelKeyVMPodScrapeAgent: consts.AgentKubernetes,
						},
					},
				},
			),
			wantLen: 2,
			wantErr: false,
		},
		{
			name:  "list does not return vmpodscrapes from other namespaces",
			agent: testkits.NewKubernetesAgentBuilder().Build(),
			cli: testkits.BuildClient(
				&vmoperatorv1beta1.VMPodScrape{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "test-podscrape-other",
						Namespace: "other-namespace",
					},
				},
			),
			wantLen: 0,
			wantErr: false,
		},
		{
			name:  "list fails with list error",
			agent: testkits.NewKubernetesAgentBuilder().Build(),
			cli: testkits.NewClientGetter(testkits.BuildClient()).
				WithListFunc(func(_ context.Context, _ client.ObjectList, _ ...client.ListOption) error {
					return errList
				}),
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			task := makeVMPodScrapeTask(tt.agent, testkits.TaskOptions{Client: testkits.BuildClient()})
			items, err := task.listVmpodscrapes(context.Background(), tt.cli)
			if tt.wantErr {
				assert.Error(t, err)
				return
			}
			assert.NoError(t, err)
			assert.Len(t, items, tt.wantLen)
		})
	}
}
