package aks

import (
	"context"
	"fmt"
	"testing"
	"time"

	v1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	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"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	"github.com/tidbcloud/infra-provider/pkg/util"
	"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/controllers"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/vpc/task"
	resvpc "github.com/tidbcloud/infra-provider/regional-server/pkg/resourcemanager/vpc"
	"github.com/tidbcloud/infra-provider/regional-server/test/kubemock"
	"github.com/tidbcloud/infra-provider/regional-server/test/testdata"
	azurev1beta1 "github.com/upbound/provider-azure/apis/azure/v1beta1"
	netv1beta1 "github.com/upbound/provider-azure/apis/network/v1beta1"
	"go.uber.org/mock/gomock"
	corev1 "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"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"
	"sigs.k8s.io/yaml"
)

func fakeTaskContext(t *testing.T) *task.Context {
	s := runtime.NewScheme()
	v1alpha1.AddToScheme(s)
	azurev1beta1.SchemeBuilder.AddToScheme(s)
	netv1beta1.SchemeBuilder.AddToScheme(s)
	fakeVPC := &v1alpha1.VPC{}
	bs, err := testdata.LoadAKSVPCYAML()
	assert.Nil(t, err)
	assert.Nil(t, yaml.Unmarshal(bs, fakeVPC))
	cli := fake.NewClientBuilder().WithScheme(s).WithObjects(fakeVPC).WithStatusSubresource(
		fakeVPC,
		&azurev1beta1.ResourceGroup{},
		&netv1beta1.VirtualNetwork{},
		&netv1beta1.Subnet{}).Build()
	c := &task.Context{
		Local:  k8s.NewMutClientWithOwner(cli, fakeVPC.DeepCopy(), namer.NewNamer()).Client(),
		Logger: logr.Discard(),
		VPC:    fakeVPC.DeepCopy(),
		VPCRW:  resvpc.NewVPCReaderWriter(cli),
		Config: config.RootConfig{},
	}
	c, err = c.Default()
	assert.Nil(t, err)
	return c
}

func fakeResourceGroupTask(t *testing.T) *ResourceGroup {
	return NewResourceGroup(fakeTaskContext(t))
}

func TestResourceGroup_Create(t *testing.T) {
	rgTask := fakeResourceGroupTask(t)
	t.Run("Create resourcegroup success", func(t *testing.T) {
		result := rgTask.Create(context.Background())
		assert.True(t, result.IsDoneState())
		rg := &azurev1beta1.ResourceGroup{}
		assert.Nil(t, rgTask.Local.Get(context.Background(), types.NamespacedName{Name: controllers.GetResourceGroupName(rgTask.VPC)}, rg))
		tags := cloudv1.TagsFromLabels(rgTask.VPC.Labels)
		labels := map[string]string{
			v1alpha1.VPCNameKey: rgTask.VPC.Name,
		}
		assert.Equal(t, labels, rg.Labels)
		assert.Equal(t, azurev1beta1.ResourceGroupSpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{
					Name: rgTask.ProviderConfigName(),
				},
			},
			ForProvider: azurev1beta1.ResourceGroupParameters{
				Location: ptr.To(rgTask.VPC.Spec.Region),
				Tags:     util.ToMapPointer(tags),
			},
		}, rg.Spec)
	})
	t.Run("Create resourcegroup failed", func(t *testing.T) {
		ctl := gomock.NewController(t)
		mockcli := kubemock.NewMockClient(ctl)
		rgTask.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 := rgTask.Create(context.Background())
		res, err, retriableErr := deploy.MinRequeue([]deploy.Result{result})
		assert.Nil(t, err)
		assert.NotNil(t, retriableErr)
		assert.Equal(t, defaultRequeuInterval, res.RequeueAfter)
	})
}

func Test_setResourceGroupStatus(t *testing.T) {
	rgTask := fakeResourceGroupTask(t)
	t.Run("id is empty", func(t *testing.T) {
		rgName := "rg-test"
		err := rgTask.setResourceGroupStatus(context.Background(), &azurev1beta1.ResourceGroup{
			ObjectMeta: metav1.ObjectMeta{
				Name: rgName,
			},
		})
		assert.Nil(t, err)
		vpc := &v1alpha1.VPC{}
		assert.Nil(t, rgTask.Local.Get(context.Background(), types.NamespacedName{
			Name:      rgTask.VPC.Name,
			Namespace: rgTask.VPC.Namespace,
		}, vpc))
		assert.Equal(t, vpc.Status.ResourceGroup, v1alpha1.ResourceGroup{
			Name: rgName,
		})
	})
	t.Run("set name and id", func(t *testing.T) {
		rgName := "rg-test"
		rgID := "rg-test-id"
		err := rgTask.setResourceGroupStatus(context.Background(), &azurev1beta1.ResourceGroup{
			ObjectMeta: metav1.ObjectMeta{
				Name: rgName,
				Annotations: map[string]string{
					meta.AnnotationKeyExternalName: rgID,
				},
			},
		})
		assert.Nil(t, err)
		vpc := &v1alpha1.VPC{}
		assert.Nil(t, rgTask.Local.Get(context.Background(), types.NamespacedName{
			Name:      rgTask.VPC.Name,
			Namespace: rgTask.VPC.Namespace,
		}, vpc))
		assert.Equal(t, vpc.Status.ResourceGroup, v1alpha1.ResourceGroup{
			Name: rgName,
			ID:   rgID,
		})
	})
}

func TestResourceGroup_PostCreate(t *testing.T) {
	t.Run("PostCreate resourcegroup success", func(t *testing.T) {
		rgTask := fakeResourceGroupTask(t)
		rgName := controllers.GetResourceGroupName(rgTask.VPC)
		rg := &azurev1beta1.ResourceGroup{
			ObjectMeta: metav1.ObjectMeta{
				Name: rgName,
				Annotations: map[string]string{
					meta.AnnotationKeyExternalName: "rg-test-id",
				},
			},
			Status: azurev1beta1.ResourceGroupStatus{
				ResourceStatus: v1.ResourceStatus{
					ConditionedStatus: xpv1.ConditionedStatus{
						Conditions: []xpv1.Condition{
							{
								Type:   xpv1.TypeReady,
								Status: corev1.ConditionTrue,
							},
						},
					},
				},
			},
		}
		assert.Nil(t, rgTask.Local.Create(context.Background(), rg))
		result := rgTask.PostCreate(context.Background())
		assert.True(t, result.IsDoneState())
	})
	t.Run("resourcegroup not found", func(t *testing.T) {
		rgTask := fakeResourceGroupTask(t)
		result := rgTask.PostCreate(context.Background())
		res, err, retriableErr := deploy.MinRequeue([]deploy.Result{result})
		assert.Nil(t, err)
		assert.ErrorContains(t, retriableErr, fmt.Sprintf("failed to get resource group %s", controllers.GetResourceGroupName(rgTask.VPC)))
		assert.Equal(t, defaultRequeuInterval, res.RequeueAfter)
	})
	t.Run("resourcegroup not ready", func(t *testing.T) {
		rgTask := fakeResourceGroupTask(t)
		rgName := controllers.GetResourceGroupName(rgTask.VPC)
		rg := &azurev1beta1.ResourceGroup{
			ObjectMeta: metav1.ObjectMeta{
				Name: rgName,
			},
		}
		assert.Nil(t, rgTask.Local.Create(context.Background(), rg))
		result := rgTask.PostCreate(context.Background())
		res, err, retriableErr := deploy.MinRequeue([]deploy.Result{result})
		assert.Nil(t, err)
		assert.ErrorContains(t, retriableErr, fmt.Sprintf("resource group %s is not ready", rgName))
		assert.Equal(t, defaultRequeuInterval, res.RequeueAfter)
	})
}

func TestResourceGroup_Delete(t *testing.T) {
	t.Run("Delete resourcegroup success", func(t *testing.T) {
		rgTask := fakeResourceGroupTask(t)
		rgName := controllers.GetResourceGroupName(rgTask.VPC)
		rg := &azurev1beta1.ResourceGroup{
			ObjectMeta: metav1.ObjectMeta{
				Name: rgName,
			},
		}
		assert.Nil(t, rgTask.Local.Create(context.Background(), rg))
		result := rgTask.Delete(context.Background())
		assert.True(t, result.IsDoneState())
		err := rgTask.Local.Get(context.Background(), types.NamespacedName{Name: rgName}, rg)
		assert.True(t, apierrors.IsNotFound(err))
	})
	t.Run("Delete resourcegroup failed", func(t *testing.T) {
		rgTask := fakeResourceGroupTask(t)
		rgName := controllers.GetResourceGroupName(rgTask.VPC)
		rg := &azurev1beta1.ResourceGroup{
			ObjectMeta: metav1.ObjectMeta{
				Name: rgName,
			},
		}
		assert.Nil(t, rgTask.Local.Create(context.Background(), rg))
		ctl := gomock.NewController(t)
		mockcli := kubemock.NewMockClient(ctl)
		rgTask.Local = mockcli
		mockcli.EXPECT().Delete(gomock.Any(), gomock.Any()).Return(assert.AnError).Times(1)
		result := rgTask.Delete(context.Background())
		res, err, retriableErr := deploy.MinRequeue([]deploy.Result{result})
		assert.NotNil(t, err)
		assert.Nil(t, retriableErr)
		assert.Equal(t, time.Second*0, res.RequeueAfter)
	})
}

func TestResourceGroup_PostDelete(t *testing.T) {
	rgTask := fakeResourceGroupTask(t)
	t.Run("resourcegroup is not found", func(t *testing.T) {
		result := rgTask.PostDelete(context.Background())
		assert.True(t, result.IsDoneState())
	})
	t.Run("resourcegroup is found", func(t *testing.T) {
		rgName := controllers.GetResourceGroupName(rgTask.VPC)
		rg := &azurev1beta1.ResourceGroup{
			ObjectMeta: metav1.ObjectMeta{
				Name: rgName,
			},
		}
		assert.Nil(t, rgTask.Local.Create(context.Background(), rg))
		result := rgTask.PostDelete(context.Background())
		res, err, retriableErr := deploy.MinRequeue([]deploy.Result{result})
		assert.NotNil(t, err)
		assert.Nil(t, retriableErr)
		assert.Equal(t, time.Second*0, res.RequeueAfter)
	})
}
