package clinic_internal_api

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

	"github.com/stretchr/testify/require"
)

const validO11yClusterID = "byoc-o11y_cluster-1"

func validSlowQueryTarget(clusterID string) SlowQueryTarget {
	return SlowQueryTarget{
		BaseURL:       "http://byoc-ds.internal:8481",
		O11yClusterID: validO11yClusterID,
		Headers: SlowQueryTargetHeaders{
			Env:           "prod",
			BizType:       "byoc",
			OrgID:         "123",
			RegionID:      "aws-us-west-2",
			ApplicationID: clusterID,
		},
		Provider:     "aws",
		Region:       "us-west-2",
		DeployType:   "byoc",
		DeployTypeV2: "byoc",
	}
}

func TestGetSlowQueryTargetUsesFixedEscapedPathAndDecodesResponse(t *testing.T) {
	want := validSlowQueryTarget("cluster/a b")
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, http.MethodGet, r.Method)
		require.Equal(t,
			"/clinic/api/v1/internal/data-proxy/v1/orgs/123/clusters/cluster%2Fa%20b/targets/slow-query",
			r.URL.EscapedPath(),
		)
		require.Empty(t, r.URL.RawQuery)
		require.Empty(t, r.Header.Get("Authorization"))
		require.Empty(t, r.Header.Get("Cookie"))
		require.NoError(t, json.NewEncoder(w).Encode(want))
	}))
	defer server.Close()

	client := NewClient(server.URL)
	target, err := client.GetSlowQueryTarget(context.Background(), 123, "cluster/a b")
	require.NoError(t, err)
	require.Equal(t, &want, target)
}

func TestGetSlowQueryTargetRejectsInvalidScopeWithoutRequest(t *testing.T) {
	for _, tt := range []struct {
		name      string
		orgID     uint64
		clusterID string
		wantError string
	}{
		{name: "zero org", orgID: 0, clusterID: "cluster-a", wantError: "org_id must be a positive integer"},
		{name: "empty cluster", orgID: 123, clusterID: "", wantError: "cluster_id is required"},
	} {
		t.Run(tt.name, func(t *testing.T) {
			called := false
			server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
				called = true
			}))
			defer server.Close()

			client := NewClient(server.URL)
			target, err := client.GetSlowQueryTarget(context.Background(), tt.orgID, tt.clusterID)
			require.Nil(t, target)
			require.EqualError(t, err, tt.wantError)
			require.False(t, called)
		})
	}
}

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

	client := NewClient(server.URL)
	target, err := client.GetSlowQueryTarget(context.Background(), 123, "cluster-a")
	require.Nil(t, target)
	var upstreamError *UpstreamError
	require.True(t, errors.As(err, &upstreamError))
	require.Equal(t, http.StatusServiceUnavailable, upstreamError.StatusCode)
	require.NotContains(t, err.Error(), "sensitive internal target detail")
}

func TestSlowQueryTargetValidationAcceptsBYOCResourcePool(t *testing.T) {
	target := validSlowQueryTarget("physical-cluster")
	target.DeployType = "byoc-host"
	target.DeployTypeV2 = "byoc resource pool"
	require.NoError(t, target.validate(123, "physical-cluster"))
}

func TestSlowQueryTargetValidationRejectsInvalidTarget(t *testing.T) {
	tests := []struct {
		name       string
		mutate     func(*SlowQueryTarget)
		errMessage string
	}{
		{
			name:       "missing required field",
			mutate:     func(target *SlowQueryTarget) { target.Headers.Env = "" },
			errMessage: "headers.env is required",
		},
		{
			name:       "surrounding whitespace",
			mutate:     func(target *SlowQueryTarget) { target.Provider = " aws" },
			errMessage: "provider must not contain surrounding whitespace",
		},
		{
			name:       "invalid O11y cluster ID",
			mutate:     func(target *SlowQueryTarget) { target.O11yClusterID = "../other-cluster" },
			errMessage: "o11yClusterId is invalid",
		},
		{
			name:       "wrong biz type",
			mutate:     func(target *SlowQueryTarget) { target.Headers.BizType = "tidbcloud" },
			errMessage: "headers.bizType must be byoc",
		},
		{
			name:       "organization mismatch",
			mutate:     func(target *SlowQueryTarget) { target.Headers.OrgID = "999" },
			errMessage: "headers.orgId does not match the requested organization",
		},
		{
			name:       "cluster mismatch",
			mutate:     func(target *SlowQueryTarget) { target.Headers.ApplicationID = "cluster-b" },
			errMessage: "headers.applicationId does not match the requested cluster",
		},
		{
			name:       "region mismatch",
			mutate:     func(target *SlowQueryTarget) { target.Headers.RegionID = "gcp-us-west-2" },
			errMessage: "headers.regionId does not match provider and region",
		},
		{
			name:       "non BYOC deploy type",
			mutate:     func(target *SlowQueryTarget) { target.DeployType = "premium" },
			errMessage: "deploy type is not a consistent BYOC target",
		},
		{
			name:       "conflicting BYOC deploy types",
			mutate:     func(target *SlowQueryTarget) { target.DeployTypeV2 = "byoc resource pool" },
			errMessage: "deploy type is not a consistent BYOC target",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			target := validSlowQueryTarget("cluster-a")
			tt.mutate(&target)
			require.ErrorContains(t, target.validate(123, "cluster-a"), tt.errMessage)
		})
	}
}

func TestSlowQueryTargetValidationRejectsUnsafeBaseURL(t *testing.T) {
	for _, tt := range []struct {
		name       string
		baseURL    string
		errMessage string
	}{
		{name: "relative", baseURL: "/internal", errMessage: "absolute HTTP(S) URL"},
		{name: "unsupported scheme", baseURL: "ftp://byoc-ds.internal", errMessage: "absolute HTTP(S) URL"},
		{name: "userinfo", baseURL: "https://user:secret@byoc-ds.internal", errMessage: "only scheme and host"},
		{name: "path", baseURL: "https://byoc-ds.internal/internal", errMessage: "only scheme and host"},
		{name: "query", baseURL: "https://byoc-ds.internal?token=secret", errMessage: "only scheme and host"},
		{name: "empty query", baseURL: "https://byoc-ds.internal?", errMessage: "only scheme and host"},
		{name: "fragment", baseURL: "https://byoc-ds.internal#secret", errMessage: "only scheme and host"},
		{name: "localhost", baseURL: "http://localhost:8481", errMessage: "localhost target is not allowed"},
		{name: "localhost subdomain", baseURL: "http://x.localhost:8481", errMessage: "localhost target is not allowed"},
		{name: "localhost trailing dot", baseURL: "http://localhost.:8481", errMessage: "localhost target is not allowed"},
		{name: "IPv4 loopback", baseURL: "http://127.0.0.1:8481", errMessage: "localhost target is not allowed"},
		{name: "IPv6 loopback", baseURL: "http://[::1]:8481", errMessage: "localhost target is not allowed"},
	} {
		t.Run(tt.name, func(t *testing.T) {
			target := validSlowQueryTarget("cluster-a")
			target.BaseURL = tt.baseURL
			require.ErrorContains(t, target.validate(123, "cluster-a"), tt.errMessage)
		})
	}
}

func TestGetSlowQueryTargetRejectsTrailingJSON(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		body, err := json.Marshal(validSlowQueryTarget("cluster-a"))
		require.NoError(t, err)
		_, _ = w.Write(append(body, []byte(` {}`)...))
	}))
	defer server.Close()

	client := NewClient(server.URL)
	target, err := client.GetSlowQueryTarget(context.Background(), 123, "cluster-a")
	require.Nil(t, target)
	require.ErrorContains(t, err, "trailing JSON data")
}

func TestGetSlowQueryTargetRejectsOversizedResponse(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		_, _ = io.CopyN(w, slowQueryZeroReader{}, int64(maxResourceResponseBytes+1))
	}))
	defer server.Close()

	client := NewClient(server.URL)
	target, err := client.GetSlowQueryTarget(context.Background(), 123, "cluster-a")
	require.Nil(t, target)
	require.EqualError(t, err, "Clinic internal API response exceeds size limit")
}

type slowQueryZeroReader struct{}

func (slowQueryZeroReader) Read(p []byte) (int, error) {
	clear(p)
	return len(p), nil
}
