package action

import (
	"context"
	"errors"
	"testing"

	"github.com/stretchr/testify/mock"
	"github.com/stretchr/testify/require"
	apiv1 "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	dplanemgrv1 "github.com/tidbcloud/cluster-service-ng/api/dataplane-manager/v1"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/domain/workflow"
	dplanemgrmocks "github.com/tidbcloud/cluster-service-ng/pkg/dataplanemgr/mocks"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/actions"
	"github.com/tidbcloud/cluster-service-ng/pkg/storage/mysql/schema"
	repomocks "github.com/tidbcloud/cluster-service-ng/pkg/storage/repo/mocks"
	o11yv2 "github.com/tidbcloud/observability/agent-control-server/api/v2"
	"google.golang.org/protobuf/encoding/protojson"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

func TestCreateO11yTiDBGroupAgentActionSkipsInternalTiDBGroup(t *testing.T) {
	ctx := context.Background()
	kubeClient := fake.NewClientBuilder().WithScheme(newO11yTestScheme(t)).Build()
	tidbGroup := newO11yTestTiDBGroup(apiv1.TiDBGroupType_INTERNAL)

	action := NewCreateO11yTiDBGroupAgentAction(
		"test-biz",
		false,
		dplanemgrmocks.NewClient(t),
		kubeClient,
		repomocks.NewSharedPoolRepo(t),
	)

	require.NoError(t, action.Invoke(newO11yCreateArgs(t, tidbGroup)))

	var agents o11yv2.TiDBGroupAgentList
	require.NoError(t, kubeClient.List(ctx, &agents, client.InNamespace(entity.GetNamespace())))
	require.Empty(t, agents.Items)
}

func TestCreateO11yTiDBGroupAgentActionCreatesDefaultWithLabels(t *testing.T) {
	ctx := context.Background()
	kubeClient := fake.NewClientBuilder().WithScheme(newO11yTestScheme(t)).Build()
	tidbGroup := newO11yTestTiDBGroup(apiv1.TiDBGroupType_DEFAULT)
	dataplaneClient := dplanemgrmocks.NewClient(t)
	sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
	dataplaneClient.EXPECT().
		GetDataPlane(mock.Anything, &dplanemgrv1.GetDataPlaneRequest{DataPlaneId: "dp-1"}).
		Return(&dplanemgrv1.DataPlane{
			Kubernetes: &dplanemgrv1.DataPlane_Kubernetes{Name: "k8s-1"},
		}, nil).
		Once()
	sharedPoolRepo.EXPECT().
		Load(mock.Anything, "sp-1", false).
		Return(&entity.SharedPool{SharedPool: apiv1.SharedPool{Id: "sp-1"}}, nil).
		Once()

	action := NewCreateO11yTiDBGroupAgentAction(
		"test-biz",
		false,
		dataplaneClient,
		kubeClient,
		sharedPoolRepo,
	)

	require.NoError(t, action.Invoke(newO11yCreateArgs(t, tidbGroup)))

	var got o11yv2.TiDBGroupAgent
	require.NoError(t, kubeClient.Get(ctx, types.NamespacedName{Namespace: entity.GetNamespace(), Name: "tg-1"}, &got))
	require.Equal(t, "cluster-1", got.Labels[entity.O11yTiDBGroupAgentClusterIDLabelKey])
	require.Equal(t, "tg-1", got.Labels[entity.O11yTiDBGroupAgentGroupIDLabelKey])
	require.Equal(t, "cluster-1", got.Spec.ClusterID)
	require.Equal(t, "cluster-1", got.Spec.O11y.ApplicationID)
	require.Equal(t, "tg-1", got.Spec.TiDBGroup)
}

func TestCreateO11yTiDBGroupAgentActionUsesConfiguredNamespace(t *testing.T) {
	ctx := context.Background()
	kubeClient := fake.NewClientBuilder().WithScheme(newO11yTestScheme(t)).Build()
	tidbGroup := newO11yTestTiDBGroup(apiv1.TiDBGroupType_DEFAULT)
	dataplaneClient := dplanemgrmocks.NewClient(t)
	sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
	dataplaneClient.EXPECT().
		GetDataPlane(mock.Anything, &dplanemgrv1.GetDataPlaneRequest{DataPlaneId: "dp-1"}).
		Return(&dplanemgrv1.DataPlane{
			Kubernetes: &dplanemgrv1.DataPlane_Kubernetes{Name: "k8s-1"},
		}, nil).
		Once()
	sharedPoolRepo.EXPECT().
		Load(mock.Anything, "sp-1", false).
		Return(&entity.SharedPool{SharedPool: apiv1.SharedPool{Id: "sp-1"}}, nil).
		Once()

	action := NewCreateO11yTiDBGroupAgentAction(
		"test-biz",
		false,
		dataplaneClient,
		kubeClient,
		sharedPoolRepo,
		"cp9-premium",
	)

	require.NoError(t, action.Invoke(newO11yCreateArgs(t, tidbGroup)))

	var got o11yv2.TiDBGroupAgent
	require.NoError(t, kubeClient.Get(ctx, types.NamespacedName{Namespace: "cp9-premium", Name: "tg-1"}, &got))
	require.Error(t, kubeClient.Get(ctx, types.NamespacedName{Namespace: entity.GetNamespace(), Name: "tg-1"}, &o11yv2.TiDBGroupAgent{}))
}

func TestCreateO11yTiDBGroupAgentActionRepairsExistingLabels(t *testing.T) {
	ctx := context.Background()
	existing := &o11yv2.TiDBGroupAgent{}
	existing.Name = "tg-1"
	existing.Namespace = entity.GetNamespace()
	kubeClient := fake.NewClientBuilder().
		WithScheme(newO11yTestScheme(t)).
		WithObjects(existing).
		Build()
	tidbGroup := newO11yTestTiDBGroup(apiv1.TiDBGroupType_DEFAULT)
	dataplaneClient := dplanemgrmocks.NewClient(t)
	sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
	dataplaneClient.EXPECT().
		GetDataPlane(mock.Anything, &dplanemgrv1.GetDataPlaneRequest{DataPlaneId: "dp-1"}).
		Return(&dplanemgrv1.DataPlane{
			Kubernetes: &dplanemgrv1.DataPlane_Kubernetes{Name: "k8s-1"},
		}, nil).
		Once()
	sharedPoolRepo.EXPECT().
		Load(mock.Anything, "sp-1", false).
		Return(&entity.SharedPool{SharedPool: apiv1.SharedPool{Id: "sp-1"}}, nil).
		Once()

	action := NewCreateO11yTiDBGroupAgentAction(
		"test-biz",
		false,
		dataplaneClient,
		kubeClient,
		sharedPoolRepo,
	)

	require.NoError(t, action.Invoke(newO11yCreateArgs(t, tidbGroup)))

	var got o11yv2.TiDBGroupAgent
	require.NoError(t, kubeClient.Get(ctx, types.NamespacedName{Namespace: entity.GetNamespace(), Name: "tg-1"}, &got))
	require.Equal(t, "cluster-1", got.Labels[entity.O11yTiDBGroupAgentClusterIDLabelKey])
	require.Equal(t, "tg-1", got.Labels[entity.O11yTiDBGroupAgentGroupIDLabelKey])
}

func TestCreateO11yTiDBGroupAgentActionRemovesStaleManagedAnnotations(t *testing.T) {
	ctx := context.Background()
	existing := &o11yv2.TiDBGroupAgent{}
	existing.Name = "tg-1"
	existing.Namespace = entity.GetNamespace()
	existing.Annotations = map[string]string{
		tidbNodeExporterScrapeAnnotation: "true",
		tiproxyDeployTypeAnnotation:      schema.SharedPoolDeployTypeSharedTiProxy,
		"unrelated":                      "keep",
	}
	kubeClient := fake.NewClientBuilder().
		WithScheme(newO11yTestScheme(t)).
		WithObjects(existing).
		Build()
	tidbGroup := newO11yTestTiDBGroup(apiv1.TiDBGroupType_DEFAULT)
	dataplaneClient := dplanemgrmocks.NewClient(t)
	sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
	dataplaneClient.EXPECT().
		GetDataPlane(mock.Anything, &dplanemgrv1.GetDataPlaneRequest{DataPlaneId: "dp-1"}).
		Return(&dplanemgrv1.DataPlane{
			Kubernetes: &dplanemgrv1.DataPlane_Kubernetes{Name: "k8s-1"},
		}, nil).
		Once()
	sharedPoolRepo.EXPECT().
		Load(mock.Anything, "sp-1", false).
		Return(&entity.SharedPool{SharedPool: apiv1.SharedPool{Id: "sp-1"}}, nil).
		Once()

	action := NewCreateO11yTiDBGroupAgentAction(
		"test-biz",
		false,
		dataplaneClient,
		kubeClient,
		sharedPoolRepo,
	)

	require.NoError(t, action.Invoke(newO11yCreateArgs(t, tidbGroup)))

	var got o11yv2.TiDBGroupAgent
	require.NoError(t, kubeClient.Get(ctx, types.NamespacedName{Namespace: entity.GetNamespace(), Name: "tg-1"}, &got))
	require.NotContains(t, got.Annotations, tidbNodeExporterScrapeAnnotation)
	require.NotContains(t, got.Annotations, tiproxyDeployTypeAnnotation)
	require.Equal(t, "keep", got.Annotations["unrelated"])
}

func TestCreateO11yTiDBGroupAgentActionSetsManagedAnnotations(t *testing.T) {
	ctx := context.Background()
	kubeClient := fake.NewClientBuilder().WithScheme(newO11yTestScheme(t)).Build()
	tidbGroup := newO11yTestTiDBGroup(apiv1.TiDBGroupType_DEFAULT)
	dataplaneClient := dplanemgrmocks.NewClient(t)
	sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
	dataplaneClient.EXPECT().
		GetDataPlane(mock.Anything, &dplanemgrv1.GetDataPlaneRequest{DataPlaneId: "dp-1"}).
		Return(&dplanemgrv1.DataPlane{
			Kubernetes: &dplanemgrv1.DataPlane_Kubernetes{Name: "k8s-1"},
		}, nil).
		Once()
	sharedPoolRepo.EXPECT().
		Load(mock.Anything, "sp-1", false).
		Return(&entity.SharedPool{
			SharedPool: apiv1.SharedPool{
				Id:           "sp-1",
				CapacityMode: apiv1.CapacityMode_TOPOLOGY_DRIVEN,
			},
			DeployType: schema.SharedPoolDeployTypeSharedTiProxy,
		}, nil).
		Once()

	action := NewCreateO11yTiDBGroupAgentAction(
		"test-biz",
		false,
		dataplaneClient,
		kubeClient,
		sharedPoolRepo,
	)

	require.NoError(t, action.Invoke(newO11yCreateArgs(t, tidbGroup)))

	var got o11yv2.TiDBGroupAgent
	require.NoError(t, kubeClient.Get(ctx, types.NamespacedName{Namespace: entity.GetNamespace(), Name: "tg-1"}, &got))
	require.Equal(t, "true", got.Annotations[tidbNodeExporterScrapeAnnotation])
	require.Equal(t, schema.SharedPoolDeployTypeSharedTiProxy, got.Annotations[tiproxyDeployTypeAnnotation])
}

func TestCreateO11yTiDBGroupAgentActionFailsWhenSharedPoolLoadFails(t *testing.T) {
	ctx := context.Background()
	kubeClient := fake.NewClientBuilder().WithScheme(newO11yTestScheme(t)).Build()
	tidbGroup := newO11yTestTiDBGroup(apiv1.TiDBGroupType_DEFAULT)
	dataplaneClient := dplanemgrmocks.NewClient(t)
	sharedPoolRepo := repomocks.NewSharedPoolRepo(t)
	dataplaneClient.EXPECT().
		GetDataPlane(mock.Anything, &dplanemgrv1.GetDataPlaneRequest{DataPlaneId: "dp-1"}).
		Return(&dplanemgrv1.DataPlane{
			Kubernetes: &dplanemgrv1.DataPlane_Kubernetes{Name: "k8s-1"},
		}, nil).
		Once()
	sharedPoolRepo.EXPECT().
		Load(mock.Anything, "sp-1", false).
		Return(nil, errors.New("repo unavailable")).
		Once()

	action := NewCreateO11yTiDBGroupAgentAction(
		"test-biz",
		false,
		dataplaneClient,
		kubeClient,
		sharedPoolRepo,
	)

	err := action.Invoke(newO11yCreateArgs(t, tidbGroup))
	require.ErrorContains(t, err, "failed to load shared pool sp-1")

	var agents o11yv2.TiDBGroupAgentList
	require.NoError(t, kubeClient.List(ctx, &agents, client.InNamespace(entity.GetNamespace())))
	require.Empty(t, agents.Items)
}

func TestDeleteO11yTiDBGroupAgentActionIsNoop(t *testing.T) {
	ctx := context.Background()
	existing := &o11yv2.TiDBGroupAgent{}
	existing.Name = "tg-1"
	existing.Namespace = entity.GetNamespace()
	kubeClient := fake.NewClientBuilder().
		WithScheme(newO11yTestScheme(t)).
		WithObjects(existing).
		Build()

	action := NewDeleteO11yTiDBGroupAgentAction(false)
	require.NoError(t, action.Invoke(actions.ActionArgs{
		workflow.TiDBGroupIdKey: actions.ActionArgString("tg-1"),
	}))

	var got o11yv2.TiDBGroupAgent
	require.NoError(t, kubeClient.Get(ctx, types.NamespacedName{Namespace: entity.GetNamespace(), Name: "tg-1"}, &got))
}

func TestDeleteO11yTiDBGroupAgentsByClusterActionDeletesMatchingAgents(t *testing.T) {
	ctx := context.Background()
	matched := newO11yTestAgent("tg-1", "cluster-1")
	alsoMatched := newO11yTestAgent("tg-2", "cluster-1")
	unmatched := newO11yTestAgent("tg-3", "cluster-2")
	kubeClient := fake.NewClientBuilder().
		WithScheme(newO11yTestScheme(t)).
		WithObjects(matched, alsoMatched, unmatched).
		Build()

	action := NewDeleteO11yTiDBGroupAgentsByClusterAction(false, kubeClient)
	require.NoError(t, action.Invoke(actions.ActionArgs{
		workflow.TiDBClusterIdKey: actions.ActionArgString("cluster-1"),
	}))

	var got o11yv2.TiDBGroupAgent
	require.Error(t, kubeClient.Get(ctx, types.NamespacedName{Namespace: entity.GetNamespace(), Name: "tg-1"}, &got))
	require.Error(t, kubeClient.Get(ctx, types.NamespacedName{Namespace: entity.GetNamespace(), Name: "tg-2"}, &got))
	require.NoError(t, kubeClient.Get(ctx, types.NamespacedName{Namespace: entity.GetNamespace(), Name: "tg-3"}, &got))
}

func TestDeleteO11yTiDBGroupAgentsByClusterActionOnlyDeletesConfiguredNamespace(t *testing.T) {
	ctx := context.Background()
	premium := newO11yTestAgent("tg-1", "cluster-1")
	premium.Namespace = "cp9-premium"
	byoc := newO11yTestAgent("tg-1", "cluster-1")
	byoc.Namespace = "cp-byoc"
	kubeClient := fake.NewClientBuilder().
		WithScheme(newO11yTestScheme(t)).
		WithObjects(premium, byoc).
		Build()

	action := NewDeleteO11yTiDBGroupAgentsByClusterAction(false, kubeClient, "cp9-premium")
	require.NoError(t, action.Invoke(actions.ActionArgs{
		workflow.TiDBClusterIdKey: actions.ActionArgString("cluster-1"),
	}))

	require.Error(t, kubeClient.Get(ctx, types.NamespacedName{Namespace: "cp9-premium", Name: "tg-1"}, &o11yv2.TiDBGroupAgent{}))
	require.NoError(t, kubeClient.Get(ctx, types.NamespacedName{Namespace: "cp-byoc", Name: "tg-1"}, &o11yv2.TiDBGroupAgent{}))
}

func newO11yTestScheme(t *testing.T) *runtime.Scheme {
	t.Helper()
	scheme := runtime.NewScheme()
	require.NoError(t, o11yv2.AddToScheme(scheme))
	return scheme
}

func newO11yCreateArgs(t *testing.T, tidbGroup *entity.TiDBGroup) actions.ActionArgs {
	t.Helper()
	tidbGroupStr, err := protojson.Marshal(tidbGroup)
	require.NoError(t, err)
	return actions.ActionArgs{
		workflow.TiDBGroupIdKey: actions.ActionArgString(tidbGroup.GetId()),
		workflow.TiDBGroupKey:   actions.ActionArgString(string(tidbGroupStr)),
		workflow.DataplaneIdKey: actions.ActionArgString("dp-1"),
	}
}

func newO11yTestTiDBGroup(groupType apiv1.TiDBGroupType) *entity.TiDBGroup {
	return &entity.TiDBGroup{
		TiDBGroup: apiv1.TiDBGroup{
			Id:           "tg-1",
			ClusterId:    "cluster-1",
			TenantId:     "tenant-1",
			SharedPoolId: "sp-1",
			Type:         groupType,
		},
	}
}

func newO11yTestAgent(name, clusterID string) *o11yv2.TiDBGroupAgent {
	agent := &o11yv2.TiDBGroupAgent{}
	agent.Name = name
	agent.Namespace = entity.GetNamespace()
	agent.Labels = map[string]string{entity.O11yTiDBGroupAgentClusterIDLabelKey: clusterID}
	agent.Spec.ClusterID = clusterID
	agent.Spec.TiDBGroup = name
	return agent
}
