package planmanage

import (
	"errors"
	"fmt"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
	"github.com/tidbcloud/AgentTeam/internal/store/workflow/plan"
)

// TestMapRepositoryErrorNeverTrustsRepositoryProse proves every repository
// BadRequest/Conflict and adjacent store sentinel wrapper 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/As classification graph.
func TestMapRepositoryErrorNeverTrustsRepositoryProse(t *testing.T) {
	const secret = "REPO-SECRET-7f3a9c21"
	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: "idempotency conflict", err: plan.IdempotencyConflictError{Operation: secret},
			wantCode: ErrorCodeIdempotencyConflict,
		},
		{
			name: "draft revision conflict", err: plan.RevisionConflictError{Resource: "plan draft", Expected: 1, Actual: 2},
			wantCode: ErrorCodeDraftConflict,
		},
		{
			name: "plan revision conflict", err: plan.RevisionConflictError{Resource: "plan", Expected: 1, Actual: 2},
			wantCode: ErrorCodeRevisionConflict,
		},
		{
			name: "archived sentinel", err: fmt.Errorf("archive "+secret+": %w", plan.ErrArchived),
			wantCode: ErrorCodeArchived,
		},
		{
			name: "not found selector", err: storeerr.NotFound("missing Plan " + 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")
			require.False(t, errors.Is(mapped, storeerr.ErrBadRequest) && test.wantCode == ErrorCodeConflict,
				"an internal bad-request cause must never override the declared conflict kind")
		})
	}
}

// TestMapRepositoryErrorPreservesRevisionDetails proves the revision conflict
// numbers and resource stay in the structured details while the public message
// drops the repository-generated prose.
func TestMapRepositoryErrorPreservesRevisionDetails(t *testing.T) {
	mapped := mapRepositoryError(plan.RevisionConflictError{Resource: "plan", Expected: 3, Actual: 7})
	require.NotNil(t, mapped)
	code, details, ok := ErrorDetails(mapped)
	require.True(t, ok)
	require.Equal(t, ErrorCodeRevisionConflict, code)
	require.Equal(t, RevisionConflictDetails{Resource: "plan", ExpectedRevision: 3, CurrentRevision: 7}, details)
}

// TestMapMessageErrorNeverTrustsMessageStoreProse proves the plan agent-edit
// mapping converts message-store sentinel wrappers to reviewed stable store
// errors: a secret marker never becomes the public message, the message-store
// sentinel is still classified as the corresponding store kind, and the
// original sentinel remains reachable only through the separate InternalCause
// accessor (logs/wrapping), never through the errors.Is classification graph.
func TestMapMessageErrorNeverTrustsMessageStoreProse(t *testing.T) {
	const secret = "REPO-SECRET-agent-edit"
	tests := []struct {
		name       string
		err        error
		wantKind   error
		wantTarget error
	}{
		{name: "message not found", err: fmt.Errorf("%s: %w", secret, message.ErrNotFound), wantKind: storeerr.ErrNotFound, wantTarget: message.ErrNotFound},
		{name: "message forbidden", err: fmt.Errorf("%s: %w", secret, message.ErrForbidden), wantKind: storeerr.ErrForbidden, wantTarget: message.ErrForbidden},
		{name: "message conflict", err: fmt.Errorf("%s: %w", secret, message.ErrConflict), wantKind: storeerr.ErrConflict, wantTarget: message.ErrConflict},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			mapped := mapMessageError(test.err)
			require.NotNil(t, mapped)
			require.NotContains(t, mapped.Error(), secret, "message-store prose must never become the public message")
			require.True(t, errors.Is(mapped, test.wantKind), "mapped error must classify as the corresponding store kind")
			// The message-store sentinel 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 message-store sentinel for logging")
			require.True(t, errors.Is(cause, test.wantTarget), "preserved cause must retain the message-store sentinel")
		})
	}
}

// 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,
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			err := RequestError{Kind: test.kind, Code: test.code, Message: "Plan 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)
		})
	}
}
