package diagnosis

import (
	"bytes"
	"context"
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	"testing"

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

	"github.com/pingcap-inc/clinic/pkg/config"
)

func TestRegisterDiagnosisAPI(t *testing.T) {
	g := gomega.NewWithT(t)
	metricConfig := config.MetricSingleEndpointConfig{
		VMClusterAddressConfig: config.VMClusterAddressConfig{
			AddressConfig: config.AddressConfig{Host: "dev-mock.com", Port: 8080},
			VMClusterMode: false,
		},
		SelectAddr: config.VMClusterAddressConfig{
			AddressConfig: config.AddressConfig{Host: "dev-mock.com", Port: 8081},
			VMClusterMode: true,
		},
	}
	logConfig := config.LogConfig{
		Endpoint: "http://localhost:8082",
	}
	gin.SetMode(gin.TestMode)
	engin := gin.Default()
	api, err := NewDiagnosisAPI(&metricConfig, &logConfig)
	g.Expect(err).To(gomega.Succeed())
	RegisterRouter(&engin.RouterGroup, api)
	routersInfo := engin.Routes()
	pathLookup := make(map[string]string)
	for _, info := range routersInfo {
		pathLookup[info.Path] = info.Method
	}
	g.Expect(pathLookup["/diagnosis/annotations"]).To(gomega.Equal(http.MethodGet))
	g.Expect(pathLookup["/diagnosis/node_graph"]).To(gomega.Equal(http.MethodGet))
	g.Expect(pathLookup["/diagnosis/metrics_sample"]).To(gomega.Equal(http.MethodPost))
	g.Expect(pathLookup["/diagnosis/log_sample/:index/_doc"]).To(gomega.Equal(http.MethodPost))
	g.Expect(pathLookup["/diagnosis/log_sample/:index/_doc/:doc_id"]).To(gomega.Equal(http.MethodPut))
	g.Expect(pathLookup["/diagnosis/log_sample/:index/_create/:doc_id"]).To(gomega.Or(gomega.Equal(http.MethodPost), gomega.Equal(http.MethodPut)))
	g.Expect(pathLookup["/diagnosis/log_sample/_bulk"]).To(gomega.Or(gomega.Equal(http.MethodPost), gomega.Equal(http.MethodPut)))
	g.Expect(pathLookup["/diagnosis/log_sample/:index/_bulk"]).To(gomega.Or(gomega.Equal(http.MethodPost), gomega.Equal(http.MethodPut)))
}

// func TestInsertSampleAPI(t *testing.T) {
// 	g := gomega.NewWithT(t)
// 	gin.SetMode(gin.TestMode)
// 	httpmock.Activate()
// 	defer httpmock.DeactivateAndReset()

// 	metricConfig := config.MetricSingleEndpointConfig{
// 		VMClusterAddressConfig: config.VMClusterAddressConfig{
// 			AddressConfig: config.AddressConfig{Host: "dev-mock.com", Port: 8080},
// 			VMClusterMode: false,
// 		},
// 		SelectAddr: config.VMClusterAddressConfig{
// 			AddressConfig: config.AddressConfig{Host: "dev-mock.com", Port: 8081},
// 			VMClusterMode: true,
// 		},
// 	}
// 	logConfig := config.LogConfig{
// 		Endpoint: "http://localhost:8082",
// 	}
// 	insertSampleHandlerFunc := func(r *http.Request) (*http.Response, error) {
// 		if r.URL.Scheme != "http" {
// 			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()
// 		// clinic_test_data,tidb_cluster=6865911482951002112,title=helloworld seconds=10 3414585025849906176
// 		if string(b) != "clinic_test_data,tidb_cluster=6865911482951002112,title=helloworld seconds=10 1645181856000000000\n" {
// 			return httpmock.NewJsonResponse(http.StatusBadRequest, struct{}{})
// 		}
// 		return httpmock.NewJsonResponse(http.StatusNoContent, struct{}{})
// 	}
// 	notFoundHandlerFunc := func(r *http.Request) (*http.Response, error) {
// 		return httpmock.NewJsonResponse(http.StatusNotFound, struct{}{})
// 	}

// 	httpmock.RegisterResponder(http.MethodPost, "http://dev-mock.com:8080/influx/api/v2/write", insertSampleHandlerFunc)
// 	httpmock.RegisterNoResponder(notFoundHandlerFunc)
// 	api, err := NewDiagnosisAPI(&metricConfig, &logConfig)
// 	g.Expect(err).To(gomega.Succeed())
// 	_, err = api.InsertMetricsSample(context.Background(), &InsertMetricsSampleParam{
// 		Timestamp:   1645181856,
// 		Measurement: "clinic_test_data",
// 		TiDBCluster: "6865911482951002112",
// 		Fields: map[string]any{
// 			"seconds": float64(10),
// 		},
// 		Tags: map[string]string{
// 			"title": "helloworld",
// 		},
// 	})
// 	g.Expect(err).To(gomega.Succeed())
// }

func TestQueryAnnotationsAPI(t *testing.T) {
	g := gomega.NewWithT(t)
	gin.SetMode(gin.TestMode)
	httpmock.Activate()
	defer httpmock.DeactivateAndReset()

	metricConfig := config.MetricSingleEndpointConfig{
		VMClusterAddressConfig: config.VMClusterAddressConfig{
			AddressConfig: config.AddressConfig{Host: "dev-mock.com", Port: 8080},
			VMClusterMode: false,
		},
		SelectAddr: config.VMClusterAddressConfig{
			AddressConfig: config.AddressConfig{Host: "dev-mock.com", Port: 8081},
			VMClusterMode: true,
		},
	}
	logConfig := config.LogConfig{
		Endpoint: "http://localhost:8082",
	}
	queryAnnotationsHandlerFunc := func(r *http.Request) (*http.Response, error) {
		if r.URL.Scheme != "http" {
			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()
		q, _ := url.QueryUnescape(string(b))
		if q != `query={__name__=~"clinic_test_data.*",tidb_cluster="6865911482951002112"}[1000s]&time=1645181856` {
			return httpmock.NewJsonResponse(http.StatusBadRequest, struct{}{})
		}
		resp := httpmock.NewBytesResponse(http.StatusOK, []byte(`{"status":"success","data":{"resultType":"matrix","result":[{
"metric":{"__name__":"clinic_test_data","panel_id":"9566","tags":"anomaly","text":"counts","tidb_cluster_id":"6865911482951002112"},
"values":[[1645180866,"10"],[1645180886,"20"]]
}]}}`))
		resp.Header.Set("Content-Type", "application/json")
		return resp, nil
	}
	notFoundHandlerFunc := func(r *http.Request) (*http.Response, error) {
		return httpmock.NewJsonResponse(http.StatusNotFound, struct{}{})
	}

	httpmock.RegisterResponder(http.MethodPost, "http://dev-mock.com:8081/select/1/prometheus/api/v1/query", queryAnnotationsHandlerFunc)
	httpmock.RegisterNoResponder(notFoundHandlerFunc)
	api, err := NewDiagnosisAPI(&metricConfig, &logConfig)
	g.Expect(err).To(gomega.Succeed())
	data, err := api.QueryAnnotations(context.Background(), &QueryAnnotationsParam{
		TsRange: TsRange{
			StartTS: 1645180856,
			EndTS:   1645181856,
		},
		Measurement: "clinic_test_data",
		TiDBCluster: "6865911482951002112",
	})
	g.Expect(err).To(gomega.Succeed())
	// make sure we parse the json correctly
	g.Expect(len(data)).To(gomega.Equal(2))
	g.Expect(data[0].Time).To(gomega.Equal(int64(1645180866000)))
	// annotation use milliseconds
	g.Expect(data[0].TimeEnd - data[0].Time).To(gomega.Equal(int64(10000)))
	g.Expect(data[0].PanelID).To(gomega.Equal(int64(9566)))
	g.Expect(data[1].Time).To(gomega.Equal(int64(1645180886000)))
	g.Expect(data[1].TimeEnd - data[1].Time).To(gomega.Equal(int64(20000)))
	g.Expect(data[1].PanelID).To(gomega.Equal(int64(9566)))
}

func TestQueryNodeGraphAPI(t *testing.T) {
	g := gomega.NewWithT(t)
	gin.SetMode(gin.TestMode)
	httpmock.Activate()
	defer httpmock.DeactivateAndReset()

	metricConfig := config.MetricSingleEndpointConfig{
		VMClusterAddressConfig: config.VMClusterAddressConfig{
			AddressConfig: config.AddressConfig{Host: "dev-mock.com", Port: 8080},
			VMClusterMode: false,
		},
		SelectAddr: config.VMClusterAddressConfig{
			AddressConfig: config.AddressConfig{Host: "dev-mock.com", Port: 8081},
			VMClusterMode: true,
		},
	}
	logConfig := config.LogConfig{
		Endpoint: "http://localhost:8082",
	}
	queryNodeGraphHandlerFunc := func(r *http.Request) (*http.Response, error) {
		if r.URL.Scheme != "http" {
			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()
		q, _ := url.QueryUnescape(string(b))
		if q != `query=first_over_time({__name__=~"clinic_test_data.*",tidb_cluster="6865911482951002112"}[1000s])&time=1645181856` {
			return httpmock.NewJsonResponse(http.StatusBadRequest, struct{}{})
		}
		resp := httpmock.NewBytesResponse(http.StatusOK, []byte(`{"status":"success","data":{"resultType":"vector","result":[]}}`))
		resp.Header.Set("Content-Type", "application/json")
		return resp, nil
	}
	notFoundHandlerFunc := func(r *http.Request) (*http.Response, error) {
		return httpmock.NewJsonResponse(http.StatusNotFound, struct{}{})
	}

	httpmock.RegisterResponder(http.MethodPost, "http://dev-mock.com:8081/select/1/prometheus/api/v1/query", queryNodeGraphHandlerFunc)
	httpmock.RegisterNoResponder(notFoundHandlerFunc)
	api, err := NewDiagnosisAPI(&metricConfig, &logConfig)
	g.Expect(err).To(gomega.Succeed())
	_, err = api.QueryNodeGraph(context.Background(), &QueryNodeGraphParam{
		TsRange: TsRange{
			StartTS: 1645180856,
			EndTS:   1645181856,
		},
		Measurement: "clinic_test_data",
		TiDBCluster: "6865911482951002112",
	})
	g.Expect(err).To(gomega.Succeed())
}

func TestQueryDynamicTextValueAPI(t *testing.T) {
	g := gomega.NewWithT(t)
	gin.SetMode(gin.TestMode)
	httpmock.Activate()
	defer httpmock.DeactivateAndReset()

	metricConfig := config.MetricSingleEndpointConfig{
		VMClusterAddressConfig: config.VMClusterAddressConfig{
			AddressConfig: config.AddressConfig{Host: "dev-mock.com", Port: 8080},
			VMClusterMode: false,
		},
		SelectAddr: config.VMClusterAddressConfig{
			AddressConfig: config.AddressConfig{Host: "dev-mock.com", Port: 8081},
			VMClusterMode: true,
		},
	}
	logConfig := config.LogConfig{
		Endpoint: "http://localhost:8082",
	}
	queryDynamicTextValueHandlerFunc := func(r *http.Request) (*http.Response, error) {
		if r.URL.Scheme != "http" {
			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()
		q, _ := url.QueryUnescape(string(b))
		if q != `query={__name__=~"diagnosis_summary_v1.*",tidb_cluster="6862533764151471104"}[7200s]&time=1641335550` {
			t.Errorf("query: %s", q)
			return httpmock.NewJsonResponse(http.StatusBadRequest, struct{}{})
		}
		resp := httpmock.NewBytesResponse(http.StatusOK, []byte(`{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"__name__":"diagnosis_summary_v1_time","type":"duration_seconds","tidb_cluster":"6862533764151471104"},"values":[[1641335542,"1260"]]}]}}`))
		resp.Header.Set("Content-Type", "application/json")
		return resp, nil
	}
	notFoundHandlerFunc := func(r *http.Request) (*http.Response, error) {
		return httpmock.NewStringResponse(http.StatusNotFound, r.URL.String()), nil
	}

	httpmock.RegisterResponder(http.MethodPost,
		"http://dev-mock.com:8081/select/1/prometheus/api/v1/query",
		queryDynamicTextValueHandlerFunc)
	httpmock.RegisterNoResponder(notFoundHandlerFunc)
	api, err := NewDiagnosisAPI(&metricConfig, &logConfig)
	g.Expect(err).To(gomega.Succeed())
	data, err := api.QueryDynamicTextValue(context.Background(), &QueryDynamicTextValueParam{
		TsRange: TsRange{
			StartTS: 1641328350,
			EndTS:   1641335550,
		},
		Measurement: "diagnosis_summary_v1",
		TiDBCluster: "6862533764151471104",
		Default1:    "total_latency_good",
	})
	g.Expect(err).To(gomega.Succeed())
	g.Expect(data["time_1"]).To(gomega.Equal(int64(1)))
	g.Expect(data["total_latency_good"]).To(gomega.Equal(int64(1)))
	g.Expect(data["time_1_start_unix"]).To(gomega.Equal(int64(1641335542)))
	g.Expect(data["time_1_end_unix"]).To(gomega.Equal(int64(1641335542 + 1260)))
}

func TestInsertLogSampleGinHandlerFunc(t *testing.T) {
	g := gomega.NewWithT(t)
	gin.SetMode(gin.TestMode)
	httpmock.Activate()
	defer httpmock.DeactivateAndReset()
	insertLogSamplePath := "/clinic/api/v1/diagnosis/log_sample/6865911482951002112/_doc"
	insertDocBody := `{"@timestamp":"2099-11-15T13:12:00","user":{"id":"kimchy"}}`
	metricsCfg := config.MetricSingleEndpointConfig{
		VMClusterAddressConfig: config.VMClusterAddressConfig{
			AddressConfig: config.AddressConfig{Host: "dev-mock.com", Port: 8080},
			VMClusterMode: false,
		},
	}
	logConfig := config.LogConfig{
		Endpoint: "https://opensearch-mock.com",
		User:     "foo",
		Password: "bar",
	}
	logHandlerFunc := func(r *http.Request) (*http.Response, error) {
		if r.URL.Scheme != "https" {
			t.Log("scheme incorrect")
			return httpmock.NewJsonResponse(http.StatusBadRequest, struct{}{})
		}
		if r.URL.Path != "/6865911482951002112/_doc" {
			t.Logf("path incorrect %s", r.URL.Path)
			return httpmock.NewJsonResponse(http.StatusBadRequest, struct{}{})
		}
		user, pwd, ok := r.BasicAuth()
		if !ok {
			t.Log("auth is not ok")
			return httpmock.NewJsonResponse(http.StatusBadRequest, struct{}{})
		}
		if user != "foo" || pwd != "bar" {
			t.Log("auth failed")
			return httpmock.NewJsonResponse(http.StatusBadRequest, struct{}{})
		}
		b, err := io.ReadAll(r.Body)
		if err != nil {
			t.Log("read body failed")
			return httpmock.NewJsonResponse(http.StatusBadRequest, struct{}{})
		}
		defer r.Body.Close()
		if string(b) != insertDocBody {
			t.Log("body is not equal")
			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/_doc", logHandlerFunc)
		httpmock.RegisterNoResponder(notFoundHandlerFunc)
		api, err := NewDiagnosisAPI(&metricsCfg, &logConfig)
		g.Expect(err).To(gomega.Succeed())
		api.logProxy.WithBasePath("/clinic/api/v1")
		resp := createTestResponseRecorder()
		c, _ := gin.CreateTestContext(resp)
		buf := bytes.NewBufferString(insertDocBody)
		c.Request = httptest.NewRequest(http.MethodPost, insertLogSamplePath, buf)
		InsertLogSampleGinHandlerFunc(api)(c)
		g.Expect(resp.Result().StatusCode).To(gomega.Equal(http.StatusOK)) //nolint:bodyclose
		httpmock.Reset()
	}
}

func TestInsertLogSampleGinHandlerFuncWithNDJson(t *testing.T) {
	g := gomega.NewWithT(t)
	gin.SetMode(gin.TestMode)
	httpmock.Activate()
	defer httpmock.DeactivateAndReset()
	insertLogSamplePath := "/clinic/api/v1/diagnosis/log_sample/6865911482951002112/_bulk"
	insertDocBody := `{"@timestamp":"2099-11-15T13:12:00","user":{"id":"kimchy"}}`
	metricsCfg := config.MetricSingleEndpointConfig{
		VMClusterAddressConfig: config.VMClusterAddressConfig{
			AddressConfig: config.AddressConfig{Host: "dev-mock.com", Port: 8080},
			VMClusterMode: false,
		},
	}
	logConfig := config.LogConfig{
		Endpoint: "https://opensearch-mock.com",
		User:     "foo",
		Password: "bar",
	}
	logHandlerFunc := func(r *http.Request) (*http.Response, error) {
		if r.URL.Scheme != "https" {
			t.Log("scheme incorrect")
			return httpmock.NewJsonResponse(http.StatusBadRequest, struct{}{})
		}
		if r.URL.Path != "/6865911482951002112/_bulk" {
			t.Logf("path incorrect %s", r.URL.Path)
			return httpmock.NewJsonResponse(http.StatusBadRequest, struct{}{})
		}

		if r.Header.Get("Content-Type") != "application/x-ndjson" {
			return httpmock.NewJsonResponse(http.StatusNotAcceptable, struct{}{})
		}
		user, pwd, ok := r.BasicAuth()
		if !ok {
			t.Log("auth is not ok")
			return httpmock.NewJsonResponse(http.StatusBadRequest, struct{}{})
		}
		if user != "foo" || pwd != "bar" {
			t.Log("auth failed")
			return httpmock.NewJsonResponse(http.StatusBadRequest, struct{}{})
		}
		b, err := io.ReadAll(r.Body)
		if err != nil {
			t.Log("read body failed")
			return httpmock.NewJsonResponse(http.StatusBadRequest, struct{}{})
		}
		defer r.Body.Close()
		if string(b) != insertDocBody {
			t.Log("body is not equal")
			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/_bulk", logHandlerFunc)
		httpmock.RegisterNoResponder(notFoundHandlerFunc)
		api, err := NewDiagnosisAPI(&metricsCfg, &logConfig)
		g.Expect(err).To(gomega.Succeed())
		api.logProxy.WithBasePath("/clinic/api/v1")
		resp := createTestResponseRecorder()
		c, _ := gin.CreateTestContext(resp)
		buf := bytes.NewBufferString(insertDocBody)
		c.Request = httptest.NewRequest(http.MethodPost, insertLogSamplePath, buf)
		c.Request.Header.Set("Content-Type", "application/vnd.elasticsearch+x-ndjson;compatible-with=8")
		InsertLogSampleGinHandlerFunc(api)(c)
		g.Expect(resp.Result().StatusCode).To(gomega.Equal(http.StatusOK)) //nolint:bodyclose
		httpmock.Reset()
	}
}

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