package bindingmanage

import (
	"errors"
	"fmt"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
	"github.com/tidbcloud/AgentTeam/internal/store/workflow/binding"
	"github.com/tidbcloud/AgentTeam/internal/workflow/planmanage"
)

// TestMapRepositoryErrorNeverTrustsRepositoryProse proves every repository
// BadRequest/Conflict, invalid-state, revision, and idempotency sentinel maps
// to reviewed stable problem text. A secret marker embedded in the repository
// error never reaches the RequestError message, the stable domain code is
// preserved, and the original repository error remains reachable only through
// the separate InternalCause accessor (logs/wrapping), never through the
// public message or the errors.Is classification graph.
func TestMapRepositoryErrorNeverTrustsRepositoryProse(t *testing.T) {
	const secret = "REPO-SECRET-5c91f0a7"
	tests := []struct {
		name     string
		err      error
		wantCode ErrorCode
	}{
		{
			name: "generic bad request", err: storeerr.BadRequest("repository rejected input: " + secret),
			wantCode: ErrorCodeInvalidRequest,
		},
		{
			name: "generic conflict", err: storeerr.Conflict("repository CAS conflict: " + secret),
			wantCode: ErrorCodeConflict,
		},
		{
			name: "invalid state", err: fmt.Errorf("state "+secret+": %w", binding.ErrInvalidState),
			wantCode: ErrorCodeInvalidState,
		},
		{
			name: "idempotency conflict", err: binding.IdempotencyConflictError{Operation: secret},
			wantCode: ErrorCodeIdempotencyConflict,
		},
		{
			name: "revision conflict", err: binding.RevisionConflictError{Resource: "binding", Expected: 1, Actual: 2},
			wantCode: ErrorCodeRevisionConflict,
		},
		{
			name: "plan archived", err: binding.ErrPlanArchived,
			wantCode: ErrorCodePlanArchived,
		},
		{
			name: "binding archived", err: binding.ErrArchived,
			wantCode: ErrorCodeArchived,
		},
		{
			name: "not found selector", err: storeerr.NotFound("missing Binding " + secret),
			wantCode: ErrorCodeNotFound,
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			mapped := mapRepositoryError(test.err)
			require.NotNil(t, mapped)
			require.NotContains(t, mapped.Error(), secret, "repository prose must never become the public message")
			code, _, ok := ErrorDetails(mapped)
			require.True(t, ok)
			require.Equal(t, test.wantCode, code)
			// The internal cause is retained only for logs/wrapping through the
			// separate accessor; it must not participate in the errors.Is graph.
			cause := storeerr.InternalCause(mapped)
			require.NotNil(t, cause, "mapped error must preserve the repository cause for logging")
			require.True(t, errors.Is(cause, test.err), "preserved cause must wrap the repository error")
		})
	}
}

// TestMapPlanErrorNeverTrustsRepositoryProse proves the Plan error mapping
// (used when resolving/inspecting a PlanVersion for a Binding) replaces the
// inner Plan error message with reviewed stable text even when the inner
// RequestError carries repository-derived prose.
func TestMapPlanErrorNeverTrustsRepositoryProse(t *testing.T) {
	const secret = "REPO-SECRET-plan-version"
	inner := planmanage.RequestError{
		Kind: storeerr.ErrBadRequest, Code: planmanage.ErrorCodeInvalidRequest, Message: secret,
	}
	mapped := mapPlanError(inner)
	require.NotNil(t, mapped)
	require.NotContains(t, mapped.Error(), secret, "inner Plan error prose must never become the public message")
	code, _, ok := ErrorDetails(mapped)
	require.True(t, ok)
	require.Equal(t, ErrorCodePlanVersionInvalid, code)
	// The inner Plan error is preserved for logs only through the separate
	// accessor; it must not participate in the errors.Is graph.
	cause := storeerr.InternalCause(mapped)
	require.NotNil(t, cause, "mapped error must preserve the Plan cause for logging")
	require.True(t, errors.Is(cause, inner), "preserved cause must wrap the Plan error")
}

// TestRequestErrorKindAuthoritativeAcrossMixedSentinels proves the declared
// manage RequestError Kind stays authoritative for public status/code even when
// the preserved Cause wraps a DIFFERENT store sentinel: a conflict RequestError
// whose internal cause is a bad-request or not-found must never be classified as
// 400/404 through errors.Is. The cause remains reachable only through the
// InternalCause accessor.
func TestRequestErrorKindAuthoritativeAcrossMixedSentinels(t *testing.T) {
	tests := []struct {
		name      string
		kind      error
		code      ErrorCode
		cause     error
		wantOther error
	}{
		{
			name: "conflict wraps bad request", kind: storeerr.ErrConflict, code: ErrorCodeConflict,
			cause: storeerr.BadRequest("internal repository bad request"), wantOther: storeerr.ErrBadRequest,
		},
		{
			name: "conflict wraps not found", kind: storeerr.ErrConflict, code: ErrorCodeConflict,
			cause: storeerr.NotFound("internal repository not found"), wantOther: storeerr.ErrNotFound,
		},
		{
			name: "not found wraps bad request", kind: storeerr.ErrNotFound, code: ErrorCodeNotFound,
			cause: storeerr.BadRequest("internal repository bad request"), wantOther: storeerr.ErrBadRequest,
		},
		{
			name: "forbidden wraps not found", kind: storeerr.ErrForbidden, code: ErrorCodeForbidden,
			cause: storeerr.NotFound("internal repository not found"), wantOther: storeerr.ErrNotFound,
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			err := RequestError{Kind: test.kind, Code: test.code, Message: "Binding request failed", Cause: test.cause}
			require.True(t, errors.Is(err, test.kind), "declared kind must match")
			require.False(t, errors.Is(err, test.wantOther), "internal cause sentinel must not participate in errors.Is")
			require.NotNil(t, storeerr.InternalCause(err))
			require.True(t, errors.Is(storeerr.InternalCause(err), test.wantOther), "InternalCause must retain the internal cause")
			code, _, ok := ErrorDetails(err)
			require.True(t, ok)
			require.Equal(t, test.code, code)
		})
	}
}
