package proxy

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

	"github.com/gin-gonic/gin"
	"github.com/jarcoal/httpmock"
	"github.com/onsi/gomega"

	"github.com/pingcap-inc/clinic/pkg/config"
	"github.com/pingcap-inc/clinic/pkg/db"
	"github.com/pingcap-inc/clinic/pkg/o11y"
	o11yurls "github.com/pingcap-inc/clinic/pkg/o11y/urls"
)

// https://github.com/gin-gonic/gin/blob/ce20f107f5dc498ec7489d7739541a25dcd48463/context_test.go#L1747-L1765
type testResponseRecorder struct {
	*httptest.ResponseRecorder
	closeChannel chan bool
}

func (r *testResponseRecorder) CloseNotify() <-chan bool {
	return r.closeChannel
}

func (r *testResponseRecorder) closeClient() {
	r.closeChannel <- true
}

func createTestResponseRecorder() *testResponseRecorder {
	return &testResponseRecorder{
		httptest.NewRecorder(),
		make(chan bool, 1),
	}
}

func routerTrimPrefix(s string) string {
	return strings.TrimPrefix(s, "/metric/query")
}

//type rbacMock struct{}
//
//func (r *rbacMock) InitOrgRBAC(orgID uint) error                                 { return nil }
//func (r *rbacMock) AttachRoleForUser(orgID uint, userID uint, roleID uint) error { return nil }
//func (r *rbacMock) Auth(userID uint, internal bool, resource rbac.Resource, act rbac.Acting, orgID uint) (bool, error) {
//	return true, nil
//}
//func (r *rbacMock) GetRolesOfUserInOrg(userID uint, orgID uint) []uint           { return nil }
//func (r *rbacMock) DeleteOrg(orgId uint) error                                   { return nil }
//func (r *rbacMock) GetUserAccessOrgs(userId uint) []uint                         { return nil }
//func (r *rbacMock) GetAllUsersInOrg(orgID uint) []uint                           { return nil }
//func (r *rbacMock) DeleteRoleForUser(orgID uint, userID uint, roleID uint) error { return nil }
//func (r *rbacMock) GetOneUserAccessOrg(userId uint) uint                         { return 0 }
//func (r *rbacMock) GetEnforcer() casbin.IEnforcer                                { return casbin.NewEnforcer() }

func Test_buildModifyFuncForVM(t *testing.T) {
	g := gomega.NewWithT(t)
	{
		vmSingleAddr := config.VMClusterAddressConfig{
			AddressConfig: config.AddressConfig{
				Host: "127.0.0.1",
				Port: 8084,
			},
			VMClusterMode: false,
		}
		modifyFunc := buildModifyFuncForVM(&vmSingleAddr, 42, routerTrimPrefix)
		r, _ := http.NewRequest(http.MethodGet, "http://localhost:8080/metric/query/api/v1/labels", nil)
		r.URL.RawPath = "/metric/query/api/v1/labels"
		modifiedRequest := modifyFunc(r)
		g.Expect(modifiedRequest.URL.Path).To(gomega.Equal("/api/v1/labels"))
		g.Expect(modifiedRequest.URL.RawPath).To(gomega.Equal("/api/v1/labels"))
	}
	{
		vmClusterAddr := config.VMClusterAddressConfig{
			AddressConfig: config.AddressConfig{
				Host: "127.0.0.1",
				Port: 8084,
			},
			VMClusterMode: true,
		}
		modifyFunc := buildModifyFuncForVM(&vmClusterAddr, 42, routerTrimPrefix)
		r, _ := http.NewRequest(http.MethodGet, "http://localhost:8080/metric/query/api/v1/labels", nil)
		r.URL.RawPath = "/metric/query/api/v1/labels"
		modifiedRequest := modifyFunc(r)
		g.Expect(modifiedRequest.URL.Path).To(gomega.Equal("/select/42/prometheus/api/v1/labels"))
		g.Expect(modifiedRequest.URL.RawPath).To(gomega.Equal("/select/42/prometheus/api/v1/labels"))
	}
}

func TestGetMetricsScope(t *testing.T) {
	g := gomega.NewWithT(t)
	gin.SetMode(gin.TestMode)

	{
		c, _ := gin.CreateTestContext(httptest.NewRecorder())
		c.Request, _ = http.NewRequest(http.MethodGet, "http://localhost:8080/metric/query/api/v1/query", nil)
		scope, err := getMetricsScope(c)
		g.Expect(err).To(gomega.Succeed())
		g.Expect(scope).To(gomega.Equal(MetricScopeChild))
	}
	{
		c, _ := gin.CreateTestContext(httptest.NewRecorder())
		c.Request, _ = http.NewRequest(http.MethodGet, "http://localhost:8080/metric/query/api/v1/query?metrics_scope=parent", nil)
		scope, err := getMetricsScope(c)
		g.Expect(err).To(gomega.Succeed())
		g.Expect(scope).To(gomega.Equal(MetricScopeParent))
	}
	{
		c, _ := gin.CreateTestContext(httptest.NewRecorder())
		c.Request, _ = http.NewRequest(http.MethodGet, "http://localhost:8080/metric/query/api/v1/query?metrics_scope=bad", nil)
		_, err := getMetricsScope(c)
		g.Expect(err).NotTo(gomega.Succeed())
	}
}

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

	g.Expect(isLogicalControlPlaneMetricsInfo("seed/gcp-us-west1/seed")).To(gomega.BeTrue())
	g.Expect(isLogicalControlPlaneMetricsInfo("us-east-1-f01/aws-us-east-1/devtier-infra")).To(gomega.BeFalse())
	g.Expect(isLogicalControlPlaneMetricsInfo("")).To(gomega.BeFalse())
}

func TestStripMetricsScopeParam(t *testing.T) {
	g := gomega.NewWithT(t)
	r, _ := http.NewRequest(http.MethodGet, "http://localhost:8080/metric/query/api/v1/query?metrics_scope=parent&query=up", nil)

	stripMetricsScopeParam(r)

	g.Expect(r.URL.Query().Get("metrics_scope")).To(gomega.Equal(""))
	g.Expect(r.URL.Query().Get("query")).To(gomega.Equal("up"))
}

func Test_buildModifyFuncByO11yID(t *testing.T) {
	g := gomega.NewWithT(t)
	cli, err := o11y.New(
		"www.gs.us-west-2.aws.observability-staging.pingcap.cloud",
		"www.ds.%s.%s.observability-staging.pingcap.cloud",
		"foo",
		"bar",
		"dev")
	g.Expect(err).To(gomega.Succeed())
	modifyFunc := buildModifyFuncByO11yID(cli, "www.ds.us-west-2.aws.observability-staging.pingcap.cloud", "foo", routerTrimPrefix, false)
	r, _ := http.NewRequest(http.MethodGet, "http://localhost:8080/metric/query/api/v1/labels", nil)
	r.URL.RawPath = "/metric/query/api/v1/labels"
	modifiedRequest := modifyFunc(r)
	g.Expect(modifiedRequest.URL.Path).To(gomega.Equal("/internal/metrics/foo/api/v1/labels"))
	g.Expect(modifiedRequest.URL.RawPath).To(gomega.Equal("/internal/metrics/foo/api/v1/labels"))
}

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

	for _, deployType := range []string{o11y.ByocType, o11y.ByocHostType} {
		g.Expect(isByocDeployType(deployType)).To(gomega.BeTrue())

		modifyFunc := withByocRequestHeaders(
			func(r *http.Request) *http.Request {
				r.URL.Path = "/internal/cluster-id/metrics/api/v1/query_range"
				return r
			},
			&db.Cluster{
				DeployType: deployType,
				TenantID:   2130533,
				Provider:   "aws",
				Region:     "us-west-2",
			},
			&o11y.ClusterInfo{
				BizType: "byoc",
			},
		)

		r, _ := http.NewRequest(http.MethodGet, "http://localhost:8080/metric/query/api/v1/query_range", nil)
		modifiedRequest := modifyFunc(r)
		g.Expect(modifiedRequest.URL.Path).To(gomega.Equal("/internal/cluster-id/metrics/api/v1/query_range"))
		g.Expect(modifiedRequest.Header.Get("X-Biz-Type")).To(gomega.Equal("byoc"))
		g.Expect(modifiedRequest.Header.Get("X-Org-Id")).To(gomega.Equal("2130533"))
		g.Expect(modifiedRequest.Header.Get("X-Region-Id")).To(gomega.Equal("aws-us-west-2"))
	}

	g.Expect(isByocDeployType(o11y.DedicatedType)).To(gomega.BeFalse())
}

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

	modifyFunc := withO11yClusterRequestHeaders(
		func(r *http.Request) *http.Request {
			r.URL.Path = "/internal/k8s-cluster-id/metrics/api/v1/query_range"
			return r
		},
		&o11y.ClusterInfo{
			BizType:  "byoc",
			TenantID: "2130533",
			Vendor:   "aws",
			Region:   "us-west-2",
		},
	)

	r, _ := http.NewRequest(http.MethodPost, "http://localhost:8080/metric/project/query/api/v1/query_range", nil)
	modifiedRequest := modifyFunc(r)
	g.Expect(modifiedRequest.URL.Path).To(gomega.Equal("/internal/k8s-cluster-id/metrics/api/v1/query_range"))
	g.Expect(modifiedRequest.Header.Get("X-Biz-Type")).To(gomega.Equal("byoc"))
	g.Expect(modifiedRequest.Header.Get("X-Org-Id")).To(gomega.Equal("2130533"))
	g.Expect(modifiedRequest.Header.Get("X-Region-Id")).To(gomega.Equal("aws-us-west-2"))
}

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

	_, targetHost, modifyFunc, _, err := buildProxyForMetricsTarget( //nolint:bodyclose
		context.Background(),
		&o11yurls.MetricsTarget{
			MetricsURL: "https://metrics.example.com/internal/cluster-id/metrics/api/v1/",
			O11yCluster: &o11y.ClusterInfo{
				BizType:  o11y.ByocType,
				TenantID: "1372813089196992794",
				Vendor:   "aws",
				Region:   "us-west-2",
			},
		},
		&db.Cluster{
			DeployType: o11y.ByocType,
			TenantID:   2130533,
			Provider:   "aws",
			Region:     "us-west-2",
		},
		routerTrimPrefix,
	)
	g.Expect(err).To(gomega.Succeed())
	g.Expect(targetHost).To(gomega.Equal("metrics.example.com"))

	r, _ := http.NewRequest(http.MethodGet, "http://localhost:8080/metric/query/api/v1/query?metrics_scope=child&query=up", nil)
	r.URL.RawPath = "/metric/query/api/v1/query"
	modifiedRequest := modifyFunc(r)

	g.Expect(modifiedRequest.URL.Path).To(gomega.Equal("/internal/cluster-id/metrics/api/v1/query"))
	g.Expect(modifiedRequest.URL.Query().Get("metrics_scope")).To(gomega.Equal(""))
	g.Expect(modifiedRequest.URL.Query().Get("query")).To(gomega.Equal("up"))
	g.Expect(modifiedRequest.Header.Get("X-Biz-Type")).To(gomega.Equal("byoc"))
	g.Expect(modifiedRequest.Header.Get("X-Org-Id")).To(gomega.Equal("2130533"))
	g.Expect(modifiedRequest.Header.Get("X-Region-Id")).To(gomega.Equal("aws-us-west-2"))
}

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

	_, targetHost, modifyFunc, _, err := buildProxyForMetricsTarget( //nolint:bodyclose
		context.Background(),
		&o11yurls.MetricsTarget{
			MetricsURL: "https://metrics.example.com/internal/v2/metrics/multitenant/api/v1/?" +
				"extra_filters%5B%5D=vm_account_id%3D%22123%22&" +
				"extra_filters%5B%5D=biz_type%3D%22tidbcloud-nextgen%22&tenant=fixed",
		},
		nil,
		routerTrimPrefix,
	)
	g.Expect(err).To(gomega.Succeed())
	g.Expect(targetHost).To(gomega.Equal("metrics.example.com"))

	r, _ := http.NewRequest(
		http.MethodGet,
		"http://localhost:8080/metric/query/api/v1/query_range?metrics_scope=child&query=up&start=1&end=2&step=60&tenant=attacker&extra_filters%5B%5D=attacker",
		nil,
	)
	r.URL.RawPath = "/metric/query/api/v1/query_range"
	modifiedRequest := modifyFunc(r)

	g.Expect(modifiedRequest.URL.Path).To(gomega.Equal("/internal/v2/metrics/multitenant/api/v1/query_range"))
	g.Expect(modifiedRequest.URL.Query().Get("metrics_scope")).To(gomega.Equal(""))
	g.Expect(modifiedRequest.URL.Query().Get("query")).To(gomega.Equal("up"))
	g.Expect(modifiedRequest.URL.Query().Get("start")).To(gomega.Equal("1"))
	g.Expect(modifiedRequest.URL.Query().Get("end")).To(gomega.Equal("2"))
	g.Expect(modifiedRequest.URL.Query().Get("step")).To(gomega.Equal("60"))
	g.Expect(modifiedRequest.URL.Query().Get("tenant")).To(gomega.Equal("fixed"))
	g.Expect(modifiedRequest.URL.Query()["extra_filters[]"]).To(gomega.Equal([]string{
		`vm_account_id="123"`,
		`biz_type="tidbcloud-nextgen"`,
	}))
}

func TestBuildProxyForMetricsTargetRejectsRelativeURL(t *testing.T) {
	targetScheme, targetHost, requestModifyFunc, responseModifyFunc, err := buildProxyForMetricsTarget( //nolint:bodyclose
		context.Background(),
		&o11yurls.MetricsTarget{MetricsURL: "/internal/v2/metrics/multitenant/api/v1/"},
		nil,
		routerTrimPrefix,
	)

	g := gomega.NewWithT(t)
	g.Expect(err).NotTo(gomega.Succeed())
	g.Expect(targetScheme).To(gomega.BeEmpty())
	g.Expect(targetHost).To(gomega.BeEmpty())
	g.Expect(requestModifyFunc).To(gomega.BeNil())
	g.Expect(responseModifyFunc).To(gomega.BeNil())
}

func TestHandleControlPlaneParentMetricsUsesO11yURLs(t *testing.T) {
	g := gomega.NewWithT(t)
	httpmock.Activate()
	defer httpmock.DeactivateAndReset()

	httpmock.RegisterResponder(http.MethodPost, "https://tidb-soc2.us.auth0.com/oauth/token", func(r *http.Request) (*http.Response, error) {
		return httpmock.NewStringResponse(http.StatusOK, `{"access_token":"xxx","scope":"read:client_grants","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()
			g.Expect(query.Get("bizType")).To(gomega.Equal("dev-devtier"))
			g.Expect(query.Get("tenantID")).To(gomega.Equal("dev-devtier"))
			g.Expect(query.Get("vendor")).To(gomega.Equal("aws"))
			g.Expect(query.Get("region")).To(gomega.Equal("us-east-1"))
			g.Expect(query.Get("projectID")).To(gomega.Equal("us-east-1-f01"))
			g.Expect(query.Get("applicationID")).To(gomega.Equal("devtier-infra"))
			g.Expect(query.Get("showDeleted")).To(gomega.Equal("true"))

			content := `{"code":0,"data":{"clusters":[{"applicationID":"devtier-infra","bizType":"dev-devtier","id":"control-plane-o11y-id","projectID":"us-east-1-f01","region":"us-east-1","tenantID":"dev-devtier","vendor":"aws","version":"1","internalReadUri":"https://www.ds.us-east-1.aws.observability-staging.pingcap.cloud/internal/metrics/control-plane-o11y-id/api/v1/"}],"continueToken":"","remainingItemCount":0},"message":""}`
			return httpmock.NewStringResponse(http.StatusOK, content), nil
		})

	cli, err := o11y.New(
		"www.gs.us-west-2.aws.observability-staging.pingcap.cloud",
		"www.ds.%s.%s.observability-staging.pingcap.cloud",
		"foo",
		"bar",
		"dev")
	g.Expect(err).To(gomega.Succeed())
	handler := NewTiDBClusterMetricHandler(nil, nil, "dev")
	handler.SetO11y(cli)

	//nolint:bodyclose
	_, targetHost, modifyFunc, _, err := handler.handleControlPlaneParentMetrics(
		context.Background(),
		"us-east-1-f01/aws-us-east-1/devtier-infra",
		routerTrimPrefix,
	)
	g.Expect(err).To(gomega.Succeed())
	g.Expect(targetHost).To(gomega.Equal("www.ds.us-east-1.aws.observability-staging.pingcap.cloud"))

	r, _ := http.NewRequest(http.MethodGet, "http://localhost:8080/metric/query/api/v1/query_range?metrics_scope=parent&query=up", nil)
	r.URL.RawPath = "/metric/query/api/v1/query_range"
	modifiedRequest := modifyFunc(r)

	g.Expect(modifiedRequest.URL.Path).To(gomega.Equal("/internal/metrics/control-plane-o11y-id/api/v1/query_range"))
	g.Expect(modifiedRequest.URL.Query().Get("metrics_scope")).To(gomega.Equal(""))
	g.Expect(modifiedRequest.URL.Query().Get("query")).To(gomega.Equal("up"))
}
