package interceptor

import (
	"bufio"
	"io"
	"net"
	"net/http"
	"net/url"
	"strings"
	"testing"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/require"
	regionalpb "github.com/tidbcloud/cluster-service-ng/api/tidbcloud/dataplane-manager/v1"
	"google.golang.org/protobuf/encoding/protojson"
	"google.golang.org/protobuf/proto"
)

func TestRedactRequestBodyForLog(t *testing.T) {
	body := []byte(`{"user":"root","password":"db-password","s3":{"access_key_id":"ak","secretAccessKey":"sk"},"clusterId":"123"}`)
	redacted := redactRequestBodyForLog(body)

	for _, secret := range []string{"db-password", `"ak"`, `"sk"`} {
		require.NotContains(t, redacted, secret)
	}
	require.Contains(t, redacted, `"clusterId":"123"`)
	require.Contains(t, redacted, `"password":"***"`)
}

func TestRedactRequestBodyForLogNonJSON(t *testing.T) {
	body := []byte("password=db-password")
	require.Equal(t, "<non-JSON body omitted; size=20>", redactRequestBodyForLog(body))
}

func TestRedactRequestBodyForLogKongAuditSASToken(t *testing.T) {
	const sasToken = "sv=2024-01-01&ss=bf&srt=sco&sig=azure-sas-token"
	request := &regionalpb.SetKongAuditLogSettingRequest{
		Enabled:   true,
		MaxSize:   100,
		BucketUrl: "https://account.blob.core.windows.net/audit",
		Region:    "eastus",
		ProviderIdentity: &regionalpb.SetKongAuditLogSettingRequest_ProviderIdentity{
			CloudRoleIdentity: sasToken,
			ExternalId:        "safe-external-id",
		},
	}
	body, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(proto.Message(request))
	require.NoError(t, err)
	redacted := redactRequestBodyForLog(body)
	require.NotContains(t, redacted, sasToken)
	require.Contains(t, redacted, `"cloud_role_identity":"***"`)
	require.Contains(t, redacted, "account.blob.core.windows.net/audit")
	require.Contains(t, redacted, "safe-external-id")
}

func TestRedactQueryForLog(t *testing.T) {
	query := url.Values{
		"cluster_id": []string{"123"},
		"token":      []string{"query-token"},
		"api-key":    []string{"query-api-key"},
	}
	redacted := redactQueryForLog(query)

	require.Equal(t, []string{"123"}, redacted["cluster_id"])
	require.Equal(t, []string{"***"}, redacted["token"])
	require.Equal(t, []string{"***"}, redacted["api-key"])
	require.Equal(t, "query-token", query.Get("token"), "redaction must not mutate the request")
}

func TestRedactHeadersForLog(t *testing.T) {
	headers := http.Header{
		"Content-Type":  []string{"application/json"},
		"Authorization": []string{"Bearer auth-token"},
		"Cookie":        []string{"session=session-token"},
		"X-Api-Key":     []string{"api-key"},
	}
	redacted := redactHeadersForLog(headers)

	require.Equal(t, []string{"application/json"}, redacted["Content-Type"])
	require.Equal(t, []string{"***"}, redacted["Authorization"])
	require.Equal(t, []string{"***"}, redacted["Cookie"])
	require.Equal(t, []string{"***"}, redacted["X-Api-Key"])
	require.Equal(t, "Bearer auth-token", headers.Get("Authorization"), "redaction must not mutate the request")
}

func TestRedactRequestBodyForLogLimit(t *testing.T) {
	body := []byte(`{"value":"` + strings.Repeat("x", maxRequestLogEmbeddedJSONBytes) + `"}`)
	require.Equal(t, requestLogRedactedValue, redactRequestBodyForLog(body))
}

func TestCaptureRequestBodyForLogPreservesOversizedBody(t *testing.T) {
	original := strings.Repeat("x", maxRequestLogEmbeddedJSONBytes+1024)
	prefix, replayed := captureRequestBodyForLog(io.NopCloser(strings.NewReader(original)))
	replayedBody, err := io.ReadAll(replayed)
	require.NoError(t, err)
	require.Len(t, prefix, maxRequestLogEmbeddedJSONBytes+1)
	require.Equal(t, original, string(replayedBody))
}

type fakeResponseWriter struct {
	status int
}

var _ gin.ResponseWriter = &fakeResponseWriter{}

func (w *fakeResponseWriter) Header() http.Header {
	return http.Header{}
}

func (w *fakeResponseWriter) WriteHeader(code int) {
}

func (w *fakeResponseWriter) WriteHeaderNow() {
}

func (w *fakeResponseWriter) Write(data []byte) (n int, err error) {
	return 0, nil
}

func (w *fakeResponseWriter) WriteString(s string) (n int, err error) {
	return 0, nil
}

func (w *fakeResponseWriter) Status() int {
	return w.status
}
func (w *fakeResponseWriter) Size() int {
	return 0
}

func (w *fakeResponseWriter) Written() bool {
	return true
}

// Hijack implements the http.Hijacker interface.
func (w *fakeResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
	return nil, nil, nil
}

// CloseNotify implements the http.CloseNotify interface.
func (w *fakeResponseWriter) CloseNotify() <-chan bool {
	n := make(chan bool)
	return n
}

// Flush implements the http.Flush interface.
func (w *fakeResponseWriter) Flush() {
}

func (w *fakeResponseWriter) Pusher() (pusher http.Pusher) {
	return nil
}
