package errorcode

import (
	"errors"
	"fmt"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/pkgv2/bizerr"
)

// fakeSQLErr implements ClusterSQLErrorHolder for testing without importing the
// facade package (which would create an import cycle).
type fakeSQLErr struct {
	bizerr.BizError
	errno uint16
	msg   string
}

func (e *fakeSQLErr) Error() string { return fmt.Sprintf("sql error %d: %s", e.errno, e.msg) }
func (e *fakeSQLErr) ClusterSQLError() (uint16, string) {
	return e.errno, e.msg
}

func markUserActionable(err *fakeSQLErr) bizerr.BizError {
	err.BizError = bizerr.New(ErrCodeSvcClusterProxy, "cluster SQL error")
	return MarkClusterSQLErrorUserActionable(err)
}

func TestMarkClusterSQLErrorUserActionablePreservesErrorChain(t *testing.T) {
	raw := &fakeSQLErr{
		BizError: bizerr.New(ErrCodeSvcClusterProxy, "cluster SQL error"),
		errno:    1146,
		msg:      "table not found",
	}

	marked := MarkClusterSQLErrorUserActionable(raw)
	require.ErrorIs(t, marked, raw)
	assert.Same(t, marked, MarkClusterSQLErrorUserActionable(marked))
	assert.Nil(t, MarkClusterSQLErrorUserActionable(nil))
}

func TestFormatClusterSQLError_Whitelisted(t *testing.T) {
	err := &fakeSQLErr{errno: 1146, msg: "Table 'db1.orders' doesn't exist"}

	be, ok := FormatClusterSQLError(markUserActionable(err))
	assert.True(t, ok)
	assert.Equal(t, ErrCodeClusterSQLError, be.RawErrCode())
	assert.Equal(t, "The requested table does not exist. (error 1146)", be.ErrMsg())
}

func TestFormatClusterSQLError_WhitelistedDoesNotExposeRawMessage(t *testing.T) {
	// Duplicate-key and invalid-value errors may carry customer data.
	err := &fakeSQLErr{
		errno: 1062,
		msg:   "Duplicate entry 'customer@example.com' for key 'email'",
	}

	be, ok := FormatClusterSQLError(markUserActionable(err))
	assert.True(t, ok)
	assert.Equal(t, ErrCodeClusterSQLError, be.RawErrCode())
	assert.Equal(t, "A row with the same unique key already exists. (error 1062)", be.ErrMsg())
	assert.NotContains(t, be.ErrMsg(), "customer@example.com")
	assert.NotContains(t, be.ErrMsg(), "email")
}

func TestFormatClusterSQLError_UserActionableSyntaxError(t *testing.T) {
	err := &fakeSQLErr{
		errno: 1064,
		msg:   "syntax error near SELECT secret FROM private_table",
	}

	be, ok := FormatClusterSQLError(markUserActionable(err))
	assert.True(t, ok)
	assert.Equal(t, ErrCodeClusterSQLError, be.RawErrCode())
	assert.Equal(t, "The SQL statement is invalid. (error 1064)", be.ErrMsg())
	assert.NotContains(t, be.ErrMsg(), "SELECT")
	assert.NotContains(t, be.ErrMsg(), "private_table")
}

func TestFormatClusterSQLError_UserActionableInvalidDatabaseName(t *testing.T) {
	err := &fakeSQLErr{
		errno: 1102,
		msg:   "Incorrect database name 'private_database_name'",
	}

	be, ok := FormatClusterSQLError(markUserActionable(err))
	assert.True(t, ok)
	assert.Equal(t, ErrCodeClusterSQLError, be.RawErrCode())
	assert.Equal(t, "The database name is invalid. (error 1102)", be.ErrMsg())
	assert.NotContains(t, be.ErrMsg(), "private_database_name")
}

func TestFormatClusterSQLError_NotWhitelisted(t *testing.T) {
	// 1045 access denied here means the internal proxy account failed to auth.
	err := &fakeSQLErr{
		errno: 1045,
		msg:   "Access denied for user '44PVranqo8xFd4S.mashen_4DiUfGS7'@'10.0.190.123' (using password: YES)",
	}

	be, ok := FormatClusterSQLError(err)
	assert.True(t, ok)
	assert.Equal(t, ErrCodeInternalError, be.RawErrCode())
	// Error number kept for triage, but no internal account/IP leaked.
	assert.Contains(t, be.ErrMsg(), "error 1045")
	assert.NotContains(t, be.ErrMsg(), "44PVranqo8xFd4S")
	assert.NotContains(t, be.ErrMsg(), "10.0.190.123")
}

func TestFormatClusterSQLError_SyntaxErrorIsInternal(t *testing.T) {
	// The service constructs the SQL sent through this facade. Returning its
	// text would disclose an internal statement and incorrectly blame the user.
	err := &fakeSQLErr{errno: 1064, msg: "syntax error near SELECT secret FROM internal_table"}

	be, ok := FormatClusterSQLError(err)
	assert.True(t, ok)
	assert.Equal(t, ErrCodeInternalError, be.RawErrCode())
	assert.Contains(t, be.ErrMsg(), "error 1064")
	assert.NotContains(t, be.ErrMsg(), "SELECT")
	assert.NotContains(t, be.ErrMsg(), "internal_table")
}

func TestFormatClusterSQLError_UnmarkedUserErrnoIsInternal(t *testing.T) {
	// The same errno can come from service-owned SQL, so errno alone must not
	// change a response to 4xx.
	err := &fakeSQLErr{errno: 1146, msg: "Table 'mysql.internal' doesn't exist"}

	be, ok := FormatClusterSQLError(err)
	assert.True(t, ok)
	assert.Equal(t, ErrCodeInternalError, be.RawErrCode())
	assert.Contains(t, be.ErrMsg(), "error 1146")
	assert.NotContains(t, be.ErrMsg(), "mysql.internal")
}

func TestFormatClusterSQLError_NonClusterError(t *testing.T) {
	_, ok := FormatClusterSQLError(errors.New("some random error"))
	assert.False(t, ok)
}

func TestFormatClusterSQLError_Nil(t *testing.T) {
	be, ok := FormatClusterSQLError(nil)
	assert.False(t, ok)
	assert.Nil(t, be)
}

func TestFormatClusterSQLError_ZeroErrno(t *testing.T) {
	_, ok := FormatClusterSQLError(&fakeSQLErr{errno: 0, msg: ""})
	assert.False(t, ok)
}
