package rest_test

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

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

	"github.com/nexuscrm/backend/internal/interfaces/rest"
	"github.com/nexuscrm/backend/pkg/apierr"
	appErrors "github.com/nexuscrm/backend/pkg/errors"
)

// TestRespondError pins the service-error classification contract used
// by every record-mutation handler. The ordering rule below is the
// invariant that broke repeatedly before this helper existed (see PR
// #1184 gate review): InternalError must be classified BEFORE any
// errors.As(*apierr.Error) assertion, because *apierr.Error can appear
// as the cause of an InternalError and errors.As walks the Unwrap
// chain.
func TestRespondError(t *testing.T) {
	gin.SetMode(gin.TestMode)

	tests := []struct {
		name       string
		err        error
		wantHandle bool
		wantStatus int
		wantCode   string
	}{
		{
			name:       "nil error is not handled",
			err:        nil,
			wantHandle: false,
		},
		{
			name:       "plain InternalError classifies as 500",
			err:        appErrors.NewInternalError("db down", fmt.Errorf("connection refused")),
			wantHandle: true,
			wantStatus: http.StatusInternalServerError,
			wantCode:   apierr.SFCodeInternalError,
		},
		{
			// Load-bearing: InternalError wrapping an *apierr.Error
			// (e.g. tx-manager rollback envelope) must stay classified
			// as 500. Before this helper existed, the inline
			// errors.As(*apierr.Error) check in several handlers would
			// unwrap through InternalError and surface the infra
			// failure as a 403/400.
			name:       "InternalError wrapping apierr.Error still classifies as 500",
			err:        appErrors.NewInternalError("rollback", apierr.Forbidden("denied")),
			wantHandle: true,
			wantStatus: http.StatusInternalServerError,
			wantCode:   apierr.SFCodeInternalError,
		},
		{
			name:       "PermissionError classifies as 403",
			err:        appErrors.NewPermissionError("delete", "Account"),
			wantHandle: true,
			wantStatus: http.StatusForbidden,
			wantCode:   apierr.SFCodeForbidden,
		},
		{
			name:       "apierr.Error with 403 status classifies as 403",
			err:        apierr.Forbidden("FLS blocked"),
			wantHandle: true,
			wantStatus: http.StatusForbidden,
			wantCode:   apierr.SFCodeForbidden,
		},
		{
			name:       "apierr.Error with non-403 status is not handled (caller writes fallback)",
			err:        apierr.Invalid("bad field"),
			wantHandle: false,
		},
		{
			name:       "plain validation error is not handled (caller writes fallback)",
			err:        appErrors.NewValidationError("field", "is required"),
			wantHandle: false,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			w := httptest.NewRecorder()
			c, _ := gin.CreateTestContext(w)

			handled := rest.RespondError(c, tc.err)
			require.Equal(t, tc.wantHandle, handled)

			if !tc.wantHandle {
				assert.Equal(t, 0, w.Body.Len(), "caller-fallback path must not write a response body")
				return
			}

			assert.Equal(t, tc.wantStatus, w.Code)
			var body []map[string]any
			require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
			require.Len(t, body, 1, "SFErrorResponse returns a single-element array")
			assert.Equal(t, tc.wantCode, body[0]["errorCode"], "response body must carry the expected SF code")
		})
	}
}

func TestWriteError_PreservesExplicitSalesforcePayload(t *testing.T) {
	gin.SetMode(gin.TestMode)

	w := httptest.NewRecorder()
	c, _ := gin.CreateTestContext(w)

	rest.WriteError(c, apierr.NewSalesforce(http.StatusBadRequest, apierr.SFCodeInvalidCrossReferenceKey, "shadow permission set"), apierr.SFCodeInsertFailed)

	assert.Equal(t, http.StatusBadRequest, w.Code)

	var body []map[string]any
	require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
	require.Len(t, body, 1)
	assert.Equal(t, apierr.SFCodeInvalidCrossReferenceKey, body[0]["errorCode"])
	assert.Equal(t, "shadow permission set", body[0]["message"])
}
