package parent

import (
	"context"
	"errors"
	"testing"

	"github.com/go-logr/logr/testr"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	observabilitypingcapcomv1 "github.com/tidbcloud/observability/api/regionalserver/resourceworker/v1"
	"github.com/tidbcloud/observability/pkg/resourceworker/controllers/serverless/task"
	k8sapiv1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

func TestMakeVMExternalService(t *testing.T) {
	tests := []struct {
		name              string
		namespace         string
		vmComponentPrefix string
		vmClusterName     string
		clusterID         string
		expectedName      string
		expectedExtName   string
	}{
		{
			name:              "vminsert service",
			namespace:         "test-namespace",
			vmComponentPrefix: vmInsertSvcPrefix,
			vmClusterName:     "test-vm-cluster",
			clusterID:         "test-cluster",
			expectedName:      "vminsert-test-cluster",
			expectedExtName:   "vminsert-test-vm-cluster.observability.svc.cluster.local",
		},
		{
			name:              "vmselect service",
			namespace:         "test-namespace",
			vmComponentPrefix: vmSelectSvcPrefix,
			vmClusterName:     "test-vm-cluster",
			clusterID:         "test-cluster",
			expectedName:      "vmselect-test-cluster",
			expectedExtName:   "vmselect-test-vm-cluster.observability.svc.cluster.local",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			obj, err := makeVMExternalService(tt.namespace, tt.vmComponentPrefix, tt.vmClusterName, tt.clusterID)
			require.NoError(t, err)
			require.NotNil(t, obj)

			svc, ok := obj.(*k8sapiv1.Service)
			require.True(t, ok)
			assert.Equal(t, tt.expectedName, svc.Name)
			assert.Equal(t, tt.namespace, svc.Namespace)
			assert.Equal(t, k8sapiv1.ServiceTypeExternalName, svc.Spec.Type)
			assert.Equal(t, tt.expectedExtName, svc.Spec.ExternalName)
		})
	}
}

func TestMakeVMSelectExternalService(t *testing.T) {
	cluster := &observabilitypingcapcomv1.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "test-cluster",
			Namespace: "test-namespace",
		},
		Spec: observabilitypingcapcomv1.ClusterSpec{
			VictoriaAccount: observabilitypingcapcomv1.VictoriaAccount{
				VMClusterName: "test-vm-cluster",
			},
		},
	}

	obj, err := makeVMSelectExternalService(cluster)
	require.NoError(t, err)
	require.NotNil(t, obj)

	svc, ok := obj.(*k8sapiv1.Service)
	require.True(t, ok)
	assert.Equal(t, "vmselect-test-cluster", svc.Name)
	assert.Equal(t, "test-namespace", svc.Namespace)
	assert.Equal(t, k8sapiv1.ServiceTypeExternalName, svc.Spec.Type)
	assert.Equal(t, "vmselect-test-vm-cluster.observability.svc.cluster.local", svc.Spec.ExternalName)
}

func TestMakeVMInsertExternalService(t *testing.T) {
	cluster := &observabilitypingcapcomv1.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "test-cluster",
			Namespace: "test-namespace",
		},
		Spec: observabilitypingcapcomv1.ClusterSpec{
			VictoriaAccount: observabilitypingcapcomv1.VictoriaAccount{
				VMClusterName: "test-vm-cluster",
			},
		},
	}

	obj, err := makeVMInsertExternalService(cluster)
	require.NoError(t, err)
	require.NotNil(t, obj)

	svc, ok := obj.(*k8sapiv1.Service)
	require.True(t, ok)
	assert.Equal(t, "vminsert-test-cluster", svc.Name)
	assert.Equal(t, "test-namespace", svc.Namespace)
	assert.Equal(t, k8sapiv1.ServiceTypeExternalName, svc.Spec.Type)
	assert.Equal(t, "vminsert-test-vm-cluster.observability.svc.cluster.local", svc.Spec.ExternalName)
}

func TestNewExternalService(t *testing.T) {
	ctx := &task.Context{
		Cluster: &observabilitypingcapcomv1.Cluster{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "test-cluster",
				Namespace: "test-namespace",
			},
		},
		Logger: testr.New(t),
	}

	es := NewExternalService(ctx)
	assert.NotNil(t, es)
	assert.Equal(t, ctx, es.Context)
}

func TestExternalService_Name(t *testing.T) {
	ctx := &task.Context{
		Cluster: &observabilitypingcapcomv1.Cluster{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "test-cluster",
				Namespace: "test-namespace",
			},
		},
		Logger: testr.New(t),
	}

	es := NewExternalService(ctx)
	assert.Equal(t, "ExternalService", es.Name())
}

func TestExternalService_Create_NilRemote(t *testing.T) {
	ctx := &task.Context{
		Cluster: &observabilitypingcapcomv1.Cluster{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "test-cluster",
				Namespace: "test-namespace",
			},
		},
		Logger: testr.New(t),
		Remote: nil,
	}

	es := NewExternalService(ctx)
	result := es.Create(context.Background())

	assert.False(t, result.IsDoneState())
	assert.NotNil(t, result.Error())
	assert.Contains(t, result.Error().Error(), "remote client is not initialized")
}

func TestExternalService_Create_NilCluster(t *testing.T) {
	scheme := runtime.NewScheme()
	_ = k8sapiv1.AddToScheme(scheme)
	_ = observabilitypingcapcomv1.AddToScheme(scheme)

	ctx := &task.Context{
		Cluster: nil,
		Logger:  testr.New(t),
		Remote:  fake.NewClientBuilder().WithScheme(scheme).Build(),
	}

	es := NewExternalService(ctx)
	result := es.Create(context.Background())

	assert.False(t, result.IsDoneState())
	assert.NotNil(t, result.Error())
	assert.Contains(t, result.Error().Error(), "cluster context is nil")
}

func TestExternalService_Create_Success(t *testing.T) {
	scheme := runtime.NewScheme()
	_ = k8sapiv1.AddToScheme(scheme)
	_ = observabilitypingcapcomv1.AddToScheme(scheme)

	cluster := &observabilitypingcapcomv1.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "test-cluster",
			Namespace: "test-namespace",
		},
		Spec: observabilitypingcapcomv1.ClusterSpec{
			VictoriaAccount: observabilitypingcapcomv1.VictoriaAccount{
				VMClusterName: "test-vm-cluster",
			},
		},
	}

	ctx := &task.Context{
		Cluster: cluster,
		Logger:  testr.New(t),
		Remote:  fake.NewClientBuilder().WithScheme(scheme).Build(),
	}

	es := NewExternalService(ctx)
	result := es.Create(context.Background())

	assert.True(t, result.IsDoneState())
	assert.Nil(t, result.Error())

	// Verify vminsert service was created
	vmInsertSvc := &k8sapiv1.Service{}
	err := ctx.Remote.Get(context.Background(), client.ObjectKey{
		Namespace: cluster.Namespace,
		Name:      vmInsertSvcPrefix + cluster.Name,
	}, vmInsertSvc)
	require.NoError(t, err)
	assert.Equal(t, vmInsertSvcPrefix+cluster.Name, vmInsertSvc.Name)
	assert.Equal(t, k8sapiv1.ServiceTypeExternalName, vmInsertSvc.Spec.Type)
	assert.Equal(t, vmInsertSvcPrefix+cluster.Spec.VictoriaAccount.VMClusterName+externalNameSuffix, vmInsertSvc.Spec.ExternalName)

	// Verify vmselect service was created
	vmSelectSvc := &k8sapiv1.Service{}
	err = ctx.Remote.Get(context.Background(), client.ObjectKey{
		Namespace: cluster.Namespace,
		Name:      vmSelectSvcPrefix + cluster.Name,
	}, vmSelectSvc)
	require.NoError(t, err)
	assert.Equal(t, vmSelectSvcPrefix+cluster.Name, vmSelectSvc.Name)
	assert.Equal(t, k8sapiv1.ServiceTypeExternalName, vmSelectSvc.Spec.Type)
	assert.Equal(t, vmSelectSvcPrefix+cluster.Spec.VictoriaAccount.VMClusterName+externalNameSuffix, vmSelectSvc.Spec.ExternalName)
}

func TestExternalService_Create_UpdateExisting(t *testing.T) {
	scheme := runtime.NewScheme()
	_ = k8sapiv1.AddToScheme(scheme)
	_ = observabilitypingcapcomv1.AddToScheme(scheme)

	cluster := &observabilitypingcapcomv1.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "test-cluster",
			Namespace: "test-namespace",
		},
		Spec: observabilitypingcapcomv1.ClusterSpec{
			VictoriaAccount: observabilitypingcapcomv1.VictoriaAccount{
				VMClusterName: "new-vm-cluster",
			},
		},
	}

	// Create existing services with old external names
	existingInsert := &k8sapiv1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Name:      vmInsertSvcPrefix + cluster.Name,
			Namespace: cluster.Namespace,
		},
		Spec: k8sapiv1.ServiceSpec{
			Type:         k8sapiv1.ServiceTypeExternalName,
			ExternalName: "old-vminsert-external-name",
		},
	}

	existingSelect := &k8sapiv1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Name:      vmSelectSvcPrefix + cluster.Name,
			Namespace: cluster.Namespace,
		},
		Spec: k8sapiv1.ServiceSpec{
			Type:         k8sapiv1.ServiceTypeExternalName,
			ExternalName: "old-vmselect-external-name",
		},
	}

	ctx := &task.Context{
		Cluster: cluster,
		Logger:  testr.New(t),
		Remote:  fake.NewClientBuilder().WithScheme(scheme).WithObjects(existingInsert, existingSelect).Build(),
	}

	es := NewExternalService(ctx)
	result := es.Create(context.Background())

	assert.True(t, result.IsDoneState())
	assert.Nil(t, result.Error())

	// Verify vminsert service was updated
	vmInsertSvc := &k8sapiv1.Service{}
	err := ctx.Remote.Get(context.Background(), client.ObjectKey{
		Namespace: cluster.Namespace,
		Name:      vmInsertSvcPrefix + cluster.Name,
	}, vmInsertSvc)
	require.NoError(t, err)
	assert.Equal(t, vmInsertSvcPrefix+cluster.Spec.VictoriaAccount.VMClusterName+externalNameSuffix, vmInsertSvc.Spec.ExternalName)

	// Verify vmselect service was updated
	vmSelectSvc := &k8sapiv1.Service{}
	err = ctx.Remote.Get(context.Background(), client.ObjectKey{
		Namespace: cluster.Namespace,
		Name:      vmSelectSvcPrefix + cluster.Name,
	}, vmSelectSvc)
	require.NoError(t, err)
	assert.Equal(t, vmSelectSvcPrefix+cluster.Spec.VictoriaAccount.VMClusterName+externalNameSuffix, vmSelectSvc.Spec.ExternalName)
}

func TestExternalService_Delete_NilRemote(t *testing.T) {
	ctx := &task.Context{
		Cluster: &observabilitypingcapcomv1.Cluster{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "test-cluster",
				Namespace: "test-namespace",
			},
		},
		Logger: testr.New(t),
		Remote: nil,
	}

	es := NewExternalService(ctx)
	result := es.Delete(context.Background())

	assert.False(t, result.IsDoneState())
	assert.NotNil(t, result.Error())
	assert.Contains(t, result.Error().Error(), "remote client is not initialized")
}

func TestExternalService_Delete_NilCluster(t *testing.T) {
	scheme := runtime.NewScheme()
	_ = k8sapiv1.AddToScheme(scheme)
	_ = observabilitypingcapcomv1.AddToScheme(scheme)

	ctx := &task.Context{
		Cluster: nil,
		Logger:  testr.New(t),
		Remote:  fake.NewClientBuilder().WithScheme(scheme).Build(),
	}

	es := NewExternalService(ctx)
	result := es.Delete(context.Background())

	assert.False(t, result.IsDoneState())
	assert.NotNil(t, result.Error())
	assert.Contains(t, result.Error().Error(), "cluster context is nil")
}

func TestExternalService_Delete_Success(t *testing.T) {
	scheme := runtime.NewScheme()
	_ = k8sapiv1.AddToScheme(scheme)
	_ = observabilitypingcapcomv1.AddToScheme(scheme)

	cluster := &observabilitypingcapcomv1.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "test-cluster",
			Namespace: "test-namespace",
		},
	}

	// Create services to delete
	vmInsertSvc := &k8sapiv1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Name:      vmInsertSvcPrefix + cluster.Name,
			Namespace: cluster.Namespace,
		},
		Spec: k8sapiv1.ServiceSpec{
			Type: k8sapiv1.ServiceTypeExternalName,
		},
	}

	vmSelectSvc := &k8sapiv1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Name:      vmSelectSvcPrefix + cluster.Name,
			Namespace: cluster.Namespace,
		},
		Spec: k8sapiv1.ServiceSpec{
			Type: k8sapiv1.ServiceTypeExternalName,
		},
	}

	ctx := &task.Context{
		Cluster: cluster,
		Logger:  testr.New(t),
		Remote:  fake.NewClientBuilder().WithScheme(scheme).WithObjects(vmInsertSvc, vmSelectSvc).Build(),
	}

	es := NewExternalService(ctx)
	result := es.Delete(context.Background())

	assert.True(t, result.IsDoneState())
	assert.Nil(t, result.Error())

	// Verify vminsert service was deleted
	deleted := &k8sapiv1.Service{}
	err := ctx.Remote.Get(context.Background(), client.ObjectKey{
		Namespace: cluster.Namespace,
		Name:      vmInsertSvcPrefix + cluster.Name,
	}, deleted)
	assert.True(t, apierrors.IsNotFound(err))

	// Verify vmselect service was deleted
	err = ctx.Remote.Get(context.Background(), client.ObjectKey{
		Namespace: cluster.Namespace,
		Name:      vmSelectSvcPrefix + cluster.Name,
	}, deleted)
	assert.True(t, apierrors.IsNotFound(err))
}

func TestExternalService_Delete_NotFound(t *testing.T) {
	scheme := runtime.NewScheme()
	_ = k8sapiv1.AddToScheme(scheme)
	_ = observabilitypingcapcomv1.AddToScheme(scheme)

	cluster := &observabilitypingcapcomv1.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "test-cluster",
			Namespace: "test-namespace",
		},
	}

	ctx := &task.Context{
		Cluster: cluster,
		Logger:  testr.New(t),
		Remote:  fake.NewClientBuilder().WithScheme(scheme).Build(),
	}

	es := NewExternalService(ctx)
	result := es.Delete(context.Background())

	assert.True(t, result.IsDoneState())
	assert.Nil(t, result.Error())
}

func TestExternalService_Delete_PartialNotFound(t *testing.T) {
	scheme := runtime.NewScheme()
	_ = k8sapiv1.AddToScheme(scheme)
	_ = observabilitypingcapcomv1.AddToScheme(scheme)

	cluster := &observabilitypingcapcomv1.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "test-cluster",
			Namespace: "test-namespace",
		},
	}

	// Only create vminsert service, vmselect is not found
	vmInsertSvc := &k8sapiv1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Name:      vmInsertSvcPrefix + cluster.Name,
			Namespace: cluster.Namespace,
		},
		Spec: k8sapiv1.ServiceSpec{
			Type: k8sapiv1.ServiceTypeExternalName,
		},
	}

	ctx := &task.Context{
		Cluster: cluster,
		Logger:  testr.New(t),
		Remote:  fake.NewClientBuilder().WithScheme(scheme).WithObjects(vmInsertSvc).Build(),
	}

	es := NewExternalService(ctx)
	result := es.Delete(context.Background())

	assert.True(t, result.IsDoneState())
	assert.Nil(t, result.Error())

	// Verify vminsert service was deleted
	deleted := &k8sapiv1.Service{}
	err := ctx.Remote.Get(context.Background(), client.ObjectKey{
		Namespace: cluster.Namespace,
		Name:      vmInsertSvcPrefix + cluster.Name,
	}, deleted)
	assert.True(t, apierrors.IsNotFound(err))
}

// Mock client for error scenarios
type mockExternalSvcErrorClient struct {
	client.Client
	getError    error
	deleteError error
	scheme      *runtime.Scheme
}

func (m *mockExternalSvcErrorClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
	if m.getError != nil {
		return m.getError
	}
	// If no get error, return success by setting a minimal object
	if svc, ok := obj.(*k8sapiv1.Service); ok {
		svc.SetName(key.Name)
		svc.SetNamespace(key.Namespace)
		return nil
	}
	return apierrors.NewNotFound(schema.GroupResource{}, key.Name)
}

func (m *mockExternalSvcErrorClient) Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error {
	if m.deleteError != nil {
		return m.deleteError
	}
	return nil
}

func (m *mockExternalSvcErrorClient) Scheme() *runtime.Scheme {
	if m.scheme == nil {
		m.scheme = runtime.NewScheme()
		_ = k8sapiv1.AddToScheme(m.scheme)
		_ = observabilitypingcapcomv1.AddToScheme(m.scheme)
	}
	return m.scheme
}

func TestExternalService_Delete_GetError(t *testing.T) {
	cluster := &observabilitypingcapcomv1.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "test-cluster",
			Namespace: "test-namespace",
		},
	}

	mockClient := &mockExternalSvcErrorClient{
		getError: errors.New("get error"),
	}

	ctx := &task.Context{
		Cluster: cluster,
		Logger:  testr.New(t),
		Remote:  mockClient,
	}

	es := NewExternalService(ctx)
	result := es.Delete(context.Background())

	assert.False(t, result.IsDoneState())
	assert.NotNil(t, result.Error())
	assert.Contains(t, result.Error().Error(), "get error")
}

func TestExternalService_Delete_DeleteError(t *testing.T) {
	cluster := &observabilitypingcapcomv1.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "test-cluster",
			Namespace: "test-namespace",
		},
	}

	mockClient := &mockExternalSvcErrorClient{
		getError:    nil,
		deleteError: errors.New("delete error"),
	}

	ctx := &task.Context{
		Cluster: cluster,
		Logger:  testr.New(t),
		Remote:  mockClient,
	}

	es := NewExternalService(ctx)
	result := es.Delete(context.Background())

	assert.False(t, result.IsDoneState())
	assert.NotNil(t, result.Error())
	assert.Contains(t, result.Error().Error(), "delete error")
}
