package ack

import (
	"context"
	"testing"
	"time"

	alivpcv1alpha1 "github.com/crossplane-contrib/provider-upjet-alibabacloud/apis/vpc/v1alpha1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/crossplane/crossplane-runtime/pkg/meta"
	"github.com/go-logr/logr"
	"github.com/stretchr/testify/assert"
	"go.uber.org/mock/gomock"
	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"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/ptr"
	"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"
	"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/k8s"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/namer"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/repository"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/util"
	"github.com/tidbcloud/infra-provider/regional-server/test/kubemock"
	"github.com/tidbcloud/infra-provider/regional-server/test/testdata"
)

func fakeTaskContext(t *testing.T) *task.Context {
	s := runtime.NewScheme()
	v1alpha1.AddToScheme(s)
	alivpcv1alpha1.AddToScheme(s)
	fakeVPC := &v1alpha1.VPC{}
	bs, err := testdata.LoadACKVPCYAML()
	assert.Nil(t, err)
	assert.Nil(t, yaml.Unmarshal(bs, fakeVPC))
	cli := fake.NewClientBuilder().WithScheme(s).WithObjects(fakeVPC).WithStatusSubresource(fakeVPC, &alivpcv1alpha1.VPC{}, &alivpcv1alpha1.Vswitch{}).Build()
	c := &task.Context{
		Local:  k8s.NewMutClientWithOwner(cli, fakeVPC.DeepCopy(), namer.NewNamer()).Client(),
		Logger: logr.Discard(),
		VPC:    fakeVPC.DeepCopy(),
		VPCRW:  repository.NewVPCReaderWriter(cli),
		Config: config.RootConfig{
			ResourceNamePrefix: "tidbcloud",
		},
	}
	c, err = c.Default()
	assert.Nil(t, err)
	return c
}

func newFakeVPC(ctx *task.Context) *vpcTask {
	return &vpcTask{
		BaseTask: &task.BaseTask{
			Context: ctx,
		},
	}
}

func fakeVPCTask(t *testing.T) *vpcTask {
	return newFakeVPC(fakeTaskContext(t))
}

func TestVPC_Create(t *testing.T) {
	vpcTask := fakeVPCTask(t)
	t.Run("Create aliyun vpc success", func(t *testing.T) {
		result := vpcTask.Create(context.Background())
		assert.True(t, result.IsDoneState())
		aliVpc := &alivpcv1alpha1.VPC{}
		name := vpcTask.getVPCName()
		err := vpcTask.Local.Get(context.Background(), types.NamespacedName{Name: name}, aliVpc)
		assert.NoError(t, err)
		expectedTags := util.ToMapPointer(controllers.ConvertToCloudTags(vpcTask.VPC.Labels, consts.ProviderTypeACK))
		if len(expectedTags) == 0 {
			expectedTags = nil
		}
		assert.Equal(t, alivpcv1alpha1.VPCSpec{
			ForProvider: alivpcv1alpha1.VPCParameters{
				VPCName:   ptr.To(name),
				Region:    ptr.To(vpcTask.VPC.Spec.Region),
				CidrBlock: ptr.To(vpcTask.VPC.Spec.CIDR),
				Tags:      expectedTags,
			},
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: vpcTask.VPC.Spec.AccountRef.ProviderConfigName},
				ManagementPolicies:      controllers.GetResourceManagementPolicies(),
			},
		}, aliVpc.Spec)
	})
	t.Run("Create aliyun vpc failed", func(t *testing.T) {
		ctl := gomock.NewController(t)
		mockCli := kubemock.NewMockClient(ctl)
		vpcTask.Local = mockCli
		mockCli.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(apierrors.NewNotFound(schema.GroupResource{}, "not found")).Times(1)
		mockCli.EXPECT().Create(gomock.Any(), gomock.Any()).Return(assert.AnError).Times(1)
		result := vpcTask.Create(context.Background())
		res, err, retriableErr := deploy.MinRequeue([]deploy.Result{result})
		assert.NoError(t, err)
		assert.Error(t, retriableErr)
		assert.Equal(t, time.Second*30, res.RequeueAfter)
	})
}

func TestVPC_PostCreate(t *testing.T) {
	t.Run("aliyun vpc not found", func(t *testing.T) {
		vpcTask := fakeVPCTask(t)
		result := vpcTask.PostCreate(context.Background())
		assert.Error(t, result.Error())
	})

	t.Run("aliyun vpc is not ready", func(t *testing.T) {
		vpcTask := fakeVPCTask(t)
		result := vpcTask.Create(context.Background())
		assert.True(t, result.IsDoneState())

		result = vpcTask.PostCreate(context.Background())
		assert.Error(t, result.Error())
		assert.Contains(t, result.Error().Error(), "vpc is not ready")
	})

	t.Run("aliyun vpc is ready", func(t *testing.T) {
		vpcTask := fakeVPCTask(t)
		result := vpcTask.Create(context.Background())
		assert.True(t, result.IsDoneState())

		expectedVpcName := vpcTask.getVPCName()
		expectedVpcID := "aliyun-vpc-id"
		aliyunVPC := &alivpcv1alpha1.VPC{
			ObjectMeta: metav1.ObjectMeta{
				Name: expectedVpcName,
			},
		}
		err := vpcTask.Local.Get(context.Background(), client.ObjectKeyFromObject(aliyunVPC), aliyunVPC)
		assert.NoError(t, err)
		aliyunVPC.Status.SetConditions(xpv1.Available())
		err = vpcTask.Local.Status().Update(context.Background(), aliyunVPC)
		assert.NoError(t, err)
		meta.SetExternalName(aliyunVPC, expectedVpcID)
		err = vpcTask.Local.Update(context.Background(), aliyunVPC)
		assert.NoError(t, err)

		result = vpcTask.PostCreate(context.Background())
		assert.True(t, result.IsDoneState())
		assert.Equal(t, expectedVpcName, vpcTask.VPC.Status.VPC.Name)
		assert.Equal(t, expectedVpcID, vpcTask.VPC.Status.VPC.VPCID)
	})
}

func TestVPC_Delete(t *testing.T) {
	t.Run("aliyun vpc not found", func(t *testing.T) {
		vpcTask := fakeVPCTask(t)
		result := vpcTask.Delete(context.Background())
		assert.True(t, result.IsDoneState())
	})

	t.Run("get aliyun vpc error", func(t *testing.T) {
		vpcTask := fakeVPCTask(t)
		ctl := gomock.NewController(t)
		mockCli := kubemock.NewMockClient(ctl)
		vpcTask.Local = mockCli

		mockCli.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(assert.AnError).Times(1)
		result := vpcTask.Delete(context.Background())
		assert.Error(t, result.Error())
	})

	t.Run("patch aliyun vpc error", func(t *testing.T) {
		vpcTask := fakeVPCTask(t)
		ctl := gomock.NewController(t)
		mockCli := kubemock.NewMockClient(ctl)
		vpcTask.Local = mockCli

		mockCli.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(1)
		mockCli.EXPECT().Patch(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(assert.AnError).Times(1)
		result := vpcTask.Delete(context.Background())
		assert.Error(t, result.Error())
	})

	t.Run("delete aliyun vpc error", func(t *testing.T) {
		vpcTask := fakeVPCTask(t)
		ctl := gomock.NewController(t)
		mockCli := kubemock.NewMockClient(ctl)
		vpcTask.Local = mockCli

		mockCli.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(1)
		mockCli.EXPECT().Patch(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(1)
		mockCli.EXPECT().Delete(gomock.Any(), gomock.Any()).Return(assert.AnError).Times(1)
		result := vpcTask.Delete(context.Background())
		assert.Error(t, result.Error())
	})

	t.Run("delete aliyun vpc successfully", func(t *testing.T) {
		vpcTask := fakeVPCTask(t)
		result := vpcTask.Create(context.Background())
		assert.True(t, result.IsDoneState())
		aliyunVPC := &alivpcv1alpha1.VPC{
			ObjectMeta: metav1.ObjectMeta{
				Name: vpcTask.getVPCName(),
			},
		}
		err := vpcTask.Local.Get(context.Background(), client.ObjectKeyFromObject(aliyunVPC), aliyunVPC)
		assert.NoError(t, err)

		result = vpcTask.Delete(context.Background())
		assert.True(t, result.IsDoneState())
		err = vpcTask.Local.Get(context.Background(), client.ObjectKeyFromObject(aliyunVPC), aliyunVPC)
		assert.Error(t, err)
		assert.True(t, apierrors.IsNotFound(err))
	})
}

func TestVPC_PostDelete(t *testing.T) {
	t.Run("aliyun vpc still exists", func(t *testing.T) {
		vpcTask := fakeVPCTask(t)
		result := vpcTask.Create(context.Background())
		aliyunVPC := &alivpcv1alpha1.VPC{
			ObjectMeta: metav1.ObjectMeta{
				Name: vpcTask.getVPCName(),
			},
		}
		err := vpcTask.Local.Get(context.Background(), client.ObjectKeyFromObject(aliyunVPC), aliyunVPC)
		assert.NoError(t, err)

		result = vpcTask.PostDelete(context.Background())
		assert.Error(t, result.Error())
	})

	t.Run("aliyun vpc is deleted", func(t *testing.T) {
		vpcTask := fakeVPCTask(t)
		result := vpcTask.PostDelete(context.Background())
		assert.True(t, result.IsDoneState())
	})
}
