package aks

import (
	"context"
	"fmt"
	"testing"

	v1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"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/internet/task"
	resinternet "github.com/tidbcloud/infra-provider/regional-server/pkg/resourcemanager/internet"
	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"
	"github.com/upbound/provider-azure/apis/network/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"
	"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))
	fakeInternet := &v1alpha1.Internet{}
	bs, err = testdata.LoadAKSInternetYAML()
	assert.Nil(t, err)
	assert.Nil(t, yaml.Unmarshal(bs, fakeInternet))
	cli := fake.NewClientBuilder().WithScheme(s).WithObjects(fakeVPC, fakeInternet).WithStatusSubresource(
		fakeVPC,
		fakeInternet,
		&azurev1beta1.ResourceGroup{},
		&netv1beta1.VirtualNetwork{},
		&netv1beta1.Subnet{}).Build()
	c := &task.Context{
		Local:      k8s.NewMutClientWithOwner(cli, fakeInternet.DeepCopy(), namer.NewNamer()).Client(),
		Logger:     logr.Discard(),
		VPC:        fakeVPC.DeepCopy(),
		VPCR:       resvpc.NewVPCReader(cli),
		InternetRW: resinternet.NewInternetReaderWriter(cli),
		Internet:   fakeInternet.DeepCopy(),
		Config:     config.RootConfig{},
	}
	c, err = c.Default()
	assert.Nil(t, err)
	return c
}

func fakeIPAddress(t *testing.T) *IPAddress {
	return NewIPAddress(fakeTaskContext(t))
}

func TestIPAddress_newAddressName(t *testing.T) {
	ipTask := fakeIPAddress(t)
	assert.Equal(t, namer.UniqueIdentifier(ipTask.Internet.ObjectMeta, namer.WithPrefix("addr-0")), ipTask.newAddressName(0))
	assert.Equal(t, namer.UniqueIdentifier(ipTask.Internet.ObjectMeta, namer.WithPrefix("addr-1")), ipTask.newAddressName(1))
}

func TestIPAddress_newAddress(t *testing.T) {
	ipTask := fakeIPAddress(t)
	name := ipTask.newAddressName(0)
	tags := cloudv1.TagsFromLabels(ipTask.Internet.Labels)
	labels := map[string]string{
		v1alpha1.InternetNameKey: ipTask.Internet.Name,
	}
	ip := ipTask.newAddress(name, tags)
	assert.Equal(t, name, ip.Name)
	assert.Equal(t, labels, ip.Labels)
	assert.Equal(t, netv1beta1.PublicIPSpec{
		ResourceSpec: xpv1.ResourceSpec{
			ProviderConfigReference: &xpv1.Reference{
				Name: ipTask.ProviderConfigName(),
			},
		},
		ForProvider: v1beta1.PublicIPParameters{
			AllocationMethod:  ptr.To(staticAllocation),
			Location:          ptr.To(ipTask.Internet.Spec.Region),
			ResourceGroupName: ptr.To(controllers.GetResourceGroupName(ipTask.VPC)),
			Sku:               ptr.To(standardSKU),
			Tags:              util.ToMapPointer(tags),
		},
	}, ip.Spec)
}

func TestIPAddress_Create(t *testing.T) {
	ipTask := fakeIPAddress(t)
	t.Run("create success", func(t *testing.T) {
		result := ipTask.Create(context.Background())
		assert.True(t, result.IsDoneState())
		for idx := 0; idx < defaultAddressCount; idx++ {
			ip := &v1beta1.PublicIP{}
			assert.Nil(t, ipTask.Local.Get(context.Background(), types.NamespacedName{Name: ipTask.newAddressName(idx)}, ip))
		}
	})
	t.Run("create failed", func(t *testing.T) {
		ctl := gomock.NewController(t)
		mockcli := kubemock.NewMockClient(ctl)
		ipTask.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 := ipTask.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 TestIPAddress_PostCreate(t *testing.T) {
	ipTask := fakeIPAddress(t)
	labels := map[string]string{
		v1alpha1.InternetNameKey: ipTask.Internet.Name,
	}
	t.Run("publicip not found", func(t *testing.T) {
		result := ipTask.PostCreate(context.Background())
		res, err, retriableErr := deploy.MinRequeue([]deploy.Result{result})
		assert.Nil(t, err)
		assert.ErrorContains(t, retriableErr, "not found")
		assert.Equal(t, defaultRequeuInterval, res.RequeueAfter)
	})
	t.Run("publicip not ready", func(t *testing.T) {
		assert.Nil(t, ipTask.Local.Create(context.Background(), &v1beta1.PublicIP{
			ObjectMeta: metav1.ObjectMeta{
				Name:   ipTask.newAddressName(0),
				Labels: labels,
			},
		}))
		result := ipTask.PostCreate(context.Background())
		res, err, retriableErr := deploy.MinRequeue([]deploy.Result{result})
		assert.Nil(t, err)
		assert.ErrorContains(t, retriableErr, fmt.Sprintf("publicIP %s is not ready", ipTask.newAddressName(0)))
		assert.Equal(t, defaultRequeuInterval, res.RequeueAfter)
		assert.Nil(t, ipTask.Local.Delete(context.Background(), &v1beta1.PublicIP{
			ObjectMeta: metav1.ObjectMeta{
				Name: ipTask.newAddressName(0),
			},
		}))
	})
	t.Run("post create success", func(t *testing.T) {
		for idx := 0; idx < defaultAddressCount; idx++ {
			assert.Nil(t, ipTask.Local.Create(context.Background(), &v1beta1.PublicIP{
				ObjectMeta: metav1.ObjectMeta{
					Name:   ipTask.newAddressName(idx),
					Labels: labels,
				},
				Status: v1beta1.PublicIPStatus{
					ResourceStatus: v1.ResourceStatus{
						ConditionedStatus: xpv1.ConditionedStatus{
							Conditions: []xpv1.Condition{
								{
									Type:   xpv1.TypeReady,
									Status: corev1.ConditionTrue,
								},
							},
						},
					},
					AtProvider: v1beta1.PublicIPObservation{
						IPAddress: ptr.To(fmt.Sprintf("192.168.1.%d", idx)),
						ID:        ptr.To(fmt.Sprintf("id-%d", idx)),
					},
				},
			}))
		}
		result := ipTask.PostCreate(context.Background())
		assert.True(t, result.IsDoneState())
		internet := &v1alpha1.Internet{}
		assert.Nil(t, ipTask.Local.Get(context.Background(), client.ObjectKeyFromObject(ipTask.Internet), internet))
		for idx := 0; idx < defaultAddressCount; idx++ {
			assert.Equal(t, v1alpha1.IPAddress{
				Name:         ipTask.newAddressName(idx),
				IP:           fmt.Sprintf("192.168.1.%d", idx),
				AllocationID: fmt.Sprintf("id-%d", idx),
			}, internet.Status.IPAddresses[idx])
		}
	})
}

func TestIPAddress_Delete(t *testing.T) {
	ipTask := fakeIPAddress(t)
	result := ipTask.Create(context.Background())
	assert.True(t, result.IsDoneState())
	ips := &v1beta1.PublicIPList{}
	assert.Nil(t, ipTask.Local.List(context.Background(), ips))
	assert.Equal(t, len(ips.Items), defaultAddressCount)

	result = ipTask.Delete(context.Background())
	assert.True(t, result.IsDoneState())
	ips = &v1beta1.PublicIPList{}
	assert.Nil(t, ipTask.Local.List(context.Background(), ips))

	result = ipTask.Delete(context.Background())
	assert.True(t, result.IsDoneState())
}
