package common

import (
	"context"
	"testing"

	route53v1alpha1 "github.com/crossplane-contrib/provider-aws/apis/route53/v1alpha1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/crossplane/crossplane-runtime/pkg/meta"
	opv1alpha1 "github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1"
	"github.com/stretchr/testify/require"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"

	clustertask "github.com/tidbcloud/eks-provider/controllers/cluster/task"
	"github.com/tidbcloud/eks-provider/utils/tidb-operator/tidbcluster"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/crossplane/route53"
	"github.com/tidbcloud/infra-provider/pkg/dataplane"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
	"github.com/tidbcloud/infra-provider/pkg/namer"
)

func TestApplyRoute53RecordAndPause(t *testing.T) {
	tests := []struct {
		name          string
		networkName   string
		legacyNetwork *cloudv1.Network
		stable        bool
		wantPaused    bool
		wantRequeue   bool
	}{
		{name: "stable record", stable: true, wantPaused: true},
		{name: "unstable record", wantRequeue: true},
		{
			name:          "legacy record",
			networkName:   "legacy-network",
			legacyNetwork: &cloudv1.Network{},
			stable:        true,
			wantPaused:    true,
		},
		{
			name:        "migrated legacy record",
			networkName: "legacy-network",
			stable:      true,
			wantPaused:  true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			existing := &route53v1alpha1.ResourceRecordSet{
				ObjectMeta: metav1.ObjectMeta{Name: "sql.example.com"},
			}
			if tt.stable {
				existing.Status.SetConditions(xpv1.Available(), xpv1.ReconcileSuccess())
			}
			scheme := runtime.NewScheme()
			require.NoError(t, route53v1alpha1.SchemeBuilder.AddToScheme(scheme))
			cli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(existing).Build()
			cluster := &cloudv1.Cluster{Spec: cloudv1.ClusterSpec{NetworkName: tt.networkName}}
			task := NewFullService(&clustertask.Context{
				Cluster:       cluster,
				Local:         cli,
				LegacyNetwork: tt.legacyNetwork,
			})

			require.NoError(t, task.applyRoute53RecordAndPause(context.Background(), existing.DeepCopy()))

			current := &route53v1alpha1.ResourceRecordSet{}
			require.NoError(t, cli.Get(context.Background(), client.ObjectKey{Name: existing.Name}, current))
			require.Equal(t, tt.wantPaused, meta.IsPaused(current))
			require.Equal(t, tt.wantRequeue, task.needsPauseRequeue)
		})
	}
}

func TestFullServiceCreatePausesStablePublicAndPrivateRecords(t *testing.T) {
	enabled := true
	cluster := &cloudv1.Cluster{
		ObjectMeta: metav1.ObjectMeta{Namespace: "ns", Name: "cluster"},
		Spec: cloudv1.ClusterSpec{SqlEndpoints: cloudv1.SqlEndpoints{
			Public:  cloudv1.Service{Enabled: &enabled},
			Private: &cloudv1.Service{Enabled: &enabled},
		}},
	}
	n := namer.NewNamer().
		WithClusterName(cluster.Name).
		WithTiDBNamespace(cluster.PrefixedId()).
		WithClusterServerDNSNames("identity", "example.com")

	remoteScheme := runtime.NewScheme()
	require.NoError(t, corev1.AddToScheme(remoteScheme))
	remote := fake.NewClientBuilder().WithScheme(remoteScheme).WithObjects(
		&corev1.Service{
			ObjectMeta: metav1.ObjectMeta{Namespace: n.GetTiDBNamespace(), Name: n.GetPublicTiDBServiceName()},
			Status:     corev1.ServiceStatus{LoadBalancer: corev1.LoadBalancerStatus{Ingress: []corev1.LoadBalancerIngress{{Hostname: "public-lb.example.com"}}}},
		},
		&corev1.Service{
			ObjectMeta: metav1.ObjectMeta{Namespace: n.GetTiDBNamespace(), Name: n.GetPrivateTiDBServiceName()},
			Status:     corev1.ServiceStatus{LoadBalancer: corev1.LoadBalancerStatus{Ingress: []corev1.LoadBalancerIngress{{Hostname: "private-lb.example.com"}}}},
		},
	).Build()

	localScheme := runtime.NewScheme()
	require.NoError(t, route53v1alpha1.SchemeBuilder.AddToScheme(localScheme))
	public := newStableRoute53Record(n.GetPublicEndpointAddress(), "public-lb.example.com")
	private := newStableRoute53Record(n.GetPrivateEndpointAddress(), "private-lb.example.com")
	local := fake.NewClientBuilder().WithScheme(localScheme).WithObjects(public, private).Build()
	task := NewFullService(&clustertask.Context{
		Cluster:     cluster,
		Namer:       n,
		Local:       local,
		EnvProvider: &dataplane.FakeEnvProvider{Client: remote},
		TCGetter:    tidbcluster.NewV1Getter(&opv1alpha1.TidbCluster{}),
	})

	result := task.Create(context.Background())
	require.NoError(t, result.Error())
	require.False(t, task.needsPauseRequeue)
	for _, testCase := range []struct {
		record *route53v1alpha1.ResourceRecordSet
		target string
	}{
		{record: public, target: "public-lb.example.com"},
		{record: private, target: "private-lb.example.com"},
	} {
		current := &route53v1alpha1.ResourceRecordSet{}
		require.NoError(t, local.Get(context.Background(), client.ObjectKey{Name: testCase.record.Name}, current))
		require.True(t, meta.IsPaused(current), testCase.record.Name)
		require.Equal(t, testCase.target, current.Spec.ForProvider.ResourceRecords[0].Value, testCase.record.Name)
	}
}

func TestFullServiceCreateRequeuesWhenRoute53RecordIsNotStable(t *testing.T) {
	enabled := true
	cluster := &cloudv1.Cluster{
		ObjectMeta: metav1.ObjectMeta{Namespace: "ns", Name: "cluster"},
		Spec: cloudv1.ClusterSpec{SqlEndpoints: cloudv1.SqlEndpoints{
			Public:  cloudv1.Service{Enabled: &enabled},
			Private: &cloudv1.Service{Enabled: &enabled},
		}},
	}
	n := namer.NewNamer().
		WithClusterName(cluster.Name).
		WithTiDBNamespace(cluster.PrefixedId()).
		WithClusterServerDNSNames("identity", "example.com")
	remoteScheme := runtime.NewScheme()
	require.NoError(t, corev1.AddToScheme(remoteScheme))
	remote := fake.NewClientBuilder().WithScheme(remoteScheme).WithObjects(
		&corev1.Service{
			ObjectMeta: metav1.ObjectMeta{Namespace: n.GetTiDBNamespace(), Name: n.GetPublicTiDBServiceName()},
			Status:     corev1.ServiceStatus{LoadBalancer: corev1.LoadBalancerStatus{Ingress: []corev1.LoadBalancerIngress{{Hostname: "public-lb.example.com"}}}},
		},
		&corev1.Service{
			ObjectMeta: metav1.ObjectMeta{Namespace: n.GetTiDBNamespace(), Name: n.GetPrivateTiDBServiceName()},
			Status:     corev1.ServiceStatus{LoadBalancer: corev1.LoadBalancerStatus{Ingress: []corev1.LoadBalancerIngress{{Hostname: "private-lb.example.com"}}}},
		},
	).Build()
	localScheme := runtime.NewScheme()
	require.NoError(t, route53v1alpha1.SchemeBuilder.AddToScheme(localScheme))
	local := fake.NewClientBuilder().WithScheme(localScheme).Build()
	task := NewFullService(&clustertask.Context{
		Cluster:     cluster,
		Namer:       n,
		Local:       local,
		EnvProvider: &dataplane.FakeEnvProvider{Client: remote},
		TCGetter:    tidbcluster.NewV1Getter(&opv1alpha1.TidbCluster{}),
	})

	result := task.Create(context.Background())
	require.NoError(t, result.Error())
	requeue := k8s.ShouldRequeue(result.RawError())
	require.True(t, requeue.Requeue)
	require.Equal(t, route53.RequeueAfter, requeue.RequeueAfter)
}

func newStableRoute53Record(name, target string) *route53v1alpha1.ResourceRecordSet {
	rrs := &route53v1alpha1.ResourceRecordSet{ObjectMeta: metav1.ObjectMeta{Name: name}}
	rrs.Spec.ForProvider.ResourceRecords = []route53v1alpha1.ResourceRecord{{Value: target}}
	rrs.Status.SetConditions(xpv1.Available(), xpv1.ReconcileSuccess())
	return rrs
}
