package binding

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

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

	"github.com/tidbcloud/AgentTeam/internal/api"
	"github.com/tidbcloud/AgentTeam/internal/store/identity"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
	"github.com/tidbcloud/AgentTeam/internal/store/workspace"
	"github.com/tidbcloud/AgentTeam/internal/workflow/bindingmanage"
)

// TestBindingCrossTenantSelectorHTTPMatrix proves the real Binding (Trigger)
// HTTP handler projects an identical 404 not-found response for a missing
// Trigger selector and a cross-tenant/forbidden Trigger selector: same status,
// code, problem, details, and canonical fields. A caller cannot tell whether a
// protected Trigger exists in another tenant.
func TestBindingCrossTenantSelectorHTTPMatrix(t *testing.T) {
	gin.SetMode(gin.TestMode)

	missing := bindingSelectorHTTPResponse(t, bindingmanage.RequestError{
		Kind: storeerr.ErrNotFound, Code: bindingmanage.ErrorCodeNotFound, Message: "Workflow Binding bnd_secret is missing",
	})
	forbidden := bindingSelectorHTTPResponse(t, bindingmanage.RequestError{
		Kind: storeerr.ErrForbidden, Code: bindingmanage.ErrorCodeForbidden, Message: "Workflow Binding bnd_secret is forbidden",
	})

	require.Equal(t, http.StatusNotFound, missing.Code)
	require.Equal(t, missing.Code, forbidden.Code)
	require.JSONEq(t, missing.Body.String(), forbidden.Body.String())
	require.Contains(t, missing.Body.String(), `"code":"binding_not_found"`)
	require.NotContains(t, missing.Body.String(), "bnd_secret")
	require.NotContains(t, forbidden.Body.String(), "bnd_secret")
}

func bindingSelectorHTTPResponse(t *testing.T, selectorErr error) *httptest.ResponseRecorder {
	t.Helper()
	handlers := newRouteHandlers(&fakeBindingService{err: selectorErr})
	router := gin.New()
	router.GET("/workflow/bindings/:bindingId",
		func(ctx *gin.Context) {
			api.SetAuthenticatedUser(ctx, identity.AuthenticatedUser{User: identity.User{ID: "usr_1"}})
			api.SetCurrentWorkspace(ctx, workspace.Summary{WorkspaceID: "wsp_1", MemberID: "mem_1", MemberUserID: "usr_1", WorkspaceRole: "owner"})
		},
		handleWorkspace(handlers.detail.Get),
	)

	request := httptest.NewRequest(http.MethodGet, "/workflow/bindings/bnd_secret", http.NoBody)
	recorder := httptest.NewRecorder()
	router.ServeHTTP(recorder, request)

	var body map[string]any
	require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &body))
	require.Equal(t, "binding_not_found", body["code"])
	require.Equal(t, "binding", body["stage"])
	return recorder
}

// TestBindingCrossTenantSelectorServiceMatrix proves the real Binding classifier
// produces an identical api.ErrorResponse for missing and cross-tenant/forbidden
// protected Binding selectors at the service boundary, including the
// binding_plan_not_found selector code.
func TestBindingCrossTenantSelectorServiceMatrix(t *testing.T) {
	pairs := []struct {
		name       string
		missing    bindingmanage.ErrorCode
		forbidden  bindingmanage.ErrorCode
		wantCode   string
		wantKind   error
		wantStatus int
	}{
		{name: "binding selector", missing: bindingmanage.ErrorCodeNotFound, forbidden: bindingmanage.ErrorCodeForbidden, wantCode: "binding_not_found", wantKind: storeerr.ErrNotFound, wantStatus: http.StatusNotFound},
		{name: "binding plan selector", missing: bindingmanage.ErrorCodePlanNotFound, forbidden: bindingmanage.ErrorCodeForbidden, wantCode: "binding_not_found", wantKind: storeerr.ErrNotFound, wantStatus: http.StatusNotFound},
	}
	for _, pair := range pairs {
		t.Run(pair.name, func(t *testing.T) {
			missing, ok := classifyError(bindingmanage.RequestError{
				Kind: storeerr.ErrNotFound, Code: pair.missing, Message: "Workflow Binding bnd_secret is missing",
				Details: map[string]any{"bindingId": "bnd_secret"},
			})
			require.True(t, ok)
			forbidden, ok := classifyError(bindingmanage.RequestError{
				Kind: storeerr.ErrForbidden, Code: pair.forbidden, Message: "Workflow Binding bnd_secret is forbidden",
				Details: map[string]any{"bindingId": "bnd_secret"},
			})
			require.True(t, ok)

			require.Equal(t, pair.wantStatus, missing.Status)
			require.Equal(t, pair.wantStatus, forbidden.Status)
			require.Equal(t, missing.Code, forbidden.Code)
			require.Equal(t, pair.wantCode, missing.Code)
			require.Equal(t, missing.Message, forbidden.Message)
			require.Nil(t, missing.Details)
			require.Nil(t, forbidden.Details)
			require.Equal(t, missing.Stage, forbidden.Stage)
			require.Equal(t, "binding", missing.Stage)
			require.Equal(t, missing.Path, forbidden.Path)
			require.Equal(t, missing.Hint, forbidden.Hint)
			require.Equal(t, missing.Retryable, forbidden.Retryable)
			require.Equal(t, missing.Diagnostics, forbidden.Diagnostics)
			require.NotContains(t, missing.Message, "bnd_secret")
		})
	}
}

// TestBindingCrossTenantOwnerResolutionCollapses proves a 403 owner-resolution
// protection (which uses the protected binding_forbidden code) is externally
// identical to a missing Trigger selector, so an Agent cannot enumerate
// Triggers it cannot access.
func TestBindingCrossTenantOwnerResolutionCollapses(t *testing.T) {
	response, ok := classifyError(publicError(http.StatusForbidden, "binding_forbidden", "Agent has no active human execution owner in this workspace"))
	require.True(t, ok)
	require.Equal(t, http.StatusNotFound, response.Status)
	require.Equal(t, "binding_not_found", response.Code)
	require.Nil(t, response.Details)
}
