package clinic

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

func TestClientJoinsBaseURLAndPath(t *testing.T) {
	t.Parallel()

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if got, want := r.URL.Path, "/api/clinic/v1/clusters"; got != want {
			t.Fatalf("path = %q, want %q", got, want)
		}
		writeJSONResponse(t, w, map[string]string{"status": "ok"})
	}))
	defer server.Close()

	client, err := NewClient(server.URL+"/api/", "token")
	if err != nil {
		t.Fatalf("NewClient() error = %v", err)
	}

	var got map[string]string
	if err := client.Do(context.Background(), Request{
		Method: http.MethodGet,
		Path:   "/clinic/v1/clusters",
	}, &got); err != nil {
		t.Fatalf("Do() error = %v", err)
	}

	if got["status"] != "ok" {
		t.Fatalf("decoded response = %#v, want status ok", got)
	}
}

func TestClientRawPathPreservesEscapedDynamicPathSegment(t *testing.T) {
	t.Parallel()

	var requestURI string
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		requestURI = r.RequestURI
		writeJSONResponse(t, w, map[string]bool{"ok": true})
	}))
	defer server.Close()

	client, err := NewClient(server.URL+"/api", "token")
	if err != nil {
		t.Fatalf("NewClient() error = %v", err)
	}

	request := Request{
		Method:  http.MethodGet,
		Path:    "/system-log-datasources/loki/team?prod#one%/logs/query-range",
		RawPath: "/system-log-datasources/loki%2Fteam%3Fprod%23one%25/logs/query-range",
		Query:   url.Values{"query": []string{`{cluster="physical-pool"}`}},
	}
	if err := client.Do(context.Background(), request, nil); err != nil {
		t.Fatalf("Do() error = %v", err)
	}

	if got, want := requestURI, `/api/system-log-datasources/loki%2Fteam%3Fprod%23one%25/logs/query-range?query=%7Bcluster%3D%22physical-pool%22%7D`; got != want {
		t.Fatalf("RequestURI = %q, want %q", got, want)
	}
}

func TestClientRawPathPreservesEncodedBasePath(t *testing.T) {
	t.Parallel()

	testCases := []struct {
		name        string
		basePath    string
		wantURI     string
		wantURLPath string
	}{
		{
			name:        "encoded base segment",
			basePath:    "/tenant%2Froot",
			wantURI:     "/tenant%2Froot/system-log-datasources/loki%2Fteam/logs/query-range",
			wantURLPath: "/tenant/root/system-log-datasources/loki/team/logs/query-range",
		},
		{
			name:        "trailing encoded slash",
			basePath:    "/tenant%2Froot%2F",
			wantURI:     "/tenant%2Froot%2F/system-log-datasources/loki%2Fteam/logs/query-range",
			wantURLPath: "/tenant/root//system-log-datasources/loki/team/logs/query-range",
		},
	}

	for _, tc := range testCases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			var gotURI string
			var gotURLPath string
			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				gotURI = r.RequestURI
				gotURLPath = r.URL.Path
				writeJSONResponse(t, w, map[string]bool{"ok": true})
			}))
			defer server.Close()

			client, err := NewClient(server.URL+tc.basePath, "token")
			if err != nil {
				t.Fatalf("NewClient() error = %v", err)
			}
			request := Request{
				Method:  http.MethodGet,
				Path:    "/system-log-datasources/loki/team/logs/query-range",
				RawPath: "/system-log-datasources/loki%2Fteam/logs/query-range",
			}

			if err := client.Do(context.Background(), request, nil); err != nil {
				t.Fatalf("Do() error = %v", err)
			}
			if gotURI != tc.wantURI {
				t.Fatalf("RequestURI = %q, want %q", gotURI, tc.wantURI)
			}
			if gotURLPath != tc.wantURLPath {
				t.Fatalf("URL.Path = %q, want %q", gotURLPath, tc.wantURLPath)
			}
		})
	}
}

func TestClientRawPathRejectsMismatchedDecodedPath(t *testing.T) {
	t.Parallel()

	client, err := NewClient("https://example.com/api", "token")
	if err != nil {
		t.Fatalf("NewClient() error = %v", err)
	}
	request := Request{
		Method:  http.MethodGet,
		Path:    "/decoded/path",
		RawPath: "/different%2Fpath",
	}

	err = client.Do(context.Background(), request, nil)
	if err == nil {
		t.Fatal("Do() error = nil, want RawPath validation error")
	}
	if got := strings.ToLower(err.Error()); !strings.Contains(got, "rawpath") || !strings.Contains(got, "path") {
		t.Fatalf("Do() error = %q, want RawPath/Path mismatch context", err)
	}
}

func TestClientInjectsBearerAuthorizationHeader(t *testing.T) {
	t.Parallel()

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if got, want := r.Header.Get("Authorization"), "Bearer secret-token"; got != want {
			t.Fatalf("Authorization header = %q, want %q", got, want)
		}
		writeJSONResponse(t, w, map[string]bool{"ok": true})
	}))
	defer server.Close()

	client, err := NewClient(server.URL, "secret-token")
	if err != nil {
		t.Fatalf("NewClient() error = %v", err)
	}

	var got map[string]bool
	if err := client.Do(context.Background(), Request{
		Method: http.MethodGet,
		Path:   "/auth-check",
	}, &got); err != nil {
		t.Fatalf("Do() error = %v", err)
	}

	if !got["ok"] {
		t.Fatalf("decoded response = %#v, want ok=true", got)
	}
}

func TestClientEncodesQueryParametersIncludingRepeatedValues(t *testing.T) {
	t.Parallel()

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if got, want := r.URL.Query()["match[]"], []string{"a", "b"}; !equalStrings(got, want) {
			t.Fatalf("match[] = %#v, want %#v", got, want)
		}
		if got, want := r.URL.Query().Get("query"), "up{job=\"tidb cloud\"}"; got != want {
			t.Fatalf("query = %q, want %q", got, want)
		}
		writeJSONResponse(t, w, map[string]bool{"ok": true})
	}))
	defer server.Close()

	client, err := NewClient(server.URL, "token")
	if err != nil {
		t.Fatalf("NewClient() error = %v", err)
	}

	var got map[string]bool
	if err := client.Do(context.Background(), Request{
		Method: http.MethodGet,
		Path:   "/series",
		Query: url.Values{
			"match[]": []string{"a", "b"},
			"query":   []string{"up{job=\"tidb cloud\"}"},
		},
	}, &got); err != nil {
		t.Fatalf("Do() error = %v", err)
	}

	if !got["ok"] {
		t.Fatalf("decoded response = %#v, want ok=true", got)
	}
}

func TestClientSerializesJSONRequestBody(t *testing.T) {
	t.Parallel()

	type payload struct {
		ClusterID string `json:"cluster_id"`
		SQL       string `json:"sql"`
	}

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if got, want := r.Header.Get("Content-Type"), "application/json"; got != want {
			t.Fatalf("Content-Type = %q, want %q", got, want)
		}

		var gotBody payload
		if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
			t.Fatalf("decode request body: %v", err)
		}
		if gotBody != (payload{ClusterID: "123", SQL: "select 1"}) {
			t.Fatalf("request body = %#v, want cluster_id/sql payload", gotBody)
		}

		writeJSONResponse(t, w, map[string]string{"status": "queued"})
	}))
	defer server.Close()

	client, err := NewClient(server.URL, "token")
	if err != nil {
		t.Fatalf("NewClient() error = %v", err)
	}

	var got map[string]string
	if err := client.Do(context.Background(), Request{
		Method: http.MethodPost,
		Path:   "/data/query",
		Body: payload{
			ClusterID: "123",
			SQL:       "select 1",
		},
	}, &got); err != nil {
		t.Fatalf("Do() error = %v", err)
	}

	if got["status"] != "queued" {
		t.Fatalf("decoded response = %#v, want status queued", got)
	}
}

func TestClientReturnsHTTPErrorForNon2xxResponses(t *testing.T) {
	t.Parallel()

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		http.Error(w, "upstream exploded", http.StatusBadGateway)
	}))
	defer server.Close()

	client, err := NewClient(server.URL, "token")
	if err != nil {
		t.Fatalf("NewClient() error = %v", err)
	}

	err = client.Do(context.Background(), Request{
		Method: http.MethodGet,
		Path:   "/broken",
	}, nil)
	if err == nil {
		t.Fatal("Do() error = nil, want HTTPError")
	}

	var httpErr *HTTPError
	if !errors.As(err, &httpErr) {
		t.Fatalf("Do() error = %T, want *HTTPError", err)
	}
	if got, want := httpErr.StatusCode, http.StatusBadGateway; got != want {
		t.Fatalf("StatusCode = %d, want %d", got, want)
	}
	if got, want := httpErr.Body, "upstream exploded\n"; got != want {
		t.Fatalf("Body = %q, want %q", got, want)
	}
	if !strings.Contains(httpErr.Error(), "502") {
		t.Fatalf("Error() = %q, want status text", httpErr.Error())
	}
}

func TestHTTPErrorErrorSuggestsAuthSetKeyForInvalidUser(t *testing.T) {
	t.Parallel()

	httpErr := &HTTPError{
		StatusCode: http.StatusUnauthorized,
		Status:     "401 Unauthorized",
		Body:       "Invalid User",
	}

	if got := httpErr.Error(); !strings.Contains(got, "\"clinic auth set-key\"") {
		t.Fatalf("Error() = %q, want quoted auth hint command", got)
	}
}

func TestClientUsesDefaultTimeoutAndPerRequestOverride(t *testing.T) {
	t.Parallel()

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		time.Sleep(80 * time.Millisecond)
		writeJSONResponse(t, w, map[string]bool{"ok": true})
	}))
	defer server.Close()

	client, err := NewClient(server.URL, "token", WithDefaultTimeout(20*time.Millisecond))
	if err != nil {
		t.Fatalf("NewClient() error = %v", err)
	}

	err = client.Do(context.Background(), Request{
		Method: http.MethodGet,
		Path:   "/slow",
	}, nil)
	if err == nil {
		t.Fatal("Do() error = nil, want timeout error")
	}
	if !isDeadlineError(err) {
		t.Fatalf("Do() error = %v, want deadline exceeded", err)
	}

	var got map[string]bool
	if err := client.Do(context.Background(), Request{
		Method:  http.MethodGet,
		Path:    "/slow",
		Timeout: 150 * time.Millisecond,
	}, &got); err != nil {
		t.Fatalf("Do() override timeout error = %v", err)
	}
	if !got["ok"] {
		t.Fatalf("decoded response = %#v, want ok=true", got)
	}

	err = client.Do(context.Background(), Request{
		Method: http.MethodGet,
		Path:   "/slow",
	}, nil)
	if err == nil {
		t.Fatal("Do() error after override = nil, want timeout error")
	}
	if !isDeadlineError(err) {
		t.Fatalf("Do() error after override = %v, want deadline exceeded", err)
	}
}

func TestClientDoesNotApplyDeadlineByDefault(t *testing.T) {
	t.Parallel()

	client, err := NewClient("https://example.com", "token")
	if err != nil {
		t.Fatalf("NewClient() error = %v", err)
	}

	client.httpClient.Transport = roundTripperFunc(func(r *http.Request) (*http.Response, error) {
		if _, hasDeadline := r.Context().Deadline(); hasDeadline {
			t.Fatalf("request context unexpectedly has deadline")
		}

		return &http.Response{
			StatusCode: http.StatusOK,
			Header:     make(http.Header),
			Body:       io.NopCloser(strings.NewReader(`{"ok":true}`)),
		}, nil
	})

	var got map[string]bool
	if err := client.Do(context.Background(), Request{
		Method: http.MethodGet,
		Path:   "/healthz",
	}, &got); err != nil {
		t.Fatalf("Do() error = %v", err)
	}

	if !got["ok"] {
		t.Fatalf("decoded response = %#v, want ok=true", got)
	}
}

type roundTripperFunc func(*http.Request) (*http.Response, error)

func (f roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) {
	return f(r)
}

func writeJSONResponse(t *testing.T, w http.ResponseWriter, value any) {
	t.Helper()

	w.Header().Set("Content-Type", "application/json")
	if err := json.NewEncoder(w).Encode(value); err != nil {
		t.Fatalf("encode response: %v", err)
	}
}

func equalStrings(got, want []string) bool {
	if len(got) != len(want) {
		return false
	}
	for i := range got {
		if got[i] != want[i] {
			return false
		}
	}
	return true
}

func isDeadlineError(err error) bool {
	return errors.Is(err, context.DeadlineExceeded)
}
