package api

import (
	"errors"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

// TestErrorForObservationPreservesInternalCause proves the structured internal
// observation boundary preserves the repository/message-store cause
// carried by a store CauseAccessor (storeerr.wrappedError and every manage
// RequestError) so a repository failure stays diagnosable in logs/traces,
// while a plain error and an error without an internal cause keep the original
// error.
func TestErrorForObservationPreservesInternalCause(t *testing.T) {
	const secret = "REPO-SECRET-observation"
	repositoryCause := errors.New("repository rejected " + secret)

	t.Run("wrapped store error preserves internal cause", func(t *testing.T) {
		wrapped := storeerr.Wrapped(storeerr.ErrConflict, "Workflow mutation conflicts", repositoryCause)
		observed := errorForObservation(wrapped)
		require.Same(t, repositoryCause, observed, "the internal cause must be observed, not the stable wrapper")
		require.Contains(t, observed.Error(), secret)
	})

	t.Run("manage request error returns internal cause", func(t *testing.T) {
		// plan/manage RequestError implements storeerr.CauseAccessor via its
		// InternalCause method; storeerr.InternalCause walks the graph to it.
		request := storeerr.Wrapped(storeerr.ErrBadRequest, "Workflow request is invalid", repositoryCause)
		observed := errorForObservation(request)
		require.Same(t, repositoryCause, observed)
	})

	t.Run("plain error keeps original", func(t *testing.T) {
		plain := errors.New("database exploded")
		require.Same(t, plain, errorForObservation(plain))
	})

	t.Run("http error cause takes precedence then internal cause", func(t *testing.T) {
		httpCause := storeerr.Wrapped(storeerr.ErrConflict, "conflict", repositoryCause)
		httpErr := NewHTTPErrorWithCause(http.StatusInternalServerError, "Internal server error", httpCause)
		observed := errorForObservation(httpErr)
		require.Same(t, repositoryCause, observed, "HTTPError.Cause is unwrapped, then its internal cause is acked")
	})
}

// TestObservedErrorWithCauseSeparatesPublicAndInternal proves the structured
// logging helper returns the original public error (stable problem text) and
// the internal cause separately, so a structured log can record both without
// leaking the internal cause into the public JSON/prompt or re-entering
// errors.Is/As classification.
func TestObservedErrorWithCauseSeparatesPublicAndInternal(t *testing.T) {
	const secret = "REPO-SECRET-obs-sep"
	publicText := "Workflow mutation conflicts with the current state"
	internal := errors.New("repository rejected " + secret)
	err := storeerr.Wrapped(storeerr.ErrConflict, publicText, internal)

	public, cause := ObservedErrorWithCause(err)
	require.NotNil(t, public)
	require.NotNil(t, cause)
	require.Equal(t, publicText, public.Error(), "the public error keeps the reviewed stable problem text")
	require.Same(t, internal, cause)
	require.Contains(t, cause.Error(), secret)

	// An error with no internal cause yields public only.
	plain := errors.New("plain")
	public, cause = ObservedErrorWithCause(plain)
	require.Same(t, plain, public)
	require.Nil(t, cause)

	// Nil yields nil,nil.
	public, cause = ObservedErrorWithCause(nil)
	require.Nil(t, public)
	require.Nil(t, cause)
}

// TestObserveServerErrorHumanTransportCarriesInternalCause proves the Human
// HTTP observation boundary records the preserved internal repository cause in
// ObservedServerError (and hence the router/tracing span) for a 5xx workflow
// failure, while the public JSON body stays the stable reviewed problem text.
func TestObserveServerErrorHumanTransportCarriesInternalCause(t *testing.T) {
	const secret = "REPO-SECRET-human-obs"
	repositoryCause := errors.New("repository rejected " + secret)
	// A manage-style error: declared conflict Kind, stable public message,
	// preserved internal repository cause.
	managed := storeerr.Wrapped(storeerr.ErrConflict, "Workflow mutation conflicts with the current state", repositoryCause)

	recorder := httptest.NewRecorder()
	_, router := gin.CreateTestContext(recorder)
	var observed ObservedServerError
	var observedOK bool
	router.GET(
		"/test",
		seedWorkspaceScopedRequestContext(),
		HandleWorkspaceScoped(func(WorkspaceScopedRequest) (Response, error) {
			return Response{}, NewHTTPErrorWithCause(http.StatusInternalServerError, "Internal server error", managed)
		}),
		func(ctx *gin.Context) {
			observed, observedOK = ObservedServerErrorFromContext(ctx)
		},
	)

	router.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/test", nil))

	require.Equal(t, http.StatusInternalServerError, recorder.Code)
	// The public body is the stable problem text, never the repository secret.
	require.NotContains(t, recorder.Body.String(), secret)
	require.Contains(t, recorder.Body.String(), "Internal server error")
	// The structured observation returns the internal repository cause.
	require.True(t, observedOK)
	require.NotNil(t, observed.Err)
	require.Contains(t, observed.Err.Error(), secret, "the observed error must target the internal repository cause for diagnosis")
}
