package regionalapi

import (
	"encoding/json"
	"net/http"
	"testing"

	"github.com/pingcap/log"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/pkgv2/bizerr"
	"go.uber.org/zap"
	"go.uber.org/zap/zaptest/observer"
)

func TestErrorResponse2BizErrRedactsCredentials(t *testing.T) {
	const secret = "canary-regional-secret"
	const reason = "Cluster authorization failed"
	inner, _ := json.Marshal(map[string]string{
		"error_code": "CDC:ErrSinkURIInvalid",
		"error_msg":  "[CDC:ErrSinkURIInvalid]sink uri invalid 'kafka://broker/topic?protocol=debezium&sasl-password=" + secret + "': " + reason,
	})
	envelope, _ := json.Marshal(RegionalError{Code: 13, Message: "Failed to create changefeed: " + string(inner)})
	for name, body := range map[string][]byte{
		"regional envelope":   envelope,
		"kernel JSON":         inner,
		"plain text fallback": []byte("sink uri invalid 'kafka://broker/topic?sasl-password=" + secret + "': " + reason),
	} {
		t.Run(name, func(t *testing.T) {
			err := ErrorResponse2BizErr(http.StatusInternalServerError, body)
			assert.Equal(t, bizerr.ErrCodeUnknown, err.RawErrCode())
			assert.NotContains(t, err.Error(), secret)
			assert.Contains(t, err.ErrMsg(), reason)
		})
	}
	for _, body := range []string{`{"message":"password=` + secret, `{"password":"` + secret + `"}`} {
		err := ErrorResponse2BizErr(http.StatusBadRequest, []byte(body))
		assert.Equal(t, bizerr.ErrCodeParams, err.RawErrCode())
		assert.NotContains(t, err.Error(), secret)
	}
}

func TestErrorResponse2BizErr(t *testing.T) {
	testcases := []struct {
		code               int
		body               []byte
		isErr              bool
		expectedBizErrCode bizerr.ErrCode
		expectedBizErrMsg  string
	}{
		{
			code:  200,
			body:  []byte("string"),
			isErr: false,
		},
		{
			code:               400,
			body:               []byte("http status 400"),
			isErr:              true,
			expectedBizErrCode: bizerr.ErrCodeParams,
			expectedBizErrMsg:  "http status 400",
		},
		{
			code:               401,
			body:               []byte("http status 401"),
			isErr:              true,
			expectedBizErrCode: bizerr.ErrCodeAuth,
			expectedBizErrMsg:  "http status 401",
		},
		{
			code:               403,
			body:               []byte("http status 403"),
			isErr:              true,
			expectedBizErrCode: bizerr.ErrCodePermission,
			expectedBizErrMsg:  "http status 403",
		},
		{
			code:               404,
			body:               []byte("http status 404"),
			isErr:              true,
			expectedBizErrCode: bizerr.ErrCodeRecordNotFound,
			expectedBizErrMsg:  "http status 404",
		},
		{
			code:               408,
			body:               []byte("http status 408"),
			isErr:              true,
			expectedBizErrCode: bizerr.ErrCodeTimeout,
			expectedBizErrMsg:  "http status 408",
		},
		{
			code:               409,
			body:               []byte("http status 409"),
			isErr:              true,
			expectedBizErrCode: bizerr.ErrCodeRecordExist,
			expectedBizErrMsg:  "http status 409",
		},
		{
			code:               202,
			body:               []byte("http status 202"),
			isErr:              true,
			expectedBizErrCode: bizerr.ErrCodeUnknown,
			expectedBizErrMsg:  "http status 202",
		},
		{
			code:               302,
			body:               []byte("http status 302"),
			isErr:              true,
			expectedBizErrCode: bizerr.ErrCodeUnknown,
			expectedBizErrMsg:  "http status 302",
		},
		{
			code:               412,
			body:               []byte("http status 412"),
			isErr:              true,
			expectedBizErrCode: bizerr.ErrCodeUnknown,
			expectedBizErrMsg:  "http status 412",
		},
		{
			code:               500,
			body:               []byte("http status 500"),
			isErr:              true,
			expectedBizErrCode: bizerr.ErrCodeUnknown,
			expectedBizErrMsg:  "http status 500",
		},
		{
			code:               500,
			body:               []byte("{ \"code\": 500, \"message\": \"biz error\" }"),
			isErr:              true,
			expectedBizErrCode: bizerr.ErrCodeUnknown,
			expectedBizErrMsg:  "biz error",
		},
	}

	for _, tc := range testcases {
		bizErr := ErrorResponse2BizErr(tc.code, tc.body)
		if !tc.isErr {
			assert.NoError(t, bizErr)
		} else {
			assert.Error(t, bizErr)
			assert.Equal(t, bizErr.RawErrCode(), tc.expectedBizErrCode)
			assert.Equal(t, tc.expectedBizErrMsg, bizErr.ErrMsg())
		}
	}
}

func TestRegionalDecodeDiagnosticsDoNotLogInputValues(t *testing.T) {
	const inputValue = "987654321098765432109876543210"
	for _, tc := range []struct{ name, body, kind string }{
		{"numeric overflow", `{"code":` + inputValue + `}`, "type_mismatch"},
		{"invalid JSON", `{"message":"` + inputValue, "invalid_json"},
	} {
		t.Run(tc.name, func(t *testing.T) {
			core, entries := observer.New(zap.WarnLevel)
			restore := log.ReplaceGlobals(zap.New(core), &log.ZapProperties{})
			defer restore()
			var response RegionalError
			decodeErr := json.Unmarshal([]byte(tc.body), &response)
			require.Error(t, decodeErr)
			if tc.kind == "type_mismatch" {
				// The raw decoder error includes the original input value.
				require.Contains(t, decodeErr.Error(), inputValue)
			}
			require.Error(t, ErrorResponse2BizErr(http.StatusBadGateway, []byte(tc.body)))
			warnings := entries.FilterMessage("failed to unmarshal body").All()
			require.Len(t, warnings, 1)
			fields := warnings[0].ContextMap()
			assert.Equal(t, int64(http.StatusBadGateway), fields["code"])
			assert.Equal(t, tc.kind, fields["decode_error"])
			assert.Positive(t, fields["offset"])
			encoded, err := json.Marshal(fields)
			require.NoError(t, err)
			assert.NotContains(t, string(encoded), inputValue)
			assert.NotContains(t, fields, "body")
			assert.NotContains(t, fields, "error")
		})
	}
}
