package middleware

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

	"github.com/gin-gonic/gin"
	"github.com/pingcap-inc/nexuscrm-api-host/hostruntime"
)

type testScopeSessionContext struct {
	lookup             SessionLookup
	writtenConnectedID string
}

func (ctx *testScopeSessionContext) ReadSession(*gin.Context) SessionLookup {
	return ctx.lookup
}

func (ctx *testScopeSessionContext) WriteConnectedAppID(_ *gin.Context, connectedAppID string) {
	ctx.writtenConnectedID = CanonicalConnectedAppID(connectedAppID)
}

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

	tests := []struct {
		name            string
		method          string
		route           string
		target          string
		scope           string
		connectedAppID  string
		authorizer      ScopeRouteAuthorizer
		wantStatus      int
		wantAuthorizer  bool
		wantConnectedID string
	}{
		{
			name:            "unscoped session keeps legacy full access",
			route:           "/services/data/:version/sobjects",
			target:          "/services/data/v63.0/sobjects",
			wantStatus:      http.StatusNoContent,
			wantConnectedID: "",
		},
		{
			name:            "api scope allows all methods",
			method:          http.MethodPost,
			route:           "/services/data/:version/sobjects",
			target:          "/services/data/v63.0/sobjects",
			scope:           "api",
			connectedAppID:  "app-1",
			wantStatus:      http.StatusNoContent,
			wantConnectedID: "app-1",
		},
		{
			name:            "api read allows read methods",
			route:           "/services/data/:version/query",
			target:          "/services/data/v63.0/query",
			scope:           "api.read",
			connectedAppID:  "app-1",
			wantStatus:      http.StatusNoContent,
			wantConnectedID: "app-1",
		},
		{
			name:   "api read cannot be broadened by injected policy",
			method: http.MethodPost,
			route:  "/product/notifications",
			target: "/product/notifications",
			scope:  "api.read",
			authorizer: ScopeRouteAuthorizerFunc(func(ScopeRouteRequest) bool {
				return true
			}),
			wantStatus: http.StatusForbidden,
		},
		{
			name:            "openid userinfo stays host neutral",
			route:           "/services/oauth2/userinfo",
			target:          "/services/oauth2/userinfo",
			scope:           "openid",
			connectedAppID:  "app-1",
			wantStatus:      http.StatusNoContent,
			wantConnectedID: "app-1",
		},
		{
			name:       "custom scope denied without provider coverage",
			route:      "/product/notifications",
			target:     "/product/notifications",
			scope:      "product.notifications",
			wantStatus: http.StatusForbidden,
		},
		{
			name:           "custom scope delegates route decision to provider",
			route:          "/product/notifications",
			target:         "/product/notifications",
			scope:          " Product.Notifications ",
			connectedAppID: " app-1 ",
			authorizer: ScopeRouteAuthorizerFunc(func(req ScopeRouteRequest) bool {
				return req.Scope == "product.notifications" &&
					req.ConnectedAppID == "app-1" &&
					req.Method == http.MethodGet &&
					req.FullPath == "/product/notifications"
			}),
			wantStatus:      http.StatusNoContent,
			wantAuthorizer:  true,
			wantConnectedID: "app-1",
		},
		{
			name:   "openid can be extended by provider for product identity routes",
			route:  "/id/:orgId/:userId",
			target: "/id/00D000000000001/005000000000123AAA",
			scope:  "openid",
			authorizer: ScopeRouteAuthorizerFunc(func(req ScopeRouteRequest) bool {
				return req.Scope == "openid" && strings.HasPrefix(req.FullPath, "/id/")
			}),
			wantStatus:     http.StatusNoContent,
			wantAuthorizer: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := &testScopeSessionContext{
				lookup: SessionLookup{
					User: hostruntime.UserSession{
						ID:             "005000000000123AAA",
						Scope:          tt.scope,
						ConnectedAppID: tt.connectedAppID,
					},
					Exists: true,
					Valid:  true,
				},
			}
			authorizerCalled := false
			authorizer := tt.authorizer
			if authorizer != nil {
				authorizer = ScopeRouteAuthorizerFunc(func(req ScopeRouteRequest) bool {
					authorizerCalled = true
					return tt.authorizer.AllowsScopedRoute(req)
				})
			}

			router := gin.New()
			router.Use(ScopeEnforcerWithSessionContext(ctx, authorizer))
			handler := func(c *gin.Context) {
				c.Status(http.StatusNoContent)
			}
			router.GET(tt.route, handler)
			router.POST(tt.route, handler)

			method := tt.method
			if method == "" {
				method = http.MethodGet
			}
			rec := httptest.NewRecorder()
			router.ServeHTTP(rec, httptest.NewRequest(method, tt.target, nil))

			if rec.Code != tt.wantStatus {
				t.Fatalf("status = %d, want %d, body = %s", rec.Code, tt.wantStatus, rec.Body.String())
			}
			if authorizerCalled != tt.wantAuthorizer {
				t.Fatalf("authorizerCalled = %v, want %v", authorizerCalled, tt.wantAuthorizer)
			}
			if ctx.writtenConnectedID != tt.wantConnectedID {
				t.Fatalf("writtenConnectedID = %q, want %q", ctx.writtenConnectedID, tt.wantConnectedID)
			}
		})
	}
}
