package router_test

import (
	"context"
	"fmt"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/require"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/cloud/gcp"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/proxy"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/router"
	appboxhander "github.com/tidbcloud/infra-provider/regional-server/pkg/router/appbox"
	networkhandler "github.com/tidbcloud/infra-provider/regional-server/pkg/router/network"
	vpcpeeringhandler "github.com/tidbcloud/infra-provider/regional-server/pkg/router/vpcpeering"
)

func TestMountVPCPeeringHandler(t *testing.T) {
	gin.SetMode(gin.TestMode)
	eng := gin.Default()
	mountFn := router.MountVPCPeeringHandler(&vpcpeeringhandler.VPCPeeringHandler{})
	mountFn(eng.Group("/api/dedicated/v1"))
	routesInfo := eng.Routes()
	pathLookup := make(map[string]bool)
	for _, info := range routesInfo {
		pathLookup[fmt.Sprintf("%s|%s", info.Path, info.Method)] = true
	}
	require.True(t, pathLookup["/api/dedicated/v1/vpcPeerings|GET"], "route for vpcpeering handler not found")
	require.True(t, pathLookup["/api/dedicated/v1/vpcPeerings/:vpcPeeringId|GET"], "route for vpcpeering handler not found")
	require.True(t, pathLookup["/api/dedicated/v1/vpcPeerings|POST"], "route for vpcpeering handler not found")
	require.True(t, pathLookup["/api/dedicated/v1/vpcPeerings/:vpcPeeringId|DELETE"], "route for vpcpeering handler not found")
}

func TestMountAPPBoxHandler(t *testing.T) {
	gin.SetMode(gin.TestMode)
	eng := gin.Default()
	mountFn := router.MountAPPBoxServiceHandler(&appboxhander.APPBoxHandler{})
	mountFn(eng.Group("/api/dedicated/v1"))
	routesInfo := eng.Routes()
	pathLookup := make(map[string]bool)
	for _, info := range routesInfo {
		pathLookup[fmt.Sprintf("%s|%s", info.Path, info.Method)] = true
	}
	require.True(t, pathLookup["/api/dedicated/v1/appboxes|GET"], "route for appbox handler not found")
}

func TestMountCompatNetworkHandler(t *testing.T) {
	gin.SetMode(gin.TestMode)
	eng := gin.Default()
	mountFn := router.MountCompatNeworkServiceHandler(&networkhandler.NetworkHandler{})
	mountFn(eng.Group("/api/dedicated/v1"))
	routesInfo := eng.Routes()
	pathLookup := make(map[string]bool)
	for _, info := range routesInfo {
		pathLookup[fmt.Sprintf("%s|%s", info.Path, info.Method)] = true
	}
	require.True(t, pathLookup["/api/dedicated/v1/compatnetworks/:vpcId|GET"], "route for network handler not found")
	require.True(t, pathLookup["/api/dedicated/v1/regionconfig|GET"], "route for regionconfig not found")
}

func TestMountClusterServiceProxyHandler(t *testing.T) {
	gin.SetMode(gin.TestMode)
	eng := gin.Default()
	rp, err := proxy.NewReverseProxy("http://localhost:8080")
	require.NoError(t, err)
	mountFn := router.MountClusterServiceProxyHandler(rp, proxy.NewClusterAgentProxy(nil))
	mountFn(eng.Group("/api/dedicated/v1"))
	routesInfo := eng.Routes()
	pathLookup := make(map[string]bool)
	for _, info := range routesInfo {
		pathLookup[fmt.Sprintf("%s|%s", info.Path, info.Method)] = true
	}
	expectedRoutes := []string{
		"/api/dedicated/v1/clusters/:clusterId/getComponentNodeInfos|GET",
		"/api/dedicated/v1/clusters/:clusterId/getProxyConfidentialInfo|GET",
		"/api/dedicated/v1/clusters/:clusterId/tidbgroups|GET",
		"/api/dedicated/v1/clusters/:clusterId/tidbgroups/:tidbGroupId|GET",
		"/api/dedicated/v1/clusters/:clusterId/tidbgroups/:tidbGroupId|DELETE",
		"/api/dedicated/v1/clusters/:clusterId/tidbgroups/:tidbGroupId|PATCH",
		"/api/dedicated/v1/clusters/:clusterId/tidbgroups|POST",
		"/api/dedicated/v1/clusters/:clusterId/gkeHeterogeneous/nodePools|GET",
		"/api/dedicated/v1/clusters/:clusterId/gkeHeterogeneous/nodePools/:componentType|GET",
		"/api/dedicated/v1/clusters/:clusterId/gkeHeterogeneous/nodePools|POST",
		"/api/dedicated/v1/clusters/:clusterId/gkeHeterogeneous/deploys|POST",
		"/api/dedicated/v1/clusters/:clusterId/gkeHeterogeneous/deploys/:componentType|PATCH",
		"/api/dedicated/v1/clusters/:clusterId/gkeHeterogeneous/deploys|GET",
		"/api/dedicated/v1/clusters/:clusterId/gkeHeterogeneous/deploys/:componentType|GET",
		"/api/dedicated/v1/clusters/:clusterId/gkeHeterogeneous/deploys/:componentType|DELETE",
		"/api/dedicated/v1/clusters/:clusterId/gkeHeterogeneous/computeClasses|POST",
		"/api/dedicated/v1/clusters/:clusterId/gkeHeterogeneous/computeClasses|GET",
		"/api/dedicated/v1/clusters/:clusterId/gkeHeterogeneous/computeClasses/:name|PATCH",
		"/api/dedicated/v1/clusters/:clusterId/gkeHeterogeneous/computeClasses/:name|DELETE",
		"/api/dedicated/v1/clusters/:clusterId/gkeHeterogeneous/gatekeeperAssigns|POST",
		"/api/dedicated/v1/clusters/:clusterId/gkeHeterogeneous/gatekeeperAssigns/:componentType|GET",
		"/api/dedicated/v1/clusters/:clusterId/gkeHeterogeneous/gatekeeperAssigns/:name|PATCH",
		"/api/dedicated/v1/clusters/:clusterId/gkeHeterogeneous/gatekeeperAssigns/:name|DELETE",
		"/api/dedicated/v1/clusters/:clusterId/gkePolicyControl:enable|POST",
		"/api/dedicated/v1/clusters/:clusterId/gkePolicyControl|GET",
		"/api/dedicated/v1/clusters/:clusterId/getCaCertificate|GET",
		"/api/dedicated/v1/clusters/:clusterId/setClientCaCertificate|POST",
		"/api/dedicated/v1/clusters/:clusterId/setAuditLogConfiguration|POST",
		"/api/dedicated/v1/clusters/:clusterId/getAuditLogConfiguration|GET",
		"/api/dedicated/v1/clusters/:clusterId/validateAuditLogConfiguration|PUT",
		"/api/dedicated/v1/clusters/:clusterId/getVersionAndConfigurations|GET",
		"/api/dedicated/v1/clusters/:clusterId/setVersionAndConfigurations|POST",
		"/api/dedicated/v1/clusters/:clusterId/validateVersionAndConfigurations|PUT",
		"/api/dedicated/v1/clusters/:clusterId/getClusterRole|GET",
	}
	for _, route := range expectedRoutes {
		require.True(t, pathLookup[route], "route %s not found", route)
	}
}

func TestMountClusterServiceProxyHandlerRoutesGKEHeterogeneousAgentAPIsToAgent(t *testing.T) {
	gin.SetMode(gin.TestMode)
	clusterBackend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("X-Backend", "cluster")
	}))
	defer clusterBackend.Close()
	agentBackend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("X-Backend", "agent")
		w.Header().Set("X-Path", r.URL.Path)
		w.Header().Set("X-Raw-Query", r.URL.RawQuery)
	}))
	defer agentBackend.Close()

	scheme := runtime.NewScheme()
	require.NoError(t, cloudv1.AddToScheme(scheme))
	cluster := &cloudv1.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "cluster-123",
			Namespace: "default",
			Labels: map[string]string{
				cloudv1.ClusterTagKey:     "123",
				cloudv1.ProviderTypeLabel: cloudv1.ProviderTypeGKE,
				cloudv1.RegionLabel:       "us-west1",
			},
		},
		Spec: cloudv1.ClusterSpec{
			NetworkName: "network-1",
			Kubernetes: cloudv1.Kubernetes{
				Account: "cluster-project",
				Name:    "dedicated-dev-us-west1-abc",
			},
		},
		Status: cloudv1.ClusterStatus{
			Gateway: &cloudv1.GatewayStatus{Endpoint: agentBackend.URL},
		},
	}
	kubeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(cluster).Build()

	clusterRP, err := proxy.NewReverseProxy(clusterBackend.URL)
	require.NoError(t, err)
	clusterRP = clusterRP.WithPathPerfixRewrite("/api/dedicated/v1", "/api/v1")

	eng := gin.Default()
	clusterAgentProxy := proxy.NewClusterAgentProxy(kubeClient).WithGKEPolicyControlResourceManager(&routeTestGKEPolicyControlResourceManager{})
	mountFn := router.MountClusterServiceProxyHandler(clusterRP, clusterAgentProxy)
	mountFn(eng.Group("/api/dedicated/v1"))
	regionalServer := httptest.NewServer(eng)
	defer regionalServer.Close()

	tests := []struct {
		name      string
		method    string
		path      string
		backend   string
		wantPath  string
		wantQuery string
	}{
		{
			name:    "deploy uses cluster service",
			method:  http.MethodGet,
			path:    "/api/dedicated/v1/clusters/123/gkeHeterogeneous/deploys",
			backend: "cluster",
		},
		{
			name:    "nodepool uses cluster service",
			method:  http.MethodGet,
			path:    "/api/dedicated/v1/clusters/123/gkeHeterogeneous/nodePools",
			backend: "cluster",
		},
		{
			name:      "compute class uses cluster agent",
			method:    http.MethodGet,
			path:      "/api/dedicated/v1/clusters/123/gkeHeterogeneous/computeClasses?base_instance_type=n2-standard-16",
			backend:   "agent",
			wantPath:  "/cluster-agent/tidbcluster-123/api/v1/clusters/cluster-123/gkeHeterogeneous/computeClasses",
			wantQuery: "base_instance_type=n2-standard-16",
		},
		{
			name:     "gatekeeper assign uses cluster agent",
			method:   http.MethodGet,
			path:     "/api/dedicated/v1/clusters/123/gkeHeterogeneous/gatekeeperAssigns/TIDB",
			backend:  "agent",
			wantPath: "/cluster-agent/tidbcluster-123/api/v1/clusters/cluster-123/gkeHeterogeneous/gatekeeperAssigns/TIDB",
		},
		{
			name:     "policy control enable uses cluster agent",
			method:   http.MethodPost,
			path:     "/api/dedicated/v1/clusters/123/gkePolicyControl:enable",
			backend:  "agent",
			wantPath: "/cluster-agent/tidbcluster-123/api/v1/clusters/cluster-123/gkePolicyControl:enable",
		},
		{
			name:      "policy control status uses cluster agent",
			method:    http.MethodGet,
			path:      "/api/dedicated/v1/clusters/123/gkePolicyControl",
			backend:   "agent",
			wantPath:  "/cluster-agent/tidbcluster-123/api/v1/clusters/cluster-123/gkePolicyControl",
			wantQuery: "fleet_project_id=cluster-project&gke_cluster_name=dedicated-dev-us-west1-abc&location=us-west1&project_id=cluster-project",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			req, err := http.NewRequest(tt.method, regionalServer.URL+tt.path, nil)
			require.NoError(t, err)
			resp, err := http.DefaultClient.Do(req)
			require.NoError(t, err)
			defer func() { _ = resp.Body.Close() }()

			require.Equal(t, http.StatusOK, resp.StatusCode)
			require.Equal(t, tt.backend, resp.Header.Get("X-Backend"))
			if tt.wantPath != "" {
				require.Equal(t, tt.wantPath, resp.Header.Get("X-Path"))
			}
			if tt.wantQuery != "" {
				require.Equal(t, tt.wantQuery, resp.Header.Get("X-Raw-Query"))
			}
		})
	}
}

func TestMountClusterServiceHijackProxyHandler(t *testing.T) {
	gin.SetMode(gin.TestMode)
	eng := gin.Default()
	rp, err := proxy.NewReverseProxy("http://localhost:8080")
	require.NoError(t, err)
	chp, err := proxy.NewClusterServiceHijackProxy(rp, nil, nil, nil)
	require.NoError(t, err)
	mountFn := router.MountClusterServiceHijackProxyHandler(chp)
	mountFn(eng.Group("/api/dedicated/v1"))
	routesInfo := eng.Routes()
	pathLookup := make(map[string]bool)
	for _, info := range routesInfo {
		pathLookup[fmt.Sprintf("%s|%s", info.Path, info.Method)] = true
	}
	require.True(t, pathLookup["/api/dedicated/v1/clusters|GET"], "route for proxy handler not found")
	require.True(t, pathLookup["/api/dedicated/v1/clusters/:clusterId|GET"], "route for proxy handler not found")
	require.True(t, pathLookup["/api/dedicated/v1/clusters/:clusterId/scale|POST"], "route for proxy handler not found")
	require.True(t, pathLookup["/api/dedicated/v1/clusters/:clusterId/pause|POST"], "route for proxy handler not found")
	require.True(t, pathLookup["/api/dedicated/v1/clusters/:clusterId/resume|POST"], "route for proxy handler not found")
}

type routeTestGKEPolicyControlResourceManager struct{}

func (r *routeTestGKEPolicyControlResourceManager) GetProjectIdentity(_ context.Context, project string) (gcp.ProjectIdentity, error) {
	return gcp.ProjectIdentity{
		Input:         project,
		ProjectID:     project,
		ProjectNumber: project,
	}, nil
}

func (r *routeTestGKEPolicyControlResourceManager) EnsureProjectCustomRole(_ context.Context, project, roleID string, _ gcp.ProjectCustomRole) (string, error) {
	return "projects/" + project + "/roles/" + roleID, nil
}

func (r *routeTestGKEPolicyControlResourceManager) AddProjectPolicyBinding(_ context.Context, _ string, _ string, _ string) error {
	return nil
}

func (r *routeTestGKEPolicyControlResourceManager) DelProjectPolicyBinding(_ context.Context, _ string, _ string, _ string) error {
	return nil
}
