package gke

import (
	"context"
	"testing"

	gkev1beta1 "github.com/crossplane-contrib/provider-gcp/apis/container/v1beta1"
	gkev1beta2 "github.com/crossplane-contrib/provider-gcp/apis/container/v1beta2"
	"github.com/go-logr/logr"
	"github.com/stretchr/testify/require"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"

	"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/appbox/task"
)

const (
	locationTestGKEName      = "test-gke"
	locationTestNodePoolName = "test-nodepool"
)

func newLocationTestContext(t *testing.T, vpcZones, configZones []string, extraObjects ...client.Object) (*task.Context, client.Client) {
	t.Helper()

	scheme := runtime.NewScheme()
	require.NoError(t, v1alpha1.SchemeBuilder.AddToScheme(scheme))
	require.NoError(t, gkev1beta1.SchemeBuilder.AddToScheme(scheme))
	require.NoError(t, gkev1beta2.SchemeBuilder.AddToScheme(scheme))

	appbox := &v1alpha1.APPBox{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "test-appbox",
			Namespace: "test-ns",
			Annotations: map[string]string{
				v1alpha1.AnnotationKeyOwnCrossplaneNameGKE:      locationTestGKEName,
				v1alpha1.AnnotationKeyOwnCrossplaneNameNodePool: locationTestNodePoolName,
			},
			Labels: map[string]string{
				v1alpha1.TagTenantKey:  "tenant",
				v1alpha1.TagProjectKey: "project",
			},
		},
		Spec: v1alpha1.APPBoxSpec{
			Region:   "us-west1",
			Provider: "gke",
			KubernetesConfig: v1alpha1.KubernetesConfig{
				Version: "1.29",
			},
		},
		Status: v1alpha1.APPBoxStatus{
			IAM: v1alpha1.IAMStatus{
				ServiceAccountEmail: "admin@test-project.iam.gserviceaccount.com",
			},
		},
	}
	vpc := &v1alpha1.VPC{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "test-vpc",
			Namespace: "test-ns",
			Labels: map[string]string{
				v1alpha1.TagTenantKey:  "tenant",
				v1alpha1.TagProjectKey: "project",
			},
		},
		Spec: v1alpha1.VPCSpec{
			Provider: "gke",
			Region:   "us-west1",
			CIDR:     "10.10.0.0/16",
			Zones:    append([]string(nil), vpcZones...),
			AccountRef: &v1alpha1.AccountReference{
				AccountID:          "test-project",
				ProviderConfigName: "test-provider-config",
			},
		},
		Status: v1alpha1.VPCStatus{
			VPC: v1alpha1.VPCRes{
				Name: "test-network",
			},
			NodeSubnets: []v1alpha1.Subnet{
				{Name: "test-node-subnet"},
			},
		},
	}

	objects := []client.Object{appbox, vpc}
	objects = append(objects, extraObjects...)
	kubeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build()
	ctx, err := (&task.Context{
		Local:  kubeClient,
		Config: newLocationTestConfig(configZones),
		VPC:    vpc,
		APPBox: appbox,
		Cluster: task.NewGKEClusterIdentifier(&gkev1beta2.Cluster{
			ObjectMeta: metav1.ObjectMeta{Name: locationTestGKEName},
		}),
		Logger: logr.Discard(),
	}).Default()
	require.NoError(t, err)
	return ctx, kubeClient
}

func newLocationTestConfig(configZones []string) config.RootConfig {
	return config.RootConfig{
		ControllerConfig: config.ControllerConfig{
			Provider: "gke",
			RegionConfig: config.RegionConfig{
				Name:  "us-west1",
				Zones: append([]string(nil), configZones...),
			},
			APPBoxConfig: config.APPBoxConfig{
				NodeConfig: config.NodeConfig{
					MaxPodsPerNode: 16,
				},
			},
		},
	}
}

func TestBuildGKENewClusterSetsConfiguredLocations(t *testing.T) {
	vpcZones := []string{"us-west1-b", "us-west1-c", "us-west1-d"}
	configZones := []string{"us-west1-a", "us-west1-f", "us-west1-g"}
	ctx, _ := newLocationTestContext(t, vpcZones, configZones)

	cluster, err := NewGKE(ctx, nil).buildGKE(context.Background())
	require.NoError(t, err)
	require.Equal(t, vpcZones, cluster.Spec.ForProvider.Locations)
}

func TestBuildGKEExistingClusterPreservesLocations(t *testing.T) {
	initialCtx, _ := newLocationTestContext(t, nil, []string{"us-west1-b", "us-west1-c", "us-west1-d"})
	existing, err := NewGKE(initialCtx, nil).buildGKE(context.Background())
	require.NoError(t, err)
	existing.Spec.ForProvider.Locations = []string{"legacy-zone"}

	ctx, _ := newLocationTestContext(t, nil, []string{"us-west1-b", "us-west1-c", "us-west1-d"}, existing)
	cluster, err := NewGKE(ctx, nil).buildGKE(context.Background())
	require.NoError(t, err)
	require.Equal(t, []string{"legacy-zone"}, cluster.Spec.ForProvider.Locations)
}

func TestNodePoolCreateUsesGKEZone(t *testing.T) {
	configZones := []string{"us-west1-a", "us-west1-e", "us-west1-f"}
	gkeZones := []string{"us-west1-b", "us-west1-c", "us-west1-d"}
	ctx, kubeClient := newLocationTestContext(t, nil, configZones)
	ctx.Cluster = task.NewGKEClusterIdentifier(&gkev1beta2.Cluster{
		ObjectMeta: metav1.ObjectMeta{Name: locationTestGKEName},
		Spec: gkev1beta2.ClusterSpec{
			ForProvider: gkev1beta2.ClusterParameters{
				Locations: gkeZones,
			},
		},
	})

	result := NewNodePool(ctx, nil).Create(context.Background())
	require.NoError(t, result.Error())

	nodePool := &gkev1beta1.NodePool{}
	require.NoError(t, kubeClient.Get(context.Background(), types.NamespacedName{Name: locationTestNodePoolName}, nodePool))
	require.Equal(t, []string{
		stableZone(gkeZones, "test-ns/test-appbox/"+locationTestNodePoolName),
	}, nodePool.Spec.ForProvider.Locations)
	require.NotContains(t, configZones, nodePool.Spec.ForProvider.Locations[0])
}

func TestNodePoolCreatePreservesExistingLocations(t *testing.T) {
	existing := &gkev1beta1.NodePool{
		ObjectMeta: metav1.ObjectMeta{Name: locationTestNodePoolName},
		Spec: gkev1beta1.NodePoolSpec{
			ForProvider: gkev1beta1.NodePoolParameters{
				Locations: []string{"legacy-zone"},
			},
		},
	}
	ctx, kubeClient := newLocationTestContext(t, nil, []string{"us-west1-b", "us-west1-c", "us-west1-d"}, existing)

	result := NewNodePool(ctx, nil).Create(context.Background())
	require.NoError(t, result.Error())

	nodePool := &gkev1beta1.NodePool{}
	require.NoError(t, kubeClient.Get(context.Background(), types.NamespacedName{Name: locationTestNodePoolName}, nodePool))
	require.Equal(t, []string{"legacy-zone"}, nodePool.Spec.ForProvider.Locations)
}

func TestNodePoolCreatePreservesExistingEmptyLocations(t *testing.T) {
	tests := []struct {
		name      string
		locations []string
	}{
		{
			name: "nil locations",
		},
		{
			name:      "empty locations",
			locations: []string{},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			existing := &gkev1beta1.NodePool{
				ObjectMeta: metav1.ObjectMeta{Name: locationTestNodePoolName},
				Spec: gkev1beta1.NodePoolSpec{
					ForProvider: gkev1beta1.NodePoolParameters{
						Locations: tt.locations,
					},
				},
			}
			ctx, kubeClient := newLocationTestContext(t, nil, []string{"us-west1-b", "us-west1-c", "us-west1-d"}, existing)

			result := NewNodePool(ctx, nil).Create(context.Background())
			require.NoError(t, result.Error())

			nodePool := &gkev1beta1.NodePool{}
			require.NoError(t, kubeClient.Get(context.Background(), types.NamespacedName{Name: locationTestNodePoolName}, nodePool))
			require.Empty(t, nodePool.Spec.ForProvider.Locations)
		})
	}
}
