package service

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

	"github.com/stretchr/testify/require"

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

type fakeBYOCSlowQueryTargetResolver struct {
	target    *clinic_internal_api.SlowQueryTarget
	err       error
	orgID     uint64
	clusterID string
}

func (f *fakeBYOCSlowQueryTargetResolver) GetSlowQueryTarget(
	_ context.Context,
	orgID uint64,
	clusterID string,
) (*clinic_internal_api.SlowQueryTarget, error) {
	f.orgID = orgID
	f.clusterID = clusterID
	return f.target, f.err
}

func validBYOCSlowQueryTarget(baseURL string) *clinic_internal_api.SlowQueryTarget {
	return &clinic_internal_api.SlowQueryTarget{
		BaseURL:       baseURL,
		O11yClusterID: "o11y-cluster_a",
		Provider:      "aws",
		Region:        "us-east-1",
		DeployType:    "byoc",
		DeployTypeV2:  "byoc",
		Headers: clinic_internal_api.SlowQueryTargetHeaders{
			Env:           "prod",
			BizType:       "byoc",
			OrgID:         "123",
			RegionID:      "aws-us-east-1",
			ApplicationID: "cluster-a",
		},
	}
}

func TestNewBYOCSlowQueryServiceUsesQueryTimeout(t *testing.T) {
	svc := NewBYOCSlowQueryService(&fakeBYOCSlowQueryTargetResolver{})
	require.Equal(t, 2*time.Minute, svc.httpClient.Timeout)
}

func TestBYOCSlowQueryServiceCallsFixedEndpointsWithServerHeaders(t *testing.T) {
	tests := []struct {
		name      string
		operation BYOCSlowQueryOperation
		wantPath  string
		wantBody  []byte
		query     url.Values
	}{
		{
			name:      "available fields",
			operation: BYOCSlowQueryOperationAvailableFields,
			wantPath:  "/internal/o11y-cluster_a/diagnosis/api/v1/slow_query/available_fields",
			wantBody:  []byte(`["query","timestamp"]`),
		},
		{
			name:      "list",
			operation: BYOCSlowQueryOperationList,
			wantPath:  "/internal/o11y-cluster_a/diagnosis/api/v1/slow_query/list",
			wantBody:  []byte(`[{"future":true}]`),
			query:     url.Values{"db": {"db1", "db2"}, "end_time": {"2"}},
		},
		{
			name:      "detail",
			operation: BYOCSlowQueryOperationDetail,
			wantPath:  "/internal/o11y-cluster_a/diagnosis/api/v1/slow_query/detail",
			wantBody:  []byte(`{"future":true}`),
			query:     url.Values{"digest": {"abc"}, "connect_id": {"42"}},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				require.Equal(t, tt.wantPath, r.URL.Path)
				require.Equal(t, tt.query.Encode(), r.URL.Query().Encode())
				require.Equal(t, "application/json", r.Header.Get("Accept"))
				require.Equal(t, "prod", r.Header.Get("X-Env"))
				require.Equal(t, "byoc", r.Header.Get("X-Biz-Type"))
				require.Equal(t, "123", r.Header.Get("X-Org-Id"))
				require.Equal(t, "aws-us-east-1", r.Header.Get("X-Region-Id"))
				require.Equal(t, "cluster-a", r.Header.Get("X-Application-Id"))
				require.Empty(t, r.Header.Get("Authorization"))
				require.Empty(t, r.Header.Get("Cookie"))
				require.Empty(t, r.Header.Get("X-Forwarded-Host"))
				_, _ = w.Write(tt.wantBody)
			}))
			defer upstream.Close()

			baseURL, client := byocSlowQueryTestUpstream(upstream)
			resolver := &fakeBYOCSlowQueryTargetResolver{target: validBYOCSlowQueryTarget(baseURL)}
			svc := newBYOCSlowQueryService(resolver, client)
			result, err := svc.Query(context.Background(), 123, "cluster-a", tt.operation, tt.query)
			require.NoError(t, err)
			require.Equal(t, tt.wantBody, result.Body)
			require.Equal(t, "aws", result.Provider)
			require.Equal(t, "us-east-1", result.Region)
			require.Equal(t, "byoc", result.DeployTypeV2)
			require.Equal(t, uint64(123), resolver.orgID)
			require.Equal(t, "cluster-a", resolver.clusterID)
		})
	}
}

func TestBYOCSlowQueryServiceProjectsListFieldsAndPreservesExactDetailKey(t *testing.T) {
	const timestamp = "1760000000.123456789"
	upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		_, _ = io.WriteString(w, `[{
			"digest":"digest-a",
			"connection_id":"42",
			"timestamp":`+timestamp+`,
			"query_time":1.25,
			"query":"sensitive customer SQL",
			"plan":"sensitive execution plan",
			"future_field":"must not escape"
		}]`)
	}))
	defer upstream.Close()

	baseURL, client := byocSlowQueryTestUpstream(upstream)
	svc := newBYOCSlowQueryService(
		&fakeBYOCSlowQueryTargetResolver{target: validBYOCSlowQueryTarget(baseURL)},
		client,
	)
	result, err := svc.Query(context.Background(), 123, "cluster-a", BYOCSlowQueryOperationList, url.Values{
		"fields": {"query_time"},
	})
	require.NoError(t, err)

	var rows []map[string]json.RawMessage
	require.NoError(t, json.Unmarshal(result.Body, &rows))
	require.Len(t, rows, 1)
	require.ElementsMatch(t,
		[]string{"connection_id", "digest", "query_time", "timestamp"},
		byocSlowQueryMapKeys(rows[0]),
	)
	require.Equal(t, timestamp, string(rows[0]["timestamp"]))
	require.NotContains(t, string(result.Body), "sensitive customer SQL")
	require.NotContains(t, string(result.Body), "sensitive execution plan")
	require.NotContains(t, string(result.Body), "future_field")
}

func TestBYOCSlowQueryServiceReturnsExplicitlyRequestedSensitiveFieldOnly(t *testing.T) {
	upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		_, _ = io.WriteString(w, `[{"digest":"d","connection_id":"1","timestamp":1.25,"query":"selected","plan":"not selected"}]`)
	}))
	defer upstream.Close()

	baseURL, client := byocSlowQueryTestUpstream(upstream)
	svc := newBYOCSlowQueryService(
		&fakeBYOCSlowQueryTargetResolver{target: validBYOCSlowQueryTarget(baseURL)},
		client,
	)
	result, err := svc.Query(context.Background(), 123, "cluster-a", BYOCSlowQueryOperationList, url.Values{
		"fields": {"query"},
	})
	require.NoError(t, err)
	require.Contains(t, string(result.Body), `"query":"selected"`)
	require.NotContains(t, string(result.Body), `"plan"`)
}

func TestBYOCSlowQueryServiceRejectsNullListRowDuringProjection(t *testing.T) {
	upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		_, _ = io.WriteString(w, `[null]`)
	}))
	defer upstream.Close()

	baseURL, client := byocSlowQueryTestUpstream(upstream)
	svc := newBYOCSlowQueryService(
		&fakeBYOCSlowQueryTargetResolver{target: validBYOCSlowQueryTarget(baseURL)},
		client,
	)
	_, err := svc.Query(context.Background(), 123, "cluster-a", BYOCSlowQueryOperationList, url.Values{
		"fields": {"query_time"},
	})
	require.ErrorIs(t, err, ErrInvalidBYOCSlowQueryResponse)
}

func TestBYOCSlowQueryServiceDoesNotFollowRedirect(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, r *http.Request) {
		http.Redirect(w, r, destination.URL, http.StatusFound)
	}))
	defer upstream.Close()

	baseURL, client := byocSlowQueryTestUpstream(upstream)
	svc := newBYOCSlowQueryService(
		&fakeBYOCSlowQueryTargetResolver{target: validBYOCSlowQueryTarget(baseURL)},
		client,
	)
	_, err := svc.Query(context.Background(), 123, "cluster-a", BYOCSlowQueryOperationList, nil)
	var upstreamError *BYOCSlowQueryUpstreamError
	require.ErrorAs(t, err, &upstreamError)
	require.Equal(t, http.StatusFound, upstreamError.StatusCode)
	require.False(t, destinationCalled)
}

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

	baseURL, client := byocSlowQueryTestUpstream(upstream)
	svc := newBYOCSlowQueryService(
		&fakeBYOCSlowQueryTargetResolver{target: validBYOCSlowQueryTarget(baseURL)},
		client,
	)
	_, err := svc.Query(context.Background(), 123, "cluster-a", BYOCSlowQueryOperationList, nil)
	var upstreamError *BYOCSlowQueryUpstreamError
	require.ErrorAs(t, err, &upstreamError)
	require.Equal(t, http.StatusServiceUnavailable, upstreamError.StatusCode)
	require.NotContains(t, err.Error(), "sensitive customer SQL")
}

func TestBYOCSlowQueryServiceRejectsOversizedResponse(t *testing.T) {
	upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		_, _ = io.CopyN(w, strings.NewReader(strings.Repeat("x", maxBYOCSlowQueryResponse+1)), maxBYOCSlowQueryResponse+1)
	}))
	defer upstream.Close()

	baseURL, client := byocSlowQueryTestUpstream(upstream)
	svc := newBYOCSlowQueryService(
		&fakeBYOCSlowQueryTargetResolver{target: validBYOCSlowQueryTarget(baseURL)},
		client,
	)
	_, err := svc.Query(context.Background(), 123, "cluster-a", BYOCSlowQueryOperationList, nil)
	require.ErrorIs(t, err, ErrInvalidBYOCSlowQueryResponse)
}

func TestBYOCSlowQueryServiceRejectsInvalidTargetAndResponse(t *testing.T) {
	for _, tt := range []struct {
		name   string
		mutate func(*clinic_internal_api.SlowQueryTarget)
	}{
		{name: "userinfo", mutate: func(target *clinic_internal_api.SlowQueryTarget) { target.BaseURL = "https://user@example.com" }},
		{name: "path", mutate: func(target *clinic_internal_api.SlowQueryTarget) { target.BaseURL = "https://example.com/caller" }},
		{name: "localhost", mutate: func(target *clinic_internal_api.SlowQueryTarget) { target.BaseURL = "http://localhost:8080" }},
		{name: "localhost trailing dot", mutate: func(target *clinic_internal_api.SlowQueryTarget) { target.BaseURL = "http://localhost.:8080" }},
		{name: "IPv4 loopback", mutate: func(target *clinic_internal_api.SlowQueryTarget) { target.BaseURL = "http://127.0.0.1:8080" }},
		{name: "cluster slash", mutate: func(target *clinic_internal_api.SlowQueryTarget) { target.O11yClusterID = "../other" }},
	} {
		t.Run(tt.name, func(t *testing.T) {
			target := validBYOCSlowQueryTarget("https://example.com")
			tt.mutate(target)
			svc := newBYOCSlowQueryService(&fakeBYOCSlowQueryTargetResolver{target: target}, nil)
			_, err := svc.Query(context.Background(), 123, "cluster-a", BYOCSlowQueryOperationList, nil)
			require.ErrorIs(t, err, ErrInvalidBYOCSlowQueryTarget)
		})
	}

	for _, tt := range []struct {
		name      string
		operation BYOCSlowQueryOperation
		body      string
	}{
		{name: "invalid JSON", operation: BYOCSlowQueryOperationList, body: `not-json`},
		{name: "multiple JSON values", operation: BYOCSlowQueryOperationList, body: `{} {}`},
		{name: "list object", operation: BYOCSlowQueryOperationList, body: `{}`},
		{name: "available fields object", operation: BYOCSlowQueryOperationAvailableFields, body: `{}`},
		{name: "detail array", operation: BYOCSlowQueryOperationDetail, body: `[]`},
	} {
		t.Run(tt.name, func(t *testing.T) {
			upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
				_, _ = io.WriteString(w, tt.body)
			}))
			defer upstream.Close()
			baseURL, client := byocSlowQueryTestUpstream(upstream)
			svc := newBYOCSlowQueryService(
				&fakeBYOCSlowQueryTargetResolver{target: validBYOCSlowQueryTarget(baseURL)},
				client,
			)
			_, err := svc.Query(context.Background(), 123, "cluster-a", tt.operation, nil)
			require.ErrorIs(t, err, ErrInvalidBYOCSlowQueryResponse)
		})
	}
}

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

func byocSlowQueryTestUpstream(upstream *httptest.Server) (string, *http.Client) {
	client := *upstream.Client()
	transport := client.Transport.(*http.Transport).Clone()
	upstreamAddress := upstream.Listener.Addr().String()
	transport.DialContext = func(ctx context.Context, network, _ string) (net.Conn, error) {
		return (&net.Dialer{}).DialContext(ctx, network, upstreamAddress)
	}
	client.Transport = transport
	return "http://byoc-ds.example.test", &client
}

func byocSlowQueryMapKeys[V any](values map[string]V) []string {
	keys := make([]string, 0, len(values))
	for key := range values {
		keys = append(keys, key)
	}
	return keys
}
