package globalapi

import (
	"context"
	"errors"
	"fmt"
	"testing"

	"github.com/stretchr/testify/assert"
	globalstructs "github.com/tidbcloud/observability/pkg/globalservice/structs"
	serverlessglobalv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/serverless/v1"
	serverlessregionalv1 "github.com/tidbcloud/tidb-management-service/api/spec/regional/serverless/v1"
	tidbmgmt "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
	"google.golang.org/grpc"
)

func TestShouldResolveServerlessChild(t *testing.T) {
	tests := []struct {
		name       string
		routingCtx globalstructs.RoutingContext
		expected   bool
	}{
		{
			name: "devtier child cluster triggers resolution",
			routingCtx: globalstructs.RoutingContext{
				BizType:       "dev-devtier",
				ApplicationID: "10174902202723360328",
			},
			expected: true,
		},
		{
			name: "prod devtier child cluster triggers resolution",
			routingCtx: globalstructs.RoutingContext{
				BizType:       "prod-devtier",
				ApplicationID: "1234567890",
			},
			expected: true,
		},
		{
			name: "parent cluster (devtier-infra) does not trigger",
			routingCtx: globalstructs.RoutingContext{
				BizType:       "dev-devtier",
				ApplicationID: ServerlessParentApplicationID,
			},
			expected: false,
		},
		{
			name: "empty applicationID does not trigger",
			routingCtx: globalstructs.RoutingContext{
				BizType:       "dev-devtier",
				ApplicationID: "",
			},
			expected: false,
		},
		{
			name: "dedicated bizType does not trigger",
			routingCtx: globalstructs.RoutingContext{
				BizType:       "tidbcloud",
				ApplicationID: "123",
			},
			expected: false,
		},
		{
			name: "byoc bizType does not trigger",
			routingCtx: globalstructs.RoutingContext{
				BizType:       "dev-byoc",
				ApplicationID: "123",
			},
			expected: false,
		},
		{
			name: "nextgen bizType does not trigger",
			routingCtx: globalstructs.RoutingContext{
				BizType:       "prod-tidbcloud-nextgen",
				ApplicationID: "123",
			},
			expected: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			assert.Equal(t, tt.expected, shouldResolveServerlessChild(tt.routingCtx))
		})
	}
}

func TestExtractProjectIDFromK8sName(t *testing.T) {
	tests := []struct {
		name     string
		k8sName  string
		region   string
		expected string
		wantErr  bool
	}{
		{
			name:     "staging aws f01",
			k8sName:  "staging-us-east-1-f01",
			region:   "us-east-1",
			expected: "us-east-1-f01",
		},
		{
			name:     "prod aws f02",
			k8sName:  "prod-us-east-1-f02",
			region:   "us-east-1",
			expected: "us-east-1-f02",
		},
		{
			name:     "alicloud a02",
			k8sName:  "prod-ap-southeast-1-a02",
			region:   "ap-southeast-1",
			expected: "ap-southeast-1-a02",
		},
		{
			name:     "multi-dash region",
			k8sName:  "dev-southamerica-east-1-f03",
			region:   "southamerica-east-1",
			expected: "southamerica-east-1-f03",
		},
		{
			name:    "single segment errors",
			k8sName: "f01",
			region:  "us-east-1",
			wantErr: true,
		},
		{
			name:    "trailing dash errors",
			k8sName: "staging-us-east-1-",
			region:  "us-east-1",
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := extractProjectIDFromK8sName(tt.k8sName, tt.region)
			if tt.wantErr {
				assert.Error(t, err)
				return
			}
			assert.NoError(t, err)
			assert.Equal(t, tt.expected, got)
		})
	}
}

func TestResolveServerlessChild(t *testing.T) {
	// Save and restore original mgmt function
	origFn := mgmtListClustersFunc
	defer func() { mgmtListClustersFunc = origFn }()

	// Clear cache before each subtest
	clearResolverCache := func() {
		getServerlessResolver().cache.Flush()
	}

	t.Run("cache miss resolves via mgmt", func(t *testing.T) {
		clearResolverCache()
		mgmtListClustersFunc = func(ctx context.Context, env string, clusterID uint64, showDeleted bool) (*tidbmgmt.ListClustersResponse, error) {
			assert.Equal(t, "dev", env)
			assert.Equal(t, uint64(10174902202723360328), clusterID)
			assert.False(t, showDeleted)
			return &tidbmgmt.ListClustersResponse{
				Clusters: []*tidbmgmt.Cluster{
					{
						ClusterId: 10174902202723360328,
						ClusterInfo: &tidbmgmt.Cluster_ServerlessCluster{
							ServerlessCluster: &serverlessglobalv1.ServerlessServiceCluster{
								Cluster: &serverlessregionalv1.ServerlessRegionalServiceClusterInfo{
									Kubernetes: "dev-us-east-1-f02",
								},
							},
						},
					},
				},
			}, nil
		}

		routingCtx := globalstructs.RoutingContext{
			BizType:       "dev-devtier",
			TenantID:      "1369847559692509645",
			ApplicationID: "10174902202723360328",
			Region:        "us-east-1",
		}

		result, err := resolveServerlessChild(context.Background(), routingCtx)
		assert.NoError(t, err)
		assert.Equal(t, "us-east-1-f02", result.ProjectID)
		assert.Equal(t, ServerlessParentApplicationID, result.ApplicationID)
	})

	t.Run("cache hit skips mgmt call", func(t *testing.T) {
		clearResolverCache()
		mgmtCalled := false
		mgmtListClustersFunc = func(ctx context.Context, env string, clusterID uint64, showDeleted bool) (*tidbmgmt.ListClustersResponse, error) {
			mgmtCalled = true
			assert.False(t, showDeleted)
			return &tidbmgmt.ListClustersResponse{
				Clusters: []*tidbmgmt.Cluster{
					{
						ClusterInfo: &tidbmgmt.Cluster_ServerlessCluster{
							ServerlessCluster: &serverlessglobalv1.ServerlessServiceCluster{
								Cluster: &serverlessregionalv1.ServerlessRegionalServiceClusterInfo{
									Kubernetes: "prod-us-east-1-f03",
								},
							},
						},
					},
				},
			}, nil
		}

		routingCtx := globalstructs.RoutingContext{
			BizType:       "prod-devtier",
			TenantID:      "999",
			ApplicationID: "888",
			Region:        "us-east-1",
		}

		// First call populates cache
		result, err := resolveServerlessChild(context.Background(), routingCtx)
		assert.NoError(t, err)
		assert.Equal(t, "us-east-1-f03", result.ProjectID)
		assert.True(t, mgmtCalled)

		// Second call should hit cache
		mgmtCalled = false
		result, err = resolveServerlessChild(context.Background(), routingCtx)
		assert.NoError(t, err)
		assert.Equal(t, "us-east-1-f03", result.ProjectID)
		assert.False(t, mgmtCalled, "mgmt should not be called on cache hit")
	})

	t.Run("invalid applicationID returns error", func(t *testing.T) {
		clearResolverCache()
		routingCtx := globalstructs.RoutingContext{
			BizType:       "dev-devtier",
			ApplicationID: "not-a-number",
			Region:        "us-east-1",
		}

		_, err := resolveServerlessChild(context.Background(), routingCtx)
		assert.Error(t, err)
	})

	t.Run("mgmt call failure returns error", func(t *testing.T) {
		clearResolverCache()
		mgmtListClustersFunc = func(ctx context.Context, env string, clusterID uint64, showDeleted bool) (*tidbmgmt.ListClustersResponse, error) {
			assert.False(t, showDeleted)
			return nil, errors.New("grpc connection refused")
		}

		routingCtx := globalstructs.RoutingContext{
			BizType:       "dev-devtier",
			ApplicationID: "12345",
			Region:        "us-east-1",
		}

		_, err := resolveServerlessChild(context.Background(), routingCtx)
		assert.Error(t, err)
	})

	t.Run("cluster not found returns error", func(t *testing.T) {
		clearResolverCache()
		mgmtListClustersFunc = func(ctx context.Context, env string, clusterID uint64, showDeleted bool) (*tidbmgmt.ListClustersResponse, error) {
			assert.False(t, showDeleted)
			return &tidbmgmt.ListClustersResponse{Clusters: []*tidbmgmt.Cluster{}}, nil
		}

		routingCtx := globalstructs.RoutingContext{
			BizType:       "dev-devtier",
			ApplicationID: "99999",
			Region:        "us-east-1",
		}

		_, err := resolveServerlessChild(context.Background(), routingCtx)
		assert.Error(t, err)
	})

	t.Run("empty kubernetes name returns error", func(t *testing.T) {
		clearResolverCache()
		mgmtListClustersFunc = func(ctx context.Context, env string, clusterID uint64, showDeleted bool) (*tidbmgmt.ListClustersResponse, error) {
			assert.False(t, showDeleted)
			return &tidbmgmt.ListClustersResponse{
				Clusters: []*tidbmgmt.Cluster{
					{
						ClusterInfo: &tidbmgmt.Cluster_ServerlessCluster{
							ServerlessCluster: &serverlessglobalv1.ServerlessServiceCluster{
								Cluster: &serverlessregionalv1.ServerlessRegionalServiceClusterInfo{
									Kubernetes: "",
								},
							},
						},
					},
				},
			}, nil
		}

		routingCtx := globalstructs.RoutingContext{
			BizType:       "dev-devtier",
			ApplicationID: "12345",
			Region:        "us-east-1",
		}

		_, err := resolveServerlessChild(context.Background(), routingCtx)
		assert.Error(t, err)
	})

	t.Run("alicloud parent cluster suffix", func(t *testing.T) {
		clearResolverCache()
		mgmtListClustersFunc = func(ctx context.Context, env string, clusterID uint64, showDeleted bool) (*tidbmgmt.ListClustersResponse, error) {
			assert.False(t, showDeleted)
			return &tidbmgmt.ListClustersResponse{
				Clusters: []*tidbmgmt.Cluster{
					{
						ClusterInfo: &tidbmgmt.Cluster_ServerlessCluster{
							ServerlessCluster: &serverlessglobalv1.ServerlessServiceCluster{
								Cluster: &serverlessregionalv1.ServerlessRegionalServiceClusterInfo{
									Kubernetes: "prod-ap-southeast-1-a02",
								},
							},
						},
					},
				},
			}, nil
		}

		routingCtx := globalstructs.RoutingContext{
			BizType:       "prod-devtier",
			ApplicationID: "67890",
			Region:        "ap-southeast-1",
		}

		result, err := resolveServerlessChild(context.Background(), routingCtx)
		assert.NoError(t, err)
		assert.Equal(t, "ap-southeast-1-a02", result.ProjectID)
		assert.Equal(t, ServerlessParentApplicationID, result.ApplicationID)
	})

	t.Run("X-Kubernetes-Id header skips mgmt lookup", func(t *testing.T) {
		clearResolverCache()
		mgmtCalled := false
		mgmtListClustersFunc = func(ctx context.Context, env string, clusterID uint64, showDeleted bool) (*tidbmgmt.ListClustersResponse, error) {
			mgmtCalled = true
			return nil, fmt.Errorf("mgmt should not be called")
		}

		routingCtx := globalstructs.RoutingContext{
			BizType:       "staging-devtier",
			TenantID:      "999",
			ApplicationID: "12345",
			Region:        "us-east-1",
			KubernetesID:  "staging-us-east-1-f02",
		}

		result, err := resolveServerlessChild(context.Background(), routingCtx)
		assert.NoError(t, err)
		assert.False(t, mgmtCalled, "mgmt must not be called when X-Kubernetes-Id is set")
		assert.Equal(t, "us-east-1-f02", result.ProjectID)
		assert.Equal(t, ServerlessParentApplicationID, result.ApplicationID)
	})

	t.Run("showDeleted cache entries do not satisfy active lookups", func(t *testing.T) {
		clearResolverCache()
		mgmtCallCount := 0
		mgmtListClustersFunc = func(ctx context.Context, env string, clusterID uint64, showDeleted bool) (*tidbmgmt.ListClustersResponse, error) {
			mgmtCallCount++
			if showDeleted {
				return &tidbmgmt.ListClustersResponse{
					Clusters: []*tidbmgmt.Cluster{
						{
							ClusterInfo: &tidbmgmt.Cluster_ServerlessCluster{
								ServerlessCluster: &serverlessglobalv1.ServerlessServiceCluster{
									Cluster: &serverlessregionalv1.ServerlessRegionalServiceClusterInfo{
										Kubernetes: "dev-us-east-1-f04",
									},
								},
							},
						},
					},
				}, nil
			}
			return &tidbmgmt.ListClustersResponse{Clusters: []*tidbmgmt.Cluster{}}, nil
		}

		deletedLookup := globalstructs.RoutingContext{
			BizType:       "dev-devtier",
			TenantID:      "1369847559692509645",
			ApplicationID: "10174902202723360328",
			Region:        "us-east-1",
			ShowDeleted:   true,
		}
		activeLookup := deletedLookup
		activeLookup.ShowDeleted = false

		result, err := resolveServerlessChild(context.Background(), deletedLookup)
		assert.NoError(t, err)
		assert.Equal(t, "us-east-1-f04", result.ProjectID)

		_, err = resolveServerlessChild(context.Background(), activeLookup)
		assert.Error(t, err)
		assert.Equal(t, 2, mgmtCallCount)
	})
}

func TestDefaultMgmtListClustersHonorsShowDeleted(t *testing.T) {
	const env = "dev"
	const clusterID = uint64(12345)

	origClient, hadOrig := mgmtClients.Load(env)
	defer func() {
		if hadOrig {
			mgmtClients.Store(env, origClient)
			return
		}
		mgmtClients.Delete(env)
	}()

	t.Run("active lookup keeps deleted filter false", func(t *testing.T) {
		var capturedReq *tidbmgmt.ListClustersRequest
		mgmtClients.Store(env, &mockClusterServiceClient{
			listClustersFn: func(ctx context.Context, in *tidbmgmt.ListClustersRequest, opts ...grpc.CallOption) (*tidbmgmt.ListClustersResponse, error) {
				capturedReq = in
				return &tidbmgmt.ListClustersResponse{}, nil
			},
		})

		_, err := defaultMgmtListClusters(context.Background(), env, clusterID, false)
		assert.NoError(t, err)
		assert.NotNil(t, capturedReq)
		assert.NotNil(t, capturedReq.Filter)
		assert.NotNil(t, capturedReq.Filter.Deleted)
		assert.False(t, *capturedReq.Filter.Deleted)
	})

	t.Run("deleted lookup flips deleted filter true", func(t *testing.T) {
		var capturedReq *tidbmgmt.ListClustersRequest
		mgmtClients.Store(env, &mockClusterServiceClient{
			listClustersFn: func(ctx context.Context, in *tidbmgmt.ListClustersRequest, opts ...grpc.CallOption) (*tidbmgmt.ListClustersResponse, error) {
				capturedReq = in
				return &tidbmgmt.ListClustersResponse{}, nil
			},
		})

		_, err := defaultMgmtListClusters(context.Background(), env, clusterID, true)
		assert.NoError(t, err)
		assert.NotNil(t, capturedReq)
		assert.NotNil(t, capturedReq.Filter)
		assert.NotNil(t, capturedReq.Filter.Deleted)
		assert.True(t, *capturedReq.Filter.Deleted)
	})
}
