package datasource

import (
	"bytes"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"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/proxy"
)

func TestGetLogGinHandlerFunc(t *testing.T) {
	g := gomega.NewWithT(t)
	gin.SetMode(gin.TestMode)
	httpmock.Activate()
	defer httpmock.DeactivateAndReset()
	getLogPath := "/clinic/api/v1/data/log/6865911482951002112/_search"
	searchBody := `{"query": {"term": {"user.id": "kimchy"}}}`
	cfg := &config.DatasourceConfig{
		MetricConfig: config.MetricConfig{
			VMClusterAddressConfig: config.VMClusterAddressConfig{
				AddressConfig: config.AddressConfig{Host: "dev-mock.com", Port: 8080},
				VMClusterMode: false,
			},
		},
		LogConfig: config.DatasourceLogConfig{
			OpAddress: config.LogConfig{
				Endpoint: "https://opensearch-mock.com",
				User:     "foo",
				Password: "bar",
			},
			CloudAddress: config.LogConfig{
				Endpoint: "https://opensearch-mock.com",
				User:     "foo",
				Password: "bar",
			},
		},
	}
	logHandlerFunc := func(r *http.Request) (*http.Response, error) {
		if r.URL.Scheme != "https" {
			return httpmock.NewJsonResponse(http.StatusBadRequest, struct{}{})
		}
		if r.URL.Path != "/6865911482951002112/_search" {
			return httpmock.NewJsonResponse(http.StatusBadRequest, struct{}{})
		}
		if r.Header.Get("Content-Type") != "application/json" {
			return httpmock.NewJsonResponse(http.StatusNotAcceptable, struct{}{})
		}
		user, pwd, ok := r.BasicAuth()
		if !ok {
			return httpmock.NewJsonResponse(http.StatusBadRequest, struct{}{})
		}
		if user != "foo" || pwd != "bar" {
			return httpmock.NewJsonResponse(http.StatusBadRequest, struct{}{})
		}
		b, err := io.ReadAll(r.Body)
		if err != nil {
			return httpmock.NewJsonResponse(http.StatusBadRequest, struct{}{})
		}
		defer r.Body.Close()
		if string(b) != searchBody {
			return httpmock.NewJsonResponse(http.StatusBadRequest, struct{}{})
		}
		return httpmock.NewJsonResponse(http.StatusOK, struct{}{})
	}
	notFoundHandlerFunc := func(r *http.Request) (*http.Response, error) {
		return httpmock.NewJsonResponse(http.StatusNotFound, struct{}{})
	}
	{
		httpmock.RegisterResponder(http.MethodPost, "https://opensearch-mock.com/6865911482951002112/_search", logHandlerFunc)
		httpmock.RegisterNoResponder(notFoundHandlerFunc)
		api, err := NewComputeEngineDataAPI(&cfg.MetricConfig, &cfg.LogConfig, &proxy.TiDBClusterMetricHandler{}, &proxy.O11yProxyHandler{})
		g.Expect(err).To(gomega.Succeed())
		api.logProxy.WithPathPrefixTrim("/clinic/api/v1")
		resp := createTestResponseRecorder()
		c, _ := gin.CreateTestContext(resp)
		buf := bytes.NewBufferString(searchBody)
		c.Request = httptest.NewRequest(http.MethodPost, getLogPath, buf)
		c.Request.Header.Set("Content-Type", "application/vnd.elasticsearch+json;compatible-with=8")
		api.ForwardLogRequest(c)
		g.Expect(resp.Result().StatusCode).To(gomega.Equal(http.StatusOK)) //nolint:bodyclose
		g.Expect(resp.Header().Get("x-elastic-product")).To(gomega.Equal("Elasticsearch"))
		httpmock.Reset()
	}
}

func TestGetMetricsGinHandlerFunc(t *testing.T) {
	// http://localhost:8081/data/metrics?query=up{}&start=1640086500&end=1640097000&step=15s
	g := gomega.NewWithT(t)
	gin.SetMode(gin.TestMode)
	httpmock.Activate()
	defer httpmock.DeactivateAndReset()

	getMetricsPath := "/clinic/api/v1/data/metrics?query=up{}&start=1640086500&end=1640097000&step=15s"
	cfg := &config.DatasourceConfig{
		MetricConfig: config.MetricConfig{
			VMClusterAddressConfig: config.VMClusterAddressConfig{
				AddressConfig: config.AddressConfig{Host: "dev-mock.com", Port: 8080},
				VMClusterMode: false,
			},
			SelectAddr: config.MetricSelectAddressConfig{},
		},
		LogConfig: config.DatasourceLogConfig{
			OpAddress: config.LogConfig{
				Endpoint: "https://opensearch-mock.com",
				User:     "foo",
				Password: "bar",
			},
			CloudAddress: config.LogConfig{
				Endpoint: "https://opensearch-mock.com",
				User:     "foo",
				Password: "bar",
			},
		},
	}

	metricsHandlerFunc := func(r *http.Request) (*http.Response, error) {
		query := r.URL.Query()
		if query.Get("query") != "up{}" {
			return httpmock.NewJsonResponse(http.StatusBadRequest, struct{}{})
		}
		if query.Get("start") != "1640086500" {
			return httpmock.NewJsonResponse(http.StatusBadRequest, struct{}{})
		}
		if query.Get("end") != "1640097000" {
			return httpmock.NewJsonResponse(http.StatusBadRequest, struct{}{})
		}
		if query.Get("step") != "15s" {
			return httpmock.NewJsonResponse(http.StatusBadRequest, struct{}{})
		}
		return httpmock.NewJsonResponse(http.StatusOK, struct{}{})
	}
	notFoundHandlerFunc := func(r *http.Request) (*http.Response, error) {
		return httpmock.NewJsonResponse(http.StatusNotFound, struct{}{})
	}
	{
		// froward to vm-select
		cfg.MetricConfig.SelectAddr.OpAddress.VMClusterMode = true
		cfg.MetricConfig.SelectAddr.OpAddress.Host = "vm-select.dev-mock.com"
		cfg.MetricConfig.SelectAddr.OpAddress.Port = 8081
		httpmock.RegisterResponder(http.MethodGet, fmt.Sprintf("http://vm-select.dev-mock.com:8081/select/42/prometheus%s", queryRangePath), metricsHandlerFunc)
		httpmock.RegisterNoResponder(notFoundHandlerFunc)
		api, err := NewComputeEngineDataAPI(&cfg.MetricConfig, &cfg.LogConfig, &proxy.TiDBClusterMetricHandler{}, &proxy.O11yProxyHandler{})
		g.Expect(err).To(gomega.Succeed())
		resp := createTestResponseRecorder()
		c, _ := gin.CreateTestContext(resp)
		c.Request = httptest.NewRequest(http.MethodGet, getMetricsPath, nil)
		c.Request.Header.Set("X-OrgID", "42")
		api.ForwardMetricsQueryRangeRequest(c)
		g.Expect(resp.Result().StatusCode).To(gomega.Equal(http.StatusOK)) //nolint:bodyclose
		httpmock.Reset()
	}
	{
		// froward to thanos
		cfg.MetricConfig.SelectAddr.OpAddress.VMClusterMode = false
		cfg.MetricConfig.SelectAddr.OpAddress.Host = "thanos.dev-mock.com"
		cfg.MetricConfig.SelectAddr.OpAddress.Port = 8082
		httpmock.RegisterResponder(http.MethodGet, fmt.Sprintf("http://thanos.dev-mock.com:8082%s", queryRangePath), metricsHandlerFunc)
		httpmock.RegisterNoResponder(notFoundHandlerFunc)
		api, err := NewComputeEngineDataAPI(&cfg.MetricConfig, &cfg.LogConfig, &proxy.TiDBClusterMetricHandler{}, &proxy.O11yProxyHandler{})
		g.Expect(err).To(gomega.Succeed())
		resp := createTestResponseRecorder()
		c, _ := gin.CreateTestContext(resp)
		c.Request = httptest.NewRequest(http.MethodGet, getMetricsPath, nil)
		api.ForwardMetricsQueryRangeRequest(c)
		g.Expect(resp.Result().StatusCode).To(gomega.Equal(http.StatusOK)) //nolint:bodyclose
		httpmock.Reset()
	}
}

func TestRegisterComputerEngineDataAPI(t *testing.T) {
	g := gomega.NewWithT(t)
	cfg := &config.DatasourceConfig{
		MetricConfig: config.MetricConfig{
			VMClusterAddressConfig: config.VMClusterAddressConfig{
				AddressConfig: config.AddressConfig{Host: "dev-mock.com", Port: 8080},
				VMClusterMode: false,
			},
		},
		LogConfig: config.DatasourceLogConfig{
			OpAddress: config.LogConfig{
				Endpoint: "https://opensearch-mock.com",
				User:     "foo",
				Password: "bar",
			},
			CloudAddress: config.LogConfig{
				Endpoint: "https://opensearch-mock.com",
				User:     "foo",
				Password: "bar",
			},
		},
	}
	gin.SetMode(gin.TestMode)
	engine := gin.Default()
	api, err := NewComputeEngineDataAPI(&cfg.MetricConfig, &cfg.LogConfig, &proxy.TiDBClusterMetricHandler{}, &proxy.O11yProxyHandler{})
	g.Expect(err).To(gomega.Succeed())
	RegisterComputeEngineDataRouter(&engine.RouterGroup, api, nil)
	routersInfo := engine.Routes()
	pathLookup := make(map[string]bool)
	for _, info := range routersInfo {
		pathLookup[fmt.Sprintf("%s|%s", info.Path, info.Method)] = true
	}
	t.Log(pathLookup)
	g.Expect(pathLookup["/data/metrics|GET"]).To(gomega.BeTrue())
	g.Expect(pathLookup["/data/log/:index/_search|GET"]).To(gomega.BeTrue())
	g.Expect(pathLookup["/data/log/:index/_search|POST"]).To(gomega.BeTrue())
	g.Expect(pathLookup["/data/prometheus/*any|GET"]).To(gomega.BeTrue())
	g.Expect(pathLookup["/data/prometheus/*any|POST"]).To(gomega.BeTrue())
}

// 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),
	}
}
