package httpquery

import (
	"context"
	"encoding/json"
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	"slices"
	"strings"
	"testing"
	"time"

	"github.com/prometheus/prometheus/model/labels"

	metricsstore "github.com/tidbcloud/cluster-service-ng/internal/capacity-planner/pkg/prom/store"
)

func TestQueryAPIExecutesFullPromQL(t *testing.T) {
	server, evaluationTime := newTestServer(t)

	form := url.Values{
		"query": {`clamp_max(sum(cpu), 4)`},
		"time":  {evaluationTime.Format(time.RFC3339Nano)},
	}
	request := httptest.NewRequest(http.MethodPost, "/api/v1/query", strings.NewReader(form.Encode()))
	request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	responseRecorder := httptest.NewRecorder()
	server.Handler().ServeHTTP(responseRecorder, request)

	response := decodeResponse(t, responseRecorder)
	if responseRecorder.Code != http.StatusOK || response.Status != "success" {
		t.Fatalf("POST /api/v1/query status = (%d, %q), body = %s", responseRecorder.Code, response.Status, responseRecorder.Body.String())
	}
	var data struct {
		ResultType string `json:"resultType"`
		Result     []struct {
			Metric map[string]string `json:"metric"`
			Value  []json.RawMessage `json:"value"`
		} `json:"result"`
	}
	decodeData(t, response.Data, &data)
	if data.ResultType != "vector" || len(data.Result) != 1 {
		t.Fatalf("query result = %+v, want one vector sample", data)
	}
	if len(data.Result[0].Value) != 2 || string(data.Result[0].Value[1]) != `"4"` {
		t.Fatalf("query value = %s, want 4", data.Result[0].Value)
	}
}

func TestQueryRange(t *testing.T) {
	server, evaluationTime := newTestServer(t)
	query := url.Values{
		"query": {`sum by (namespace) (cpu)`},
		"start": {evaluationTime.Format(time.RFC3339Nano)},
		"end":   {evaluationTime.Add(30 * time.Second).Format(time.RFC3339Nano)},
		"step":  {"15s"},
	}
	request := httptest.NewRequest(http.MethodGet, "/api/v1/query_range?"+query.Encode(), nil)
	responseRecorder := httptest.NewRecorder()
	server.Handler().ServeHTTP(responseRecorder, request)

	response := decodeResponse(t, responseRecorder)
	if responseRecorder.Code != http.StatusOK || response.Status != "success" {
		t.Fatalf("GET /api/v1/query_range status = (%d, %q), body = %s", responseRecorder.Code, response.Status, responseRecorder.Body.String())
	}
	var data struct {
		ResultType string `json:"resultType"`
		Result     []struct {
			Metric map[string]string   `json:"metric"`
			Values [][]json.RawMessage `json:"values"`
		} `json:"result"`
	}
	decodeData(t, response.Data, &data)
	if data.ResultType != "matrix" || len(data.Result) != 1 || len(data.Result[0].Values) != 3 {
		t.Fatalf("range result = %+v, want one series with three points", data)
	}
	if data.Result[0].Metric["namespace"] != "test" {
		t.Fatalf("range labels = %v, want namespace=test", data.Result[0].Metric)
	}
}

func TestDiscoveryAPIs(t *testing.T) {
	server, _ := newTestServer(t)

	tests := []struct {
		path string
		want []string
	}{
		{
			path: "/api/v1/labels",
			want: []string{"__name__", "namespace", "pod"},
		},
		{
			path: "/api/v1/label/pod/values",
			want: []string{"tidb-0", "tidb-1"},
		},
	}
	for _, test := range tests {
		t.Run(test.path, func(t *testing.T) {
			responseRecorder := httptest.NewRecorder()
			server.Handler().ServeHTTP(responseRecorder, httptest.NewRequest(http.MethodGet, test.path, nil))
			response := decodeResponse(t, responseRecorder)
			if responseRecorder.Code != http.StatusOK || response.Status != "success" {
				t.Fatalf("GET %s status = (%d, %q), body = %s", test.path, responseRecorder.Code, response.Status, responseRecorder.Body.String())
			}
			var got []string
			decodeData(t, response.Data, &got)
			if !slices.Equal(got, test.want) {
				t.Fatalf("GET %s data = %v, want %v", test.path, got, test.want)
			}
		})
	}

	seriesPath := "/api/v1/series?" + url.Values{"match[]": {`cpu{namespace="test"}`}}.Encode()
	responseRecorder := httptest.NewRecorder()
	server.Handler().ServeHTTP(responseRecorder, httptest.NewRequest(http.MethodGet, seriesPath, nil))
	response := decodeResponse(t, responseRecorder)
	var series []map[string]string
	decodeData(t, response.Data, &series)
	if responseRecorder.Code != http.StatusOK || len(series) != 2 {
		t.Fatalf("GET /api/v1/series status = %d, data = %v, body = %s", responseRecorder.Code, series, responseRecorder.Body.String())
	}
}

func TestOnlyReadAPIsAreExposed(t *testing.T) {
	server, _ := newTestServer(t)
	for _, test := range []struct {
		method string
		path   string
		want   int
	}{
		{method: http.MethodPost, path: "/api/v1/write", want: http.StatusNotFound},
		{method: http.MethodPost, path: "/api/v1/admin/tsdb/snapshot", want: http.StatusNotFound},
		{method: http.MethodPut, path: "/api/v1/query", want: http.StatusMethodNotAllowed},
	} {
		t.Run(test.method+" "+test.path, func(t *testing.T) {
			responseRecorder := httptest.NewRecorder()
			server.Handler().ServeHTTP(responseRecorder, httptest.NewRequest(test.method, test.path, nil))
			if responseRecorder.Code != test.want {
				t.Fatalf("%s %s status = %d, want %d", test.method, test.path, responseRecorder.Code, test.want)
			}
		})
	}
}

func TestInvalidQueryReturnsPrometheusError(t *testing.T) {
	server, _ := newTestServer(t)
	responseRecorder := httptest.NewRecorder()
	server.Handler().ServeHTTP(responseRecorder, httptest.NewRequest(http.MethodGet, "/api/v1/query?query=sum%28", nil))
	response := decodeResponse(t, responseRecorder)
	if responseRecorder.Code != http.StatusBadRequest || response.Status != "error" || response.ErrorType != "bad_data" {
		t.Fatalf("invalid query response = (%d, %+v), body = %s", responseRecorder.Code, response, responseRecorder.Body.String())
	}
}

func TestNewValidatesConfig(t *testing.T) {
	if _, err := New(nil); err == nil {
		t.Fatal("New(nil) succeeded, want error")
	}
	if _, err := New(&Config{Address: "127.0.0.1:9091"}); err == nil {
		t.Fatal("New() without Queryable succeeded, want error")
	}
}

type decodedAPIResponse struct {
	Status    string          `json:"status"`
	Data      json.RawMessage `json:"data"`
	ErrorType string          `json:"errorType"`
	Error     string          `json:"error"`
}

func decodeResponse(t *testing.T, recorder *httptest.ResponseRecorder) decodedAPIResponse {
	t.Helper()
	body, err := io.ReadAll(recorder.Result().Body)
	if err != nil {
		t.Fatalf("read response: %v", err)
	}
	var response decodedAPIResponse
	if err := json.Unmarshal(body, &response); err != nil {
		t.Fatalf("decode response %q: %v", body, err)
	}
	return response
}

func decodeData(t *testing.T, input json.RawMessage, output any) {
	t.Helper()
	if err := json.Unmarshal(input, output); err != nil {
		t.Fatalf("decode data %s: %v", input, err)
	}
}

func newTestServer(t *testing.T) (*Server, time.Time) {
	t.Helper()
	db, err := metricsstore.Open(&metricsstore.Config{
		Path:      t.TempDir(),
		Retention: time.Hour,
	})
	if err != nil {
		t.Fatalf("open test TSDB: %v", err)
	}
	t.Cleanup(func() {
		if err := db.Close(); err != nil {
			t.Errorf("close test TSDB: %v", err)
		}
	})

	evaluationTime := time.Unix(1788433200, 0).UTC()
	appender := db.Appender(context.Background())
	for _, sample := range []struct {
		metric labels.Labels
		value  float64
	}{
		{
			metric: labels.FromStrings("__name__", "cpu", "namespace", "test", "pod", "tidb-0"),
			value:  2,
		},
		{
			metric: labels.FromStrings("__name__", "cpu", "namespace", "test", "pod", "tidb-1"),
			value:  3,
		},
	} {
		if _, err := appender.Append(0, sample.metric, evaluationTime.UnixMilli(), sample.value); err != nil {
			t.Fatalf("append test sample: %v", err)
		}
	}
	if err := appender.Commit(); err != nil {
		t.Fatalf("commit test samples: %v", err)
	}

	server, err := New(&Config{
		Address:   "127.0.0.1:9091",
		Queryable: db,
		Now:       func() time.Time { return evaluationTime },
	})
	if err != nil {
		t.Fatalf("create HTTP query server: %v", err)
	}
	return server, evaluationTime
}
