package kubernetesagent

import (
	"context"
	"errors"
	"testing"

	"github.com/go-logr/logr"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"

	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"
)

func TestBuildTasks(t *testing.T) {
	tests := []struct {
		name        string
		vendor      string
		peeringNLB  string
		annotations map[string]string
		agentConfig func(*agentv2.KubernetesAgent)
		build       func(*AgentReconciler, *reconciler.Context[*agentv2.KubernetesAgent]) []deploy.Task
		want        []string
	}{
		{
			name:   "aws includes private link when peering nlb is not configured",
			vendor: consts.VendorTypeAWS,
			agentConfig: func(agent *agentv2.KubernetesAgent) {
				agent.Spec.EKS = &agentv2.EKSConfig{AccountID: "account-1"}
			},
			build: (*AgentReconciler).buildTasks,
			want: []string{
				"Guardian", "Backend", "StorageClass", "PrivateLink",
				"PriorityClass", "HelmRepo", "Operator", "VMAgent",
				"Vector", "Restart", "DrainNode", "UpdateVersion",
			},
		},
		{
			name:       "aws skips private link when peering nlb is configured",
			vendor:     consts.VendorTypeAWS,
			peeringNLB: "nlb.internal",
			agentConfig: func(agent *agentv2.KubernetesAgent) {
				agent.Spec.EKS = &agentv2.EKSConfig{AccountID: "account-1"}
			},
			build: (*AgentReconciler).buildTasks,
			want: []string{
				"Guardian", "Backend", "StorageClass", "PriorityClass",
				"HelmRepo", "Operator", "VMAgent", "Vector",
				"Restart", "DrainNode", "UpdateVersion",
			},
		},
		{
			name:   "azure fetches kubelet identity before backend and private link",
			vendor: consts.VendorTypeAzure,
			agentConfig: func(agent *agentv2.KubernetesAgent) {
				agent.Spec.AKS = &agentv2.AKSConfig{SubscriptionID: "subscription-1"}
			},
			build: (*AgentReconciler).buildTasks,
			want: []string{
				"Guardian", "KubeletIdentity", "Backend", "StorageClass",
				"PrivateLink", "PriorityClass", "HelmRepo", "Operator",
				"VMAgent", "Vector", "Restart", "DrainNode",
				"UpdateVersion",
			},
		},
		{
			name:   "alicloud includes storage class and private link",
			vendor: consts.VendorTypeAliCloud,
			agentConfig: func(agent *agentv2.KubernetesAgent) {
				agent.Spec.ACK = &agentv2.ACKConfig{AccountID: "account-1"}
			},
			build: (*AgentReconciler).buildTasks,
			want: []string{
				"Guardian", "Backend", "StorageClass", "PrivateLink",
				"PriorityClass", "HelmRepo", "Operator", "VMAgent",
				"Vector", "Restart", "DrainNode", "UpdateVersion",
			},
		},
		{
			name:   "gcp has no cloud-specific nextgen setup before common tasks",
			vendor: consts.VendorTypeGCP,
			build:  (*AgentReconciler).buildTasks,
			want: []string{
				"Guardian", "Backend", "PriorityClass", "HelmRepo",
				"Operator", "VMAgent", "Vector", "Restart",
				"DrainNode", "UpdateVersion",
			},
		},
		{
			name:   "skip dataplane delete keeps only local and private connectivity tasks",
			vendor: consts.VendorTypeAWS,
			annotations: map[string]string{
				consts.AnnotationKeySkipDataPlaneDelete: "true",
			},
			agentConfig: func(agent *agentv2.KubernetesAgent) {
				agent.Spec.EKS = &agentv2.EKSConfig{AccountID: "account-1"}
			},
			build: (*AgentReconciler).buildTasks,
			want:  []string{"Guardian", "Backend", "PrivateLink"},
		},
		{
			name:   "skip dataplane delete keeps alicloud private link task",
			vendor: consts.VendorTypeAliCloud,
			annotations: map[string]string{
				consts.AnnotationKeySkipDataPlaneDelete: "true",
			},
			agentConfig: func(agent *agentv2.KubernetesAgent) {
				agent.Spec.ACK = &agentv2.ACKConfig{AccountID: "account-1"}
			},
			build: (*AgentReconciler).buildTasks,
			want:  []string{"Guardian", "Backend", "PrivateLink"},
		},
		{
			name:   "skip dataplane delete keeps azure kubelet identity and private link",
			vendor: consts.VendorTypeAzure,
			annotations: map[string]string{
				consts.AnnotationKeySkipDataPlaneDelete: "true",
			},
			agentConfig: func(agent *agentv2.KubernetesAgent) {
				agent.Spec.AKS = &agentv2.AKSConfig{SubscriptionID: "subscription-1"}
			},
			build: (*AgentReconciler).buildTasks,
			want:  []string{"Guardian", "KubeletIdentity", "Backend", "PrivateLink"},
		},
		{
			name:   "skip dataplane delete keeps only guardian and backend for providers without private connectivity",
			vendor: consts.VendorTypeGCP,
			annotations: map[string]string{
				consts.AnnotationKeySkipDataPlaneDelete: "true",
			},
			build: (*AgentReconciler).buildTasks,
			want:  []string{"Guardian", "Backend"},
		},
		{
			name:   "hibernated keeps only guardian task",
			vendor: consts.VendorTypeAWS,
			build: func(r *AgentReconciler, ctx *reconciler.Context[*agentv2.KubernetesAgent]) []deploy.Task {
				return r.buildHibernatedTasks(ctx)
			},
			want: []string{"Guardian"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			agent := newKubernetesAgent(tt.annotations)
			if tt.agentConfig != nil {
				tt.agentConfig(agent)
			}
			ar := &AgentReconciler{Reconciler: reconciler.Reconciler{
				Config: newKubernetesRootConfig(tt.vendor, tt.peeringNLB),
			}}
			ctx := &reconciler.Context[*agentv2.KubernetesAgent]{
				Agent:  agent,
				Config: ar.Config,
			}

			got := tt.build(ar, ctx)

			assert.Len(t, got, len(tt.want))
			for i, want := range tt.want {
				assert.Equal(t, want, got[i].Name())
			}
		})
	}
}

func newKubernetesAgent(annotations map[string]string) *agentv2.KubernetesAgent {
	return &agentv2.KubernetesAgent{
		ObjectMeta: metav1.ObjectMeta{
			Name:        "agent-1",
			Namespace:   "default",
			Annotations: annotations,
		},
		Spec: agentv2.KubernetesAgentSpec{
			KubernetesClusterName: "cluster-1",
			O11y: agentv2.O11yConfigV2{
				BizType:       "tidbcloud",
				TenantID:      "tenant-1",
				ApplicationID: "app-1",
			},
		},
	}
}

func TestUpdateStatusToNormalPanicPhase(t *testing.T) {
	tests := []struct {
		name       string
		startPhase string
		results    []deploy.Result
		wantPhase  string
	}{
		{
			name:       "panic in results sets phase to Panic",
			startPhase: agentv2.O11yAgentPhaseNormal,
			results: []deploy.Result{
				deploy.Done(),
				deploy.Panic(errors.New("vmpodscrape not operational")),
			},
			wantPhase: agentv2.O11yAgentPhasePanic,
		},
		{
			name:       "partial recovery with Error clears Panic",
			startPhase: agentv2.O11yAgentPhasePanic,
			results: []deploy.Result{
				deploy.Done(),
				deploy.Error(errors.New("transient")),
			},
			wantPhase: agentv2.O11yAgentPhaseNormal,
		},
		{
			name:       "full recovery returns phase to Normal",
			startPhase: agentv2.O11yAgentPhasePanic,
			results: []deploy.Result{
				deploy.Done(),
				deploy.Done(),
			},
			wantPhase: agentv2.O11yAgentPhaseNormal,
		},
		{
			name:       "Error from Normal leaves phase Normal",
			startPhase: agentv2.O11yAgentPhaseNormal,
			results: []deploy.Result{
				deploy.Error(errors.New("e")),
			},
			wantPhase: agentv2.O11yAgentPhaseNormal,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			agent := newKubernetesAgent(nil)
			agent.Status.Phase = tt.startPhase

			scheme := runtime.NewScheme()
			require.NoError(t, agentv2.AddToScheme(scheme))
			cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(agent).Build()

			ctx := &reconciler.Context[*agentv2.KubernetesAgent]{
				Agent:  agent,
				Local:  cli,
				Config: newKubernetesRootConfig(consts.VendorTypeAWS, ""),
			}

			_, _ = updateStatusToNormal(context.Background(), logr.Discard(), ctx, tt.results)
			assert.Equal(t, tt.wantPhase, agent.Status.Phase)
		})
	}
}

func newKubernetesRootConfig(vendor, peeringNLB string) *config.RootConfig {
	return &config.RootConfig{
		O11yConfig: config.ObservabilityInfra{
			Vendor:     vendor,
			PeeringNLB: peeringNLB,
			Version:    1,
			CustomerConfigsV2: config.CustomerConfigsV2{
				NextGen: config.CustomerNextGen{
					Repos: map[string]string{"vm": "https://charts.example.com"},
				},
			},
		},
	}
}
