package proxy

import (
	"context"
	"net/http"
	"net/http/httptest"
	"regexp"
	"strings"
	"testing"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/jarcoal/httpmock"
	"github.com/onsi/gomega"
	gocache "github.com/patrickmn/go-cache"
	"github.com/stretchr/testify/require"

	apierr "github.com/pingcap-inc/clinic/pkg/api/error"
	"github.com/pingcap-inc/clinic/pkg/auth/session"
	"github.com/pingcap-inc/clinic/pkg/o11y"
)

func TestGetO11yInfo(t *testing.T) {
	g := gomega.NewWithT(t)
	str1 := `{o11y_cluster_info="seed/aws/us-west-2/ba7bffa6-679b-6641-65f7-adcb81edbd33"}`
	g.Expect(GetPromQLData(str1, O11yClusterInfoVariable)).To(gomega.Equal("seed/aws/us-west-2/ba7bffa6-679b-6641-65f7-adcb81edbd33"))

	str2 := `{application_id=""}`
	g.Expect(GetPromQLData(str2, O11yClusterInfoVariable)).To(gomega.Equal(""))

	str3 := `{o11y_cluster_info="us-east-1-f01/aws/us-east-1/961a3701-8e6c-42fb-8b09-abe80f876186", application_id="base"}`
	g.Expect(GetPromQLData(str3, O11yClusterInfoVariable)).To(gomega.Equal("us-east-1-f01/aws/us-east-1/961a3701-8e6c-42fb-8b09-abe80f876186"))

	str4 := `{application_id="seed"}`
	g.Expect(GetPromQLData(str4, O11yClusterInfoVariable)).To(gomega.Equal(""))

	str5 := `kube_pod_info{namespace="cert-manager",o11y_cluster_info=~".*|seed/aws/us-west-2/ba7bffa6-679b-6641-65f7-adcb81edbd33"}`
	g.Expect(GetPromQLData(str5, O11yClusterInfoVariable)).To(gomega.Equal("seed/aws/us-west-2/ba7bffa6-679b-6641-65f7-adcb81edbd33"))
}

func TestGetO11yAppID(t *testing.T) {
	g := gomega.NewWithT(t)
	str1 := `{application_id="devtier-infra",region="us-west-2"}`
	g.Expect(GetO11yAppID(str1)).To(gomega.Equal("devtier-infra"))

	str2 := `{application_id=""}`
	g.Expect(GetO11yAppID(str2)).To(gomega.Equal(""))

	str3 := `{region="us-west-2", application_id="base"}`
	g.Expect(GetO11yAppID(str3)).To(gomega.Equal("base"))

	str4 := `{application_id="seed"}`
	g.Expect(GetO11yAppID(str4)).To(gomega.Equal("seed"))

	str5 := `{region="seed"}`
	g.Expect(GetO11yAppID(str5)).To(gomega.Equal(""))

	str6 := `{application_id="*"}`
	g.Expect(GetO11yAppID(str6)).To(gomega.Equal("*"))
}

func TestGetO11yRegion(t *testing.T) {
	g := gomega.NewWithT(t)
	str1 := `{application_id="devtier-infra",region="us-west-2"}`
	g.Expect(GetO11yRegion(str1)).To(gomega.Equal("us-west-2"))

	str2 := `{application_id=""}`
	g.Expect(GetO11yRegion(str2)).To(gomega.Equal(""))

	str3 := `{region="us-west-2", application_id="base"}`
	g.Expect(GetO11yRegion(str3)).To(gomega.Equal("us-west-2"))

	str4 := `{application_id="seed"}`
	g.Expect(GetO11yRegion(str4)).To(gomega.Equal(""))

	str5 := `{region="us-east-1"}`
	g.Expect(GetO11yRegion(str5)).To(gomega.Equal("us-east-1"))
}

func TestParseControlPlaneInfoV2(t *testing.T) {
	g := gomega.NewWithT(t)

	projectID, provider, region, applicationID, err := parseControlPlaneInfoV2("us-east-1-f03/aws-us-east-1/devtier-infra")
	g.Expect(err).To(gomega.Succeed())
	g.Expect(projectID).To(gomega.Equal("us-east-1-f03"))
	g.Expect(provider).To(gomega.Equal("aws"))
	g.Expect(region).To(gomega.Equal("us-east-1"))
	g.Expect(applicationID).To(gomega.Equal("devtier-infra"))

	badProjectID, badProvider, badRegion, badApplicationID, err := parseControlPlaneInfoV2("us-east-1-f03/aws-us-east-1")
	g.Expect(err).NotTo(gomega.Succeed())
	g.Expect(badProjectID).To(gomega.Equal(""))
	g.Expect(badProvider).To(gomega.Equal(""))
	g.Expect(badRegion).To(gomega.Equal(""))
	g.Expect(badApplicationID).To(gomega.Equal(""))
}

func TestO11yClusterInfoCacheStoresClusterWithTTL(t *testing.T) {
	g := gomega.NewWithT(t)
	handler := NewO11yProxyHandler(nil, "dev")
	handler.o11yClusterInfoCache = gocache.New(gocache.NoExpiration, gocache.NoExpiration)

	handler.setO11yClusterInfoToCache([]o11y.ClusterInfo{{
		ProjectID:       "seed",
		Vendor:          "gcp",
		Region:          "us-west1",
		ApplicationID:   "seed",
		ID:              "real-o11y-id",
		InternalReadUri: "https://www.ds.seed.example.com/internal/metrics/real-o11y-id/api/v1/",
	}})

	cluster, ok := handler.GetO11yClusterFromCache(context.Background(), "seed/gcp-us-west1/seed")
	g.Expect(ok).To(gomega.BeTrue())
	g.Expect(cluster.ID).To(gomega.Equal("real-o11y-id"))
	g.Expect(cluster.InternalReadUri).To(gomega.Equal("https://www.ds.seed.example.com/internal/metrics/real-o11y-id/api/v1/"))

	o11yClusterInfo, ok := handler.GetO11yClusterInfoFromCache(context.Background(), "seed/gcp-us-west1/seed")
	g.Expect(ok).To(gomega.BeTrue())
	g.Expect(o11yClusterInfo).To(gomega.Equal("seed/gcp/us-west1/real-o11y-id"))

	_, expiresAt, found := handler.o11yClusterInfoCache.GetWithExpiration("seed/gcp-us-west1/seed")
	g.Expect(found).To(gomega.BeTrue())
	g.Expect(time.Until(expiresAt)).To(gomega.BeNumerically(">", 4*time.Minute))
	g.Expect(time.Until(expiresAt)).To(gomega.BeNumerically("<=", o11yClusterInfoCacheTTL))
}

func TestProxyControlPlaneMetricsNormalizesTwoPartHeader(t *testing.T) {
	httpmock.Activate()
	defer httpmock.DeactivateAndReset()

	httpmock.RegisterResponder(http.MethodPost, "https://tidb-soc2.us.auth0.com/oauth/token", func(_ *http.Request) (*http.Response, error) {
		return httpmock.NewStringResponse(http.StatusOK, `{"access_token":"token","expires_in":86400,"token_type":"Bearer"}`), nil
	})
	httpmock.RegisterResponder(http.MethodGet, "http://www.gs.us-west-2.aws.observability-staging.pingcap.cloud/api/v1/clusters/", func(r *http.Request) (*http.Response, error) {
		query := r.URL.Query()
		require.Equal(t, "dev-tidbcloud", query.Get("bizType"))
		require.Equal(t, "gcp", query.Get("vendor"))
		require.Equal(t, "us-west1", query.Get("region"))
		require.Equal(t, "seed", query.Get("projectID"))
		require.Equal(t, "seed", query.Get("applicationID"))

		body := `{"code":0,"data":{"clusters":[{"applicationID":"seed","bizType":"dev-tidbcloud","id":"seed-o11y-id","projectID":"seed","region":"us-west1","tenantID":"infra-dev","vendor":"gcp","version":"1","internalReadUri":"https://metrics.example.com/internal/metrics/seed-o11y-id/api/v1/"}],"continueToken":"","remainingItemCount":0},"message":""}`
		return httpmock.NewStringResponse(http.StatusOK, body), nil
	})
	upstreamPattern := regexp.MustCompile(`^https://metrics\.example\.com/internal/metrics/seed-o11y-id/api/v1/query_range(?:\?.*)?$`)
	httpmock.RegisterRegexpResponder(http.MethodGet, upstreamPattern, func(r *http.Request) (*http.Response, error) {
		require.Equal(t, "up", r.URL.Query().Get("query"))
		return httpmock.NewStringResponse(http.StatusOK, `{"status":"success","data":{"resultType":"matrix","result":[]}}`), nil
	})

	handler := NewO11yProxyHandler(nil, "dev")
	handler.SetO11y(newO11yProxyTestClient(t))
	router := newControlPlaneMetricsTestRouter(handler)
	req := httptest.NewRequest(http.MethodGet, "/data/control_plane/metrics?query=up", nil)
	req.Header.Set(ControlPlaneInfoVariable, "seed/gcp-us-west1")
	response := createTestResponseRecorder()

	router.ServeHTTP(response, req)

	require.Equal(t, http.StatusOK, response.Code)
	require.JSONEq(t, `{"status":"success","data":{"resultType":"matrix","result":[]}}`, response.Body.String())
}

func TestProxyControlPlaneMetricsRejectsMalformedExplicitSelector(t *testing.T) {
	handler := NewO11yProxyHandler(nil, "dev")
	handler.SetO11y(newO11yProxyTestClient(t))
	router := newControlPlaneMetricsTestRouter(handler)
	req := httptest.NewRequest(http.MethodGet, "/data/control_plane/metrics?query=up", nil)
	req.Header.Set(ControlPlaneInfoVariable, "seed/aws-us-west-2/seed/extra")
	response := httptest.NewRecorder()

	router.ServeHTTP(response, req)

	require.Equal(t, http.StatusBadRequest, response.Code)
}

func TestProxyControlPlaneMetricsMapsResolverFailures(t *testing.T) {
	tests := []struct {
		name      string
		responder httpmock.Responder
		wantCode  int
	}{
		{
			name: "missing seed",
			responder: func(_ *http.Request) (*http.Response, error) {
				return httpmock.NewStringResponse(http.StatusOK, `{"code":0,"data":{"clusters":[],"continueToken":"","remainingItemCount":0},"message":""}`), nil
			},
			wantCode: http.StatusNotFound,
		},
		{
			name: "metadata failure",
			responder: func(_ *http.Request) (*http.Response, error) {
				return nil, context.DeadlineExceeded
			},
			wantCode: http.StatusInternalServerError,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			httpmock.Activate()
			defer httpmock.DeactivateAndReset()
			httpmock.RegisterResponder(http.MethodPost, "https://tidb-soc2.us.auth0.com/oauth/token", func(_ *http.Request) (*http.Response, error) {
				return httpmock.NewStringResponse(http.StatusOK, `{"access_token":"token","expires_in":86400,"token_type":"Bearer"}`), nil
			})
			httpmock.RegisterResponder(http.MethodGet, "http://www.gs.us-west-2.aws.observability-staging.pingcap.cloud/api/v1/clusters/", tt.responder)

			handler := NewO11yProxyHandler(nil, "dev")
			handler.SetO11y(newO11yProxyTestClient(t))
			router := newControlPlaneMetricsTestRouter(handler)
			req := httptest.NewRequest(http.MethodGet, "/data/control_plane/metrics?query=up", nil)
			req.Header.Set(ControlPlaneInfoVariable, "seed/aws-us-west-2/seed")
			response := httptest.NewRecorder()

			router.ServeHTTP(response, req)

			require.Equal(t, tt.wantCode, response.Code)
		})
	}
}

func TestProxyControlPlaneMetricsWithoutSelectorKeepsLegacyEmptySuccess(t *testing.T) {
	handler := NewO11yProxyHandler(nil, "dev")
	handler.SetO11y(newO11yProxyTestClient(t))
	router := newControlPlaneMetricsTestRouter(handler)
	response := httptest.NewRecorder()

	router.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/data/control_plane/metrics?query=up", nil))

	require.Equal(t, http.StatusOK, response.Code)
	require.JSONEq(t, `{"status":"success","data":{"resultType":"vector","result":[]}}`, response.Body.String())
}

func TestControlPlaneInfoFromMetricsRequestUsesPromQL(t *testing.T) {
	gin.SetMode(gin.TestMode)
	c, _ := gin.CreateTestContext(httptest.NewRecorder())
	c.Request = httptest.NewRequest(http.MethodGet, `/data/control_plane/metrics?query=up%7Bcontrol_plane_info%3D%22seed%2Faws-us-west-2%22%7D`, nil)

	selector, source := controlPlaneInfoFromMetricsRequest(c)

	require.Equal(t, "seed/aws-us-west-2", selector)
	require.Equal(t, "promql", source)
}

func newO11yProxyTestClient(t *testing.T) *o11y.Client {
	t.Helper()
	client, err := o11y.New(
		"www.gs.us-west-2.aws.observability-staging.pingcap.cloud",
		"www.ds.%s.%s.observability-staging.pingcap.cloud",
		"client-id",
		"client-secret",
		"dev",
	)
	require.NoError(t, err)
	return client
}

func newControlPlaneMetricsTestRouter(handler *O11yProxyHandler) *gin.Engine {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	router.Use(func(c *gin.Context) {
		c.Set(session.CtxUserKey, &session.CtxUser{UserID: 1, Internal: true})
		c.Next()
	})
	router.Use(apierr.ErrorHandlerFn)
	router.GET("/data/control_plane/metrics", func(c *gin.Context) {
		handler.ProxyControlPlaneMetrics(c, func(string) string { return "/api/v1/query_range" })
	})
	return router
}

func TestBuildModifyFuncByInternalReadURI(t *testing.T) {
	g := gomega.NewWithT(t)
	req, err := http.NewRequest(http.MethodGet, "http://clinic.local/metric/o11y/query/api/v1/query_range?metrics_scope=parent&query=up", nil)
	g.Expect(err).To(gomega.Succeed())

	modify := buildModifyFuncByInternalReadURI(
		"www.ds.seed.example.com",
		"/internal/metrics/real-o11y-id/api/v1/",
		"",
		func(s string) string {
			return strings.TrimPrefix(s, "/metric/o11y/query")
		},
	)
	req = modify(req)

	g.Expect(req.Host).To(gomega.Equal("www.ds.seed.example.com"))
	g.Expect(req.URL.Path).To(gomega.Equal("/internal/metrics/real-o11y-id/api/v1/query_range"))
	g.Expect(req.URL.Query().Get("metrics_scope")).To(gomega.Equal(""))
	g.Expect(req.URL.Query().Get("query")).To(gomega.Equal("up"))
}
