package gke

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

	gcpv1alpha1 "github.com/crossplane-contrib/provider-gcp/apis/compute/v1alpha1"
	gcpv1beta1 "github.com/crossplane-contrib/provider-gcp/apis/compute/v1beta1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/go-logr/logr"
	"github.com/stretchr/testify/assert"
	"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/regional-server/api/v1alpha1"
	rsv1alpha1 "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/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/testdata"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/pointer"
	"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)
	gcpv1beta1.SchemeBuilder.AddToScheme(s)
	gcpv1alpha1.SchemeBuilder.AddToScheme(s)
	fakeVPC := &v1alpha1.VPC{}
	bs, err := testdata.LoadGKEVPCYAML()
	assert.Nil(t, err)
	assert.Nil(t, yaml.Unmarshal(bs, fakeVPC))
	fakeInternet := &v1alpha1.Internet{}
	bs, err = testdata.LoadGKEInternetYAML()
	assert.Nil(t, err)
	assert.Nil(t, yaml.Unmarshal(bs, fakeInternet))
	cli := fake.NewClientBuilder().WithScheme(s).WithObjects(fakeVPC, fakeInternet).
		WithStatusSubresource(fakeVPC, fakeInternet, &gcpv1beta1.Address{}, &gcpv1alpha1.Router{}).Build()
	c := &task.Context{
		Local:      k8s.NewMutClientWithOwner(cli, fakeVPC.DeepCopy(), namer.NewNamer()).Client(),
		Logger:     logr.Discard(),
		VPC:        fakeVPC.DeepCopy(),
		VPCR:       resvpc.NewVPCReader(cli),
		Internet:   fakeInternet.DeepCopy(),
		InternetRW: resinternet.NewInternetReaderWriter(cli),
		Config: config.RootConfig{
			ControllerConfig: config.ControllerConfig{
				RegionConfig: config.RegionConfig{
					Name: "us-west1",
					Zones: []string{
						"zone1",
						"zone2",
						"zone3",
					},
				},
			},
		},
	}
	c, err = c.Default()
	assert.Nil(t, err)
	return c
}

func fakeRouterTask(t *testing.T) *Router {
	return NewRouter(fakeTaskContext(t))
}

func TestRouter_routerName(t *testing.T) {
	task := fakeRouterTask(t)
	assert.Equal(t, namer.UniqueIdentifier(task.Internet.ObjectMeta, namer.WithPrefix("egress")), task.routerName())
}

func TestRouter_newAddressName(t *testing.T) {
	task := fakeRouterTask(t)
	assert.Equal(t, namer.UniqueIdentifier(task.Internet.ObjectMeta, namer.WithPrefix(fmt.Sprintf("addr-%d", 0))), task.newAddressName(0))
}

func TestRouter_newAddress(t *testing.T) {
	task := fakeRouterTask(t)
	assert.Equal(t, &gcpv1beta1.Address{
		ObjectMeta: metav1.ObjectMeta{
			Name: "test",
		},
		Spec: gcpv1beta1.AddressSpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{
					Name: task.ProviderConfigName(),
				},
			},
			ForProvider: gcpv1beta1.AddressParameters{
				Description: pointer.String("ip address for nat"),
				Region:      task.Internet.Spec.Region,
			},
		},
	}, task.newAddress("test"))
}

func TestRouter_NewRouter(t *testing.T) {
	task := fakeRouterTask(t)
	router, err := task.NewRouter([]string{"test"}, &gcpv1beta1.Subnetwork{})
	assert.Nil(t, err)
	name := task.routerName()
	assert.Equal(t, &gcpv1alpha1.Router{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
		Spec: gcpv1alpha1.RouterSpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{
					Name: task.ProviderConfigName(),
				},
			},
			ForProvider: gcpv1alpha1.RouterParameters{
				Description: pointer.String("for pods in gke to access internet"),
				Region:      task.Internet.Spec.Region,
				NetworkRef: &xpv1.Reference{
					Name: task.VPC.Status.VPC.Name,
				},
				Nats: []*gcpv1alpha1.RouterNat{
					{
						Name:                          &name,
						NatIpAllocateOption:           "MANUAL_ONLY",
						NatIps:                        []string{"test"},
						SourceSubnetworkIpRangesToNat: "ALL_SUBNETWORKS_ALL_IP_RANGES",
					},
				},
			},
		},
	}, router)
}

func TestRouter(t *testing.T) {
	task := fakeRouterTask(t)
	subnet := v1alpha1.Subnet{
		Name: "test-subnet",
	}
	subnetRes := &gcpv1beta1.Subnetwork{
		ObjectMeta: metav1.ObjectMeta{
			Name: subnet.Name,
		},
	}
	task.VPC.Status.NodeSubnets = []v1alpha1.Subnet{subnet}
	assert.Nil(t, task.Local.Create(context.Background(), subnetRes))

	t.Run("address not ready", func(t *testing.T) {
		result := task.Create(context.Background())
		res, err, retriableErr := deploy.MinRequeue([]deploy.Result{result})
		assert.ErrorContains(t, err, "address not ready")
		assert.Nil(t, retriableErr)
		assert.True(t, res.IsZero())
	})
	t.Run("create success", func(t *testing.T) {
		for i := 0; i < defaultAddressCount; i++ {
			name := task.newAddressName(i)
			address := &gcpv1beta1.Address{}
			assert.Nil(t, task.Local.Get(context.Background(), types.NamespacedName{Name: name}, address))
			address.Spec.ForProvider.Address = pointer.String(fmt.Sprintf("test-%d", i))
			address.Status.AtProvider.SelfLink = fmt.Sprintf("test-%d", i)
			targetStatus := address.Status.DeepCopy()
			assert.Nil(t, task.Local.Update(context.Background(), address))
			address.Status = *targetStatus
			assert.Nil(t, task.Local.Status().Update(context.Background(), address))
		}
		result := task.Create(context.Background())
		assert.True(t, result.IsDoneState())
	})
	t.Run("router not ready", func(t *testing.T) {
		result := task.PostCreate(context.Background())
		res, err, retriableErr := deploy.MinRequeue([]deploy.Result{result})
		assert.Nil(t, err)
		assert.ErrorContains(t, retriableErr, "not ready")
		assert.Equal(t, time.Second*30, res.RequeueAfter)
	})
	t.Run("router create success", func(t *testing.T) {
		routerName := task.routerName()
		router := &gcpv1alpha1.Router{}
		assert.Nil(t, task.Local.Get(context.Background(), types.NamespacedName{Name: routerName}, router))
		router.Status.SetConditions(xpv1.Available())
		assert.Nil(t, task.Local.Status().Update(context.Background(), router))
		result := task.PostCreate(context.Background())
		assert.True(t, result.IsDoneState())
		nats := []rsv1alpha1.RouterNAT{}
		for _, nat := range router.Spec.ForProvider.Nats {
			nats = append(nats, rsv1alpha1.RouterNAT{
				Name: *nat.Name,
				IPs:  nat.NatIps,
			})
		}
		assert.Equal(t, []v1alpha1.CloudRouter{
			{
				Name:          routerName,
				CloudRouterID: router.Status.AtProvider.ID,
				NATs:          nats,
			},
		}, task.Internet.Status.CloudRouters)
	})
	t.Run("delete success", func(t *testing.T) {
		result := task.Delete(context.Background())
		assert.True(t, result.IsDoneState())
		routers := &gcpv1alpha1.RouterList{}
		assert.Nil(t, task.Local.List(context.Background(), routers))
		assert.Equal(t, 0, len(routers.Items))
		ipaddress := &gcpv1beta1.AddressList{}
		assert.Nil(t, task.Local.List(context.Background(), ipaddress))
		assert.Equal(t, 0, len(ipaddress.Items))

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