package service

import (
	"context"
	"errors"
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	clinic_internal_api "github.com/pingcap-inc/clinic/clinic_data_service/pkg/clinic-internal-api"
)

func TestNewTopRUServiceUsesQueryTimeout(t *testing.T) {
	svc := NewTopRUService(&fakeTopRUTargetResolver{})
	require.Equal(t, 2*time.Minute, svc.httpClient.Timeout)
}

type fakeTopRUTargetResolver struct {
	target    *clinic_internal_api.TopRUTarget
	err       error
	orgID     uint64
	clusterID string
}

func (f *fakeTopRUTargetResolver) GetTopRUTarget(_ context.Context, orgID uint64, clusterID string) (*clinic_internal_api.TopRUTarget, error) {
	f.orgID = orgID
	f.clusterID = clusterID
	return f.target, f.err
}

func validTopRUTarget(baseURL string) *clinic_internal_api.TopRUTarget {
	return &clinic_internal_api.TopRUTarget{
		BaseURL: baseURL,
		Headers: clinic_internal_api.TopRUTargetHeaders{
			Env:          "staging",
			BizType:      "staging-tidbcloud-nextgen",
			Provider:     "aws",
			Region:       "us-west-2",
			OrgID:        "123",
			ProjectID:    "456",
			ClusterID:    "cluster-a",
			DeployType:   "premium",
			DeployTypeV2: "premium",
		},
	}
}

func TestTopRUServiceCallsFixedSummaryPathWithServerHeadersAndRawJSON(t *testing.T) {
	wantBody := []byte(`{"status":"success","data":{"data":[{"sql_digest":"digest","total_ru":42}],"step":60},"future_field":true}`)
	upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, topRUSummaryPath, r.URL.Path)
		require.Equal(t, []string{"10", "20"}, r.URL.Query()["top"])
		user, exists := r.URL.Query()["user"]
		require.True(t, exists)
		require.Equal(t, []string{""}, user)
		require.Equal(t, "staging", r.Header.Get("X-Env"))
		require.Equal(t, "staging-tidbcloud-nextgen", r.Header.Get("X-Biz-Type"))
		require.Equal(t, "aws", r.Header.Get("X-Provider"))
		require.Equal(t, "us-west-2", r.Header.Get("X-Region"))
		require.Equal(t, "123", r.Header.Get("X-Org-Id"))
		require.Equal(t, "456", r.Header.Get("X-Project-Id"))
		require.Equal(t, "cluster-a", r.Header.Get("X-Cluster-Id"))
		require.Equal(t, "premium", r.Header.Get("X-Deploy-Type"))
		require.Empty(t, r.Header.Get("Authorization"))
		require.Empty(t, r.Header.Get("Cookie"))
		_, _ = w.Write(wantBody)
	}))
	defer upstream.Close()

	resolver := &fakeTopRUTargetResolver{target: validTopRUTarget(upstream.URL)}
	svc := newTopRUService(resolver, upstream.Client())
	result, err := svc.Query(context.Background(), 123, "cluster-a", TopRUOperationSummary, url.Values{
		"top":  {"10", "20"},
		"user": {""},
	})
	require.NoError(t, err)
	require.Equal(t, wantBody, result.Body)
	require.Equal(t, "aws", result.Provider)
	require.Equal(t, "us-west-2", result.Region)
	require.Equal(t, "premium", result.DeployTypeV2)
	require.Equal(t, uint64(123), resolver.orgID)
	require.Equal(t, "cluster-a", resolver.clusterID)
}

func TestTopRUServiceCallsFixedSummaryPlanPath(t *testing.T) {
	wantBody := []byte(`{"status":"success","data":{"data":[{"sql_digest":"sql-digest","plan_digest":"plan-a"},{"sql_digest":"sql-digest","plan_digest":"plan-b"}],"step":30},"future_field":true}`)
	upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, topRUSummaryPlanPath, r.URL.Path)
		require.Equal(t, "sql-digest", r.URL.Query().Get("digest"))
		require.Equal(t, "ru", r.URL.Query().Get("order_by"))
		require.Equal(t, "30", r.URL.Query().Get("step"))
		_, _ = w.Write(wantBody)
	}))
	defer upstream.Close()

	svc := newTopRUService(&fakeTopRUTargetResolver{target: validTopRUTarget(upstream.URL)}, upstream.Client())
	result, err := svc.Query(context.Background(), 123, "cluster-a", TopRUOperationSummaryPlan, url.Values{
		"digest":   {"sql-digest"},
		"order_by": {"ru"},
		"step":     {"30"},
	})
	require.NoError(t, err)
	require.Equal(t, wantBody, result.Body)
}

func TestTopRUServiceCallsFixedUserStatsPath(t *testing.T) {
	upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, topRUUserStatsPath, r.URL.Path)
		require.Equal(t, "1", r.URL.Query().Get("start"))
		_, _ = io.WriteString(w, `{"status":"success","data":[]}`)
	}))
	defer upstream.Close()

	svc := newTopRUService(&fakeTopRUTargetResolver{target: validTopRUTarget(upstream.URL)}, upstream.Client())
	result, err := svc.Query(context.Background(), 123, "cluster-a", TopRUOperationUserStats, url.Values{"start": {"1"}})
	require.NoError(t, err)
	require.JSONEq(t, `{"status":"success","data":[]}`, string(result.Body))
}

func TestTopRUServiceDoesNotFollowRedirect(t *testing.T) {
	destinationCalled := false
	destination := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		destinationCalled = true
		_, _ = io.WriteString(w, `{}`)
	}))
	defer destination.Close()
	upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		http.Redirect(w, &http.Request{}, destination.URL, http.StatusFound)
	}))
	defer upstream.Close()

	svc := newTopRUService(&fakeTopRUTargetResolver{target: validTopRUTarget(upstream.URL)}, upstream.Client())
	_, err := svc.Query(context.Background(), 123, "cluster-a", TopRUOperationSummary, nil)
	var upstreamError *TopRUUpstreamError
	require.ErrorAs(t, err, &upstreamError)
	require.Equal(t, http.StatusFound, upstreamError.StatusCode)
	require.False(t, destinationCalled)
}

func TestTopRUServiceDiscardsUpstreamErrorBody(t *testing.T) {
	upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		http.Error(w, "sensitive O11y detail", http.StatusServiceUnavailable)
	}))
	defer upstream.Close()

	svc := newTopRUService(&fakeTopRUTargetResolver{target: validTopRUTarget(upstream.URL)}, upstream.Client())
	_, err := svc.Query(context.Background(), 123, "cluster-a", TopRUOperationSummary, nil)
	var upstreamError *TopRUUpstreamError
	require.ErrorAs(t, err, &upstreamError)
	require.Equal(t, http.StatusServiceUnavailable, upstreamError.StatusCode)
	require.NotContains(t, err.Error(), "sensitive O11y detail")
}

func TestTopRUServiceRejectsInvalidTargetAndResponse(t *testing.T) {
	t.Run("target", func(t *testing.T) {
		svc := newTopRUService(&fakeTopRUTargetResolver{target: validTopRUTarget("https://user@example.com")}, nil)
		_, err := svc.Query(context.Background(), 123, "cluster-a", TopRUOperationSummary, nil)
		require.ErrorIs(t, err, ErrInvalidTopRUTarget)
	})

	for _, body := range []string{`not-json`, `{} {}`} {
		t.Run(body, func(t *testing.T) {
			upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
				_, _ = io.WriteString(w, body)
			}))
			defer upstream.Close()
			svc := newTopRUService(&fakeTopRUTargetResolver{target: validTopRUTarget(upstream.URL)}, upstream.Client())
			_, err := svc.Query(context.Background(), 123, "cluster-a", TopRUOperationSummary, nil)
			require.ErrorIs(t, err, ErrInvalidTopRUResponse)
		})
	}
}

func TestTopRUServicePreservesTargetResolutionError(t *testing.T) {
	want := &clinic_internal_api.UpstreamError{StatusCode: http.StatusNotFound}
	svc := newTopRUService(&fakeTopRUTargetResolver{err: want}, nil)
	_, err := svc.Query(context.Background(), 123, "cluster-a", TopRUOperationSummary, nil)
	var got *clinic_internal_api.UpstreamError
	require.True(t, errors.As(err, &got))
	require.Equal(t, http.StatusNotFound, got.StatusCode)
}
