package api

import (
	"net/http"
	"net/http/httptest"
	"sync"
	"testing"

	"dataservices/commons/config"
	"dataservices/commons/constants"
	"dataservices/commons/metrics"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/require"
)

var registerGatewayMetricsOnce sync.Once

func ensureGatewayMetricsRegistered() {
	registerGatewayMetricsOnce.Do(func() {
		metrics.RegisterDataApiGatewayMetrics()
	})
}

func TestRouteDataApiGatewayAPI_AliCloudRegionalMode(t *testing.T) {
	gin.SetMode(gin.TestMode)
	ensureGatewayMetricsRegistered()

	cfg := config.Global()
	oldDeployMode := cfg.ServerConf.DeployMode
	oldProvider := cfg.ServerlessConf.Provider
	defer func() {
		cfg.ServerConf.DeployMode = oldDeployMode
		cfg.ServerlessConf.Provider = oldProvider
	}()

	cfg.ServerConf.DeployMode = "region"
	cfg.ServerlessConf.Provider = constants.ClusterCloudProviderAliCloud

	engine := gin.New()
	RouteDataApiGatewayAPI(engine)

	routes := routeSet(engine)
	require.Contains(t, routes, "POST /inner/dataservice/v2/exec/sqltemplate")
	require.Contains(t, routes, "POST /inner/dataservice/exec/sql")
	require.Contains(t, routes, "POST /inner/dataservice/session/create")
	require.NotContains(t, routes, "GET /api/v1beta/app/:external_app_id/endpoint/*path")
	require.NotContains(t, routes, "POST /inner/dataservice/v2/endpoint/test")
	require.NotContains(t, routes, "POST /inner/dataservice/app_id_cluster_permission_check")
	require.NotContains(t, routes, "POST /dataservice/inner/dataservice/exec/sql")
}

func TestRouteDataApiGatewayAPI_DefaultMode(t *testing.T) {
	gin.SetMode(gin.TestMode)
	ensureGatewayMetricsRegistered()

	cfg := config.Global()
	oldDeployMode := cfg.ServerConf.DeployMode
	oldProvider := cfg.ServerlessConf.Provider
	defer func() {
		cfg.ServerConf.DeployMode = oldDeployMode
		cfg.ServerlessConf.Provider = oldProvider
	}()

	cfg.ServerConf.DeployMode = "region"
	cfg.ServerlessConf.Provider = constants.ClusterCloudProviderAWS

	engine := gin.New()
	RouteDataApiGatewayAPI(engine)

	routes := routeSet(engine)
	require.Contains(t, routes, "GET /api/v1beta/app/:external_app_id/endpoint/*path")
	require.Contains(t, routes, "POST /inner/dataservice/v2/endpoint/test")
	require.Contains(t, routes, "POST /inner/dataservice/app_id_cluster_permission_check")
	require.NotContains(t, routes, "POST /:provider/inner/dataservice/v2/endpoint/test")
}

func routeSet(engine *gin.Engine) map[string]struct{} {
	routes := make(map[string]struct{}, len(engine.Routes()))
	for _, route := range engine.Routes() {
		routes[route.Method+" "+route.Path] = struct{}{}
	}
	return routes
}

func TestIsValidInnerDataServiceHost(t *testing.T) {
	cfg := config.Global()
	oldDeployMode := cfg.ServerConf.DeployMode
	oldProvider := cfg.ServerlessConf.Provider
	defer func() {
		cfg.ServerConf.DeployMode = oldDeployMode
		cfg.ServerlessConf.Provider = oldProvider
	}()

	cfg.ServerConf.DeployMode = "region"
	cfg.ServerlessConf.Provider = constants.ClusterCloudProviderAliCloud
	require.True(t, isValidInnerDataServiceHost("regionalsvc01.ap-southeast-1.dev.shared.alicloud.tidbcloud.com"))
	require.True(t, isValidInnerDataServiceHost("regionalsvc01.ap-southeast-1.prod.alicloud.tidbcloud.com"))
	require.True(t, isValidInnerDataServiceHost("ap-southeast-1-ng.regional-api.prod.aliyun.tidbcloud.com"))
	require.False(t, isValidInnerDataServiceHost("ap-southeast-1.inner-region.o11y.prod.tidbcloud.com"))

	cfg.ServerlessConf.Provider = constants.ClusterCloudProviderAWS
	require.True(t, isValidInnerDataServiceHost("us-west-2.inner-region.o11y.prod.tidbcloud.com"))
	require.False(t, isValidInnerDataServiceHost("regionalsvc01.ap-southeast-1.dev.shared.alicloud.tidbcloud.com"))
}

func TestInnerBasicAuthGuard_AliCloudRegionalMode(t *testing.T) {
	gin.SetMode(gin.TestMode)

	cfg := config.Global()
	oldDeployMode := cfg.ServerConf.DeployMode
	oldProvider := cfg.ServerlessConf.Provider
	oldEnv := cfg.ServerConf.Env
	oldToken := cfg.ServerConf.CrossRegionToken
	defer func() {
		cfg.ServerConf.DeployMode = oldDeployMode
		cfg.ServerlessConf.Provider = oldProvider
		cfg.ServerConf.Env = oldEnv
		cfg.ServerConf.CrossRegionToken = oldToken
	}()

	cfg.ServerConf.DeployMode = "region"
	cfg.ServerlessConf.Provider = constants.ClusterCloudProviderAliCloud
	cfg.ServerConf.Env = "dev"
	cfg.ServerConf.CrossRegionToken = "*"

	engine := gin.New()
	engine.Use(innerBasicAuthGuard())
	engine.POST("/inner/dataservice/exec/sql", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"type":    c.Request.Header.Get("X-Request-Type"),
			"service": c.Request.Header.Get("X-Request-Service"),
			"auth":    c.Request.Header.Get("Authorization"),
		})
	})

	t.Run("rejects missing auth header", func(t *testing.T) {
		req := httptest.NewRequest(http.MethodPost, "/inner/dataservice/exec/sql", nil)
		resp := httptest.NewRecorder()

		engine.ServeHTTP(resp, req)
		require.Equal(t, http.StatusUnauthorized, resp.Code)
	})

	t.Run("rejects malformed token", func(t *testing.T) {
		req := httptest.NewRequest(http.MethodPost, "/inner/dataservice/exec/sql", nil)
		req.Header.Set("Authorization", "Basic not-base64")
		resp := httptest.NewRecorder()

		engine.ServeHTTP(resp, req)
		require.Equal(t, http.StatusUnauthorized, resp.Code)
	})

	t.Run("rejects wrong env", func(t *testing.T) {
		req := httptest.NewRequest(http.MethodPost, "/inner/dataservice/exec/sql", nil)
		req.Header.Set("Authorization", "Basic *")
		resp := httptest.NewRecorder()

		engine.ServeHTTP(resp, req)
		require.Equal(t, http.StatusUnauthorized, resp.Code)
	})

	t.Run("accepts configured cross region token", func(t *testing.T) {
		req := httptest.NewRequest(http.MethodPost, "/inner/dataservice/exec/sql", nil)
		req.Header.Set("Authorization", "Basic "+cfg.ServerConf.CrossRegionToken)
		resp := httptest.NewRecorder()

		engine.ServeHTTP(resp, req)
		require.Equal(t, http.StatusOK, resp.Code)
		require.Contains(t, resp.Body.String(), "\"type\":\"inner\"")
		require.Contains(t, resp.Body.String(), "\"service\":\"dataservice_gateway\"")
		require.Contains(t, resp.Body.String(), "\"auth\":\"\"")
	})
}

func TestInnerBasicAuthGuard_AWSRegionalModeBypass(t *testing.T) {
	gin.SetMode(gin.TestMode)

	cfg := config.Global()
	oldDeployMode := cfg.ServerConf.DeployMode
	oldProvider := cfg.ServerlessConf.Provider
	defer func() {
		cfg.ServerConf.DeployMode = oldDeployMode
		cfg.ServerlessConf.Provider = oldProvider
	}()

	cfg.ServerConf.DeployMode = "region"
	cfg.ServerlessConf.Provider = constants.ClusterCloudProviderAWS

	engine := gin.New()
	engine.Use(innerBasicAuthGuard())
	engine.POST("/inner/dataservice/exec/sql", func(c *gin.Context) {
		c.Status(http.StatusOK)
	})

	req := httptest.NewRequest(http.MethodPost, "/inner/dataservice/exec/sql", nil)
	resp := httptest.NewRecorder()

	engine.ServeHTTP(resp, req)
	require.Equal(t, http.StatusOK, resp.Code)
}

func TestRouteDataApiGatewayAPI_AliCloudRegionalModeRequiresAuthorizationHeader(t *testing.T) {
	gin.SetMode(gin.TestMode)
	ensureGatewayMetricsRegistered()

	cfg := config.Global()
	oldDeployMode := cfg.ServerConf.DeployMode
	oldProvider := cfg.ServerlessConf.Provider
	oldEnv := cfg.ServerConf.Env
	oldToken := cfg.ServerConf.CrossRegionToken
	defer func() {
		cfg.ServerConf.DeployMode = oldDeployMode
		cfg.ServerlessConf.Provider = oldProvider
		cfg.ServerConf.Env = oldEnv
		cfg.ServerConf.CrossRegionToken = oldToken
	}()

	cfg.ServerConf.DeployMode = "region"
	cfg.ServerlessConf.Provider = constants.ClusterCloudProviderAliCloud
	cfg.ServerConf.Env = "dev"
	cfg.ServerConf.CrossRegionToken = "*"

	engine := gin.New()
	RouteDataApiGatewayAPI(engine)

	req := httptest.NewRequest(http.MethodPost, "/inner/dataservice/exec/sql", nil)
	req.Host = "regionalsvc01.ap-southeast-1.dev.shared.alicloud.tidbcloud.com"
	resp := httptest.NewRecorder()

	engine.ServeHTTP(resp, req)
	require.Equal(t, http.StatusUnauthorized, resp.Code)
}

func TestRouteDataApiGatewayAPI_AWSRegionalModeDoesNotRequireAuthorizationHeader(t *testing.T) {
	gin.SetMode(gin.TestMode)
	ensureGatewayMetricsRegistered()

	cfg := config.Global()
	oldDeployMode := cfg.ServerConf.DeployMode
	oldProvider := cfg.ServerlessConf.Provider
	defer func() {
		cfg.ServerConf.DeployMode = oldDeployMode
		cfg.ServerlessConf.Provider = oldProvider
	}()

	cfg.ServerConf.DeployMode = "region"
	cfg.ServerlessConf.Provider = constants.ClusterCloudProviderAWS

	engine := gin.New()
	RouteDataApiGatewayAPI(engine)

	req := httptest.NewRequest(http.MethodPost, "/inner/dataservice/exec/sql", nil)
	req.Host = "us-west-2.inner-region.o11y.prod.tidbcloud.com"
	resp := httptest.NewRecorder()

	engine.ServeHTTP(resp, req)
	require.NotEqual(t, http.StatusUnauthorized, resp.Code)
}
