package clinic_internal_api

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

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

const validTopRUTargetJSON = `{
  "baseUrl": "http://topru.internal",
  "headers": {
    "env": "staging",
    "bizType": "staging-tidbcloud-nextgen",
    "provider": "aws",
    "region": "us-west-2",
    "orgId": "123",
    "projectId": "456",
    "clusterId": "cluster/a b",
    "deployType": "premium",
    "deployTypeV2": "premium"
  }
}`

func TestGetTopRUTargetUsesFixedEscapedPathAndDecodesResponse(t *testing.T) {
	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/top-ru",
			r.URL.EscapedPath(),
		)
		require.Empty(t, r.URL.RawQuery)
		require.Empty(t, r.Header.Get("Authorization"))
		require.Empty(t, r.Header.Get("Cookie"))

		w.Header().Set("Content-Type", "application/json")
		_, _ = io.WriteString(w, validTopRUTargetJSON)
	}))
	defer server.Close()

	client := NewClient(server.URL)
	target, err := client.GetTopRUTarget(context.Background(), 123, "cluster/a b")
	require.NoError(t, err)
	require.Equal(t, "http://topru.internal", target.BaseURL)
	require.Equal(t, TopRUTargetHeaders{
		Env:          "staging",
		BizType:      "staging-tidbcloud-nextgen",
		Provider:     "aws",
		Region:       "us-west-2",
		OrgID:        "123",
		ProjectID:    "456",
		ClusterID:    "cluster/a b",
		DeployType:   "premium",
		DeployTypeV2: "premium",
	}, target.Headers)
}

func TestGetTopRUTargetRejectsZeroOrgIDWithoutRequest(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.GetTopRUTarget(context.Background(), 0, "cluster-a")
	require.Nil(t, target)
	require.EqualError(t, err, "org_id must be a positive integer")
	require.False(t, called)
}

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

	client := NewClient(server.URL)
	target, err := client.GetTopRUTarget(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 detail")
}

func TestGetTopRUTargetRejectsInvalidAndTrailingJSON(t *testing.T) {
	tests := []struct {
		name       string
		body       string
		errMessage string
	}{
		{
			name:       "invalid JSON",
			body:       `{"baseUrl":`,
			errMessage: "decode Clinic internal API response",
		},
		{
			name:       "trailing JSON",
			body:       validTopRUTargetJSON + ` {}`,
			errMessage: "trailing JSON data",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
				_, _ = io.WriteString(w, tt.body)
			}))
			defer server.Close()

			client := NewClient(server.URL)
			target, err := client.GetTopRUTarget(context.Background(), 123, "cluster-a")
			require.Nil(t, target)
			require.ErrorContains(t, err, tt.errMessage)
		})
	}
}

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

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

func TestGetTopRUTargetValidatesRequiredFields(t *testing.T) {
	tests := []struct {
		name       string
		body       string
		errMessage string
	}{
		{
			name:       "missing base URL",
			body:       `{"headers":{"env":"staging","bizType":"staging-tidbcloud-nextgen","provider":"aws","region":"us-west-2","orgId":"123","clusterId":"cluster-a","deployType":"premium","deployTypeV2":"premium"}}`,
			errMessage: "baseUrl is required",
		},
		{
			name:       "missing required routing header",
			body:       `{"baseUrl":"http://topru.internal","headers":{"env":"staging","bizType":"staging-tidbcloud-nextgen","region":"us-west-2","orgId":"123","clusterId":"cluster-a","deployType":"premium","deployTypeV2":"premium"}}`,
			errMessage: "headers.provider is required",
		},
		{
			name:       "base URL contains a path",
			body:       `{"baseUrl":"http://topru.internal/untrusted","headers":{"env":"staging","bizType":"staging-tidbcloud-nextgen","provider":"aws","region":"us-west-2","orgId":"123","clusterId":"cluster-a","deployType":"premium","deployTypeV2":"premium"}}`,
			errMessage: "baseUrl must contain only scheme and host",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
				_, _ = io.WriteString(w, tt.body)
			}))
			defer server.Close()

			client := NewClient(server.URL)
			target, err := client.GetTopRUTarget(context.Background(), 123, "cluster-a")
			require.Nil(t, target)
			require.ErrorContains(t, err, tt.errMessage)
		})
	}
}

func TestGetTopRUTargetRejectsMismatchedScope(t *testing.T) {
	tests := []struct {
		name       string
		body       string
		errMessage string
	}{
		{
			name: "organization",
			body: `{
  "baseUrl":"http://topru.internal",
  "headers":{"env":"staging","bizType":"staging-tidbcloud-nextgen","provider":"aws","region":"us-west-2","orgId":"999","clusterId":"cluster-a","deployType":"premium","deployTypeV2":"premium"}
}`,
			errMessage: "headers.orgId does not match",
		},
		{
			name: "cluster",
			body: `{
  "baseUrl":"http://topru.internal",
  "headers":{"env":"staging","bizType":"staging-tidbcloud-nextgen","provider":"aws","region":"us-west-2","orgId":"123","clusterId":"cluster-b","deployType":"premium","deployTypeV2":"premium"}
}`,
			errMessage: "headers.clusterId does not match",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
				_, _ = io.WriteString(w, tt.body)
			}))
			defer server.Close()

			client := NewClient(server.URL)
			target, err := client.GetTopRUTarget(context.Background(), 123, "cluster-a")
			require.Nil(t, target)
			require.ErrorContains(t, err, tt.errMessage)
		})
	}
}

type zeroReader struct{}

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