package vpc

import (
	"context"
	"testing"
	"time"

	"github.com/go-logr/logr"
	"github.com/stretchr/testify/assert"
	"github.com/upbound/provider-aws/apis/ec2/v1beta1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/tools/record"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"
	"sigs.k8s.io/yaml"

	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/config"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/consts"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/vpc/task"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/deploy"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/repository"
	"github.com/tidbcloud/infra-provider/regional-server/test/testdata"
)

func getFakeVPC(t *testing.T) *v1alpha1.VPC {
	fakeVPC := &v1alpha1.VPC{}
	bs, err := testdata.LoadDefaultVPCYAML()
	assert.Nil(t, err)
	assert.Nil(t, yaml.Unmarshal(bs, fakeVPC))
	return fakeVPC
}

func fakeCli(t *testing.T) client.Client {
	s := runtime.NewScheme()
	v1alpha1.AddToScheme(s)
	v1beta1.SchemeBuilder.AddToScheme(s)
	fakeVPC := getFakeVPC(t)
	cli := fake.NewClientBuilder().WithScheme(s).WithObjects(fakeVPC).WithStatusSubresource(fakeVPC).Build()
	return cli
}

func Test_updateVPC(t *testing.T) {
	cli := fakeCli(t)
	vpcrw := repository.NewVPCReaderWriter(cli)
	fakeVPC := getFakeVPC(t)
	t.Run("update vpc success", func(t *testing.T) {
		vpcID := "vpc-123456"
		getVPC := &v1alpha1.VPC{}
		err := cli.Get(context.Background(), client.ObjectKey{Name: fakeVPC.Name, Namespace: fakeVPC.Namespace}, getVPC)
		getVPC.Status.VPC.VPCID = vpcID
		assert.Nil(t, err)
		err = updateVPC(context.Background(), vpcrw, getVPC)
		assert.Nil(t, err)
		getVPC = &v1alpha1.VPC{}
		err = cli.Get(context.Background(), client.ObjectKey{Name: fakeVPC.Name, Namespace: fakeVPC.Namespace}, getVPC)
		assert.Nil(t, err)
		assert.Equal(t, vpcID, getVPC.Status.VPC.VPCID)
		assert.Equal(t, vpcID, getVPC.Labels[v1alpha1.VPCIDKey])
	})
}

func TestVPCReconciler_Reconcile(t *testing.T) {
	fakeConfig := config.RootConfig{
		ControllerConfig: config.ControllerConfig{
			Provider: consts.ProviderTypeEKS,
		},
	}
	cli := fakeCli(t)
	recorder := record.NewFakeRecorder(10)
	reconciler := NewVPCReconciler(cli, logr.Discard(), cli.Scheme(), recorder, fakeConfig)
	fakeVPC := getFakeVPC(t)
	t.Run("create vpc failed", func(t *testing.T) {
		result, err := reconciler.Reconcile(context.Background(), ctrl.Request{
			NamespacedName: types.NamespacedName{Namespace: fakeVPC.Namespace, Name: fakeVPC.Name},
		})
		assert.Nil(t, err)
		assert.Equal(t, ctrl.Result{Requeue: true, RequeueAfter: time.Second * 30}, result)
	})
	t.Run("mismatch provider", func(t *testing.T) {
		getVPC := &v1alpha1.VPC{}
		err := cli.Get(context.Background(), client.ObjectKey{Name: fakeVPC.Name, Namespace: fakeVPC.Namespace}, getVPC)
		assert.Nil(t, err)
		getVPC.Status.Phase = v1alpha1.VPCPhaseCreating
		getVPC.Spec.Provider = consts.ProviderTypeGKE
		err = cli.Update(context.Background(), getVPC)
		assert.Nil(t, err)
		err = cli.Status().Update(context.Background(), getVPC)
		assert.Nil(t, err)
		result, err := reconciler.Reconcile(context.Background(), ctrl.Request{
			NamespacedName: types.NamespacedName{Namespace: fakeVPC.Namespace, Name: fakeVPC.Name},
		})
		assert.Nil(t, err)
		assert.Equal(t, ctrl.Result{}, result)
	})
}

func TestVPCReconciler_updateCreate(t *testing.T) {
	fakeConfig := config.RootConfig{
		ControllerConfig: config.ControllerConfig{
			Provider: consts.ProviderTypeEKS,
		},
	}
	cli := fakeCli(t)
	recorder := record.NewFakeRecorder(10)
	reconciler := NewVPCReconciler(cli, logr.Discard(), cli.Scheme(), recorder, fakeConfig)
	fakeVPC := getFakeVPC(t)
	t.Run("updateCreate success", func(t *testing.T) {
		getVPC := &v1alpha1.VPC{}
		assert.Nil(t, cli.Get(context.Background(), client.ObjectKey{Name: fakeVPC.Name, Namespace: fakeVPC.Namespace}, getVPC))
		getVPC.Status.Phase = v1alpha1.VPCPhaseCreating
		result, err := reconciler.updateCreate(context.Background(), logr.Discard(), &task.Context{
			VPC:   getVPC,
			VPCRW: repository.NewVPCReaderWriter(cli),
		}, []deploy.Result{})
		assert.Nil(t, err)
		assert.Equal(t, ctrl.Result{}, result)
		assert.Equal(t, v1alpha1.VPCPhaseActive, getVPC.Status.Phase)
		assert.Equal(t, "", getVPC.Status.Reason)
	})
	t.Run("updateCreate failed", func(t *testing.T) {
		getVPC := &v1alpha1.VPC{}
		assert.Nil(t, cli.Get(context.Background(), client.ObjectKey{Name: fakeVPC.Name, Namespace: fakeVPC.Namespace}, getVPC))
		getVPC.Status.Phase = v1alpha1.VPCPhaseCreating
		result, err := reconciler.updateCreate(context.Background(), logr.Discard(), &task.Context{
			VPC:   getVPC,
			VPCRW: repository.NewVPCReaderWriter(cli),
		}, []deploy.Result{deploy.Panic(assert.AnError)})
		assert.ErrorIs(t, err, assert.AnError)
		assert.Equal(t, ctrl.Result{}, result)
		assert.Equal(t, v1alpha1.VPCPhaseCreating, getVPC.Status.Phase)
		assert.Contains(t, getVPC.Status.Reason, "assert.AnError")
	})
}

func TestVPCReconciler_updateDelete(t *testing.T) {
	fakeConfig := config.RootConfig{
		ControllerConfig: config.ControllerConfig{
			Provider: consts.ProviderTypeEKS,
		},
	}
	cli := fakeCli(t)
	recorder := record.NewFakeRecorder(10)
	reconciler := NewVPCReconciler(cli, logr.Discard(), cli.Scheme(), recorder, fakeConfig)
	fakeVPC := getFakeVPC(t)
	t.Run("updateDelete success", func(t *testing.T) {
		getVPC := &v1alpha1.VPC{}
		getVPC.Status.Phase = v1alpha1.VPCPhaseDeleting
		getVPC.Finalizers = []string{v1alpha1.Finalizer}
		err := cli.Get(context.Background(), client.ObjectKey{Name: fakeVPC.Name, Namespace: fakeVPC.Namespace}, getVPC)
		result, err := reconciler.updateDelete(context.Background(), logr.Discard(), &task.Context{
			VPC:   getVPC,
			VPCRW: repository.NewVPCReaderWriter(cli),
		}, []deploy.Result{})
		assert.Nil(t, err)
		assert.Equal(t, ctrl.Result{}, result)
		assert.Equal(t, 0, len(getVPC.Finalizers))
	})
	t.Run("updateDelete failed", func(t *testing.T) {
		getVPC := &v1alpha1.VPC{}
		getVPC.Status.Phase = v1alpha1.VPCPhaseDeleting
		err := cli.Get(context.Background(), client.ObjectKey{Name: fakeVPC.Name, Namespace: fakeVPC.Namespace}, getVPC)
		result, err := reconciler.updateDelete(context.Background(), logr.Discard(), &task.Context{
			VPC:   getVPC,
			VPCRW: repository.NewVPCReaderWriter(cli),
		}, []deploy.Result{deploy.Panic(assert.AnError)})
		assert.ErrorIs(t, err, assert.AnError)
		assert.Equal(t, ctrl.Result{}, result)
	})
}

func Test_isNotReadyVPC(t *testing.T) {
	cases := []struct {
		name string
		vpc  *v1alpha1.VPC
		want bool
	}{
		{
			name: "vpc phase is empty",
			vpc:  &v1alpha1.VPC{},
			want: true,
		},
		{
			name: "vpc is pending",
			vpc:  &v1alpha1.VPC{Status: v1alpha1.VPCStatus{Phase: v1alpha1.VPCPhasePending}},
			want: true,
		},
		{
			name: "vpc is creating",
			vpc:  &v1alpha1.VPC{Status: v1alpha1.VPCStatus{Phase: v1alpha1.VPCPhaseCreating}},
			want: true,
		},
		{
			name: "vpc is failed",
			vpc:  &v1alpha1.VPC{Status: v1alpha1.VPCStatus{Phase: v1alpha1.VPCPhaseFailed}},
			want: true,
		},
		{
			name: "vpc is active",
			vpc:  &v1alpha1.VPC{Status: v1alpha1.VPCStatus{Phase: v1alpha1.VPCPhaseActive}},
			want: false,
		},
		{
			name: "vpc is deleting",
			vpc:  &v1alpha1.VPC{Status: v1alpha1.VPCStatus{Phase: v1alpha1.VPCPhaseDeleting}},
			want: false,
		},
	}

	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			assert.Equal(t, c.want, isNotReadyVPC(c.vpc))
		})
	}
}
