package middleware

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

	"github.com/gin-gonic/gin"

	"github.com/pingcap-inc/nexuscrm-api-host/hostruntime"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/permissionstatev1"
)

type authorizingAdmitterStub struct {
	request   hostruntime.AuthorizingSessionRequest
	admission hostruntime.AuthorizingSessionAdmission
	err       error
	touches   []string
	calls     int
}

func (stub *authorizingAdmitterStub) AdmitAuthorizingSession(
	_ context.Context,
	request hostruntime.AuthorizingSessionRequest,
) (hostruntime.AuthorizingSessionAdmission, error) {
	stub.calls++
	stub.request = request
	return stub.admission, stub.err
}

func (stub *authorizingAdmitterStub) TouchSession(_ context.Context, sessionID string) {
	stub.touches = append(stub.touches, sessionID)
}

type authorizingSnapshotProviderStub struct {
	snapshot permissionstatev1.EffectiveSnapshot
	err      error
	calls    int
}

func (stub *authorizingSnapshotProviderStub) LoadCurrentEffectivePermissionSnapshot(
	context.Context,
	permissionstatev1.Subject,
) (permissionstatev1.EffectiveSnapshot, error) {
	stub.calls++
	return stub.snapshot, stub.err
}

type authorizingSessionWriterStub struct {
	user      hostruntime.UserSession
	sessionID string
	calls     int
}

func (stub *authorizingSessionWriterStub) WriteSession(
	_ *gin.Context,
	user hostruntime.UserSession,
	sessionID string,
) {
	stub.user = user
	stub.sessionID = sessionID
	stub.calls++
}

type authorizingPermissionContextStub struct {
	user   hostruntime.UserSession
	system map[string]bool
	custom map[string]bool
}

func (*authorizingPermissionContextStub) ReadSession(*gin.Context) SessionLookup {
	return SessionLookup{}
}

func (stub *authorizingPermissionContextStub) WriteCurrentSession(
	_ *gin.Context,
	user hostruntime.UserSession,
) {
	stub.user = user
}

func (stub *authorizingPermissionContextStub) WritePermissions(
	_ *gin.Context,
	systemPermissions map[string]bool,
	customPermissions map[string]bool,
) {
	stub.system = systemPermissions
	stub.custom = customPermissions
}

func (*authorizingPermissionContextStub) SystemPermissions(*gin.Context) PermissionLookup {
	return PermissionLookup{}
}

func (*authorizingPermissionContextStub) RequestID(*gin.Context) string {
	return ""
}

func TestAuthorizingSessionMiddlewareInstallsFloorQualifiedAggregate(t *testing.T) {
	gin.SetMode(gin.TestMode)
	subject := testPermissionStateSubject(t)
	version := testPermissionStateVersion(t, 1, 4)
	snapshot, err := permissionstatev1.NewEffectiveSnapshot(
		version,
		[]string{"ApiEnabled"},
		[]string{"ViewRevenue"},
	)
	if err != nil {
		t.Fatal(err)
	}
	admitter := &authorizingAdmitterStub{
		admission: testAuthorizingAdmission(subject, version),
	}
	provider := &authorizingSnapshotProviderStub{snapshot: snapshot}
	sessionWriter := &authorizingSessionWriterStub{}
	permissionContext := &authorizingPermissionContextStub{}
	middleware := newTestAuthorizingSessionMiddleware(
		t,
		admitter,
		provider,
		sessionWriter,
		permissionContext,
	)
	router := gin.New()
	router.GET("/", middleware.RequireAuth(), func(c *gin.Context) {
		state, ok := hostruntime.AuthorizingPermissionStateFromContext(c.Request.Context())
		if !ok || state.Snapshot.Version().Revision() != 4 {
			t.Fatalf("request Permission State = %#v, %t", state, ok)
		}
		c.Status(http.StatusNoContent)
	})
	response := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/", nil)
	request.Header.Set(headerAuthorization, "Bearer strong-token")
	router.ServeHTTP(response, request)

	if response.Code != http.StatusNoContent {
		t.Fatalf("status = %d, want 204", response.Code)
	}
	if admitter.request.Mode != hostruntime.AuthorizingSessionStrict ||
		admitter.request.BearerToken != "strong-token" {
		t.Fatalf("admission request = %#v", admitter.request)
	}
	if provider.calls != 1 || sessionWriter.calls != 1 ||
		sessionWriter.sessionID != "session-1" {
		t.Fatalf(
			"provider/session calls = %d/%d session=%q",
			provider.calls,
			sessionWriter.calls,
			sessionWriter.sessionID,
		)
	}
	if !sessionWriter.user.SystemPermissions["ApiEnabled"] ||
		!permissionContext.custom["ViewRevenue"] ||
		len(admitter.touches) != 1 {
		t.Fatal("resolved aggregate was not installed before the handler")
	}
}

func TestAuthorizingSessionMiddlewareSelectsRenewalOnlyBehindResponseGuard(t *testing.T) {
	gin.SetMode(gin.TestMode)
	subject := testPermissionStateSubject(t)
	version := testPermissionStateVersion(t, 1, 1)
	admitter := &authorizingAdmitterStub{
		admission: testAuthorizingAdmission(subject, version),
	}
	middleware := newTestAuthorizingSessionMiddleware(
		t,
		admitter,
		&authorizingSnapshotProviderStub{
			snapshot: testPermissionStateSnapshot(t, version, "ApiEnabled"),
		},
		&authorizingSessionWriterStub{},
		&authorizingPermissionContextStub{},
	)
	router := gin.New()
	router.GET("/", middleware.RequireAuth(), func(c *gin.Context) {
		c.Status(http.StatusNoContent)
	})
	response := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/", nil)
	request.Header.Set(headerAuthorization, "Bearer renewable-token")
	ProtectSessionCredentialResponses(router).ServeHTTP(response, request)

	if response.Code != http.StatusNoContent ||
		admitter.request.Mode != hostruntime.AuthorizingSessionRenewal {
		t.Fatalf(
			"status/mode = %d/%d, want 204/%d",
			response.Code,
			admitter.request.Mode,
			hostruntime.AuthorizingSessionRenewal,
		)
	}
}

func TestAuthorizingSessionMiddlewareCarriesOnlyAdmittedControlProof(t *testing.T) {
	gin.SetMode(gin.TestMode)
	subject := testPermissionStateSubject(t)
	version := testPermissionStateVersion(t, 1, 1)
	admitter := &authorizingAdmitterStub{
		admission: testAuthorizingAdmission(subject, version),
	}
	middleware := newTestAuthorizingSessionMiddleware(
		t,
		admitter,
		&authorizingSnapshotProviderStub{
			snapshot: testPermissionStateSnapshot(t, version, "ApiEnabled"),
		},
		&authorizingSessionWriterStub{},
		&authorizingPermissionContextStub{},
	)
	router := gin.New()
	router.GET("/", middleware.RequireAuth(), func(c *gin.Context) {
		c.Status(http.StatusNoContent)
	})
	response := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/", nil)
	request.Header.Set(headerAuthorization, "Bearer controlled-token")
	proof := "0123456789abcdef0123456789abcdef"
	ctx := context.WithValue(
		request.Context(),
		sessionControlProofContextKey{},
		sessionControlRequestState{admitted: true, proof: proof},
	)
	ProtectSessionCredentialResponses(router).ServeHTTP(
		response,
		request.WithContext(ctx),
	)

	if response.Code != http.StatusNoContent ||
		admitter.request.Mode != hostruntime.AuthorizingSessionControl ||
		admitter.request.ControlProof != proof {
		t.Fatalf("status/request = %d/%#v", response.Code, admitter.request)
	}
}

func TestOptionalAuthorizingSessionOnlyCredentialRejectionMayBecomeAnonymous(t *testing.T) {
	gin.SetMode(gin.TestMode)
	tests := []struct {
		name       string
		err        error
		wantStatus int
		wantNext   bool
	}{
		{
			name:       "credential rejected",
			err:        hostruntime.ErrAuthorizingCredentialRejected,
			wantStatus: http.StatusNoContent,
			wantNext:   true,
		},
		{
			name:       "unknown admission failure",
			err:        errors.New("database unavailable"),
			wantStatus: http.StatusServiceUnavailable,
		},
		{
			name: "classified authority failure",
			err: &permissionstatev1.Failure{
				Category: permissionstatev1.FailureAuthorityUnavailable,
			},
			wantStatus: http.StatusServiceUnavailable,
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			subject := testPermissionStateSubject(t)
			version := testPermissionStateVersion(t, 1, 1)
			admitter := &authorizingAdmitterStub{
				admission: testAuthorizingAdmission(subject, version),
				err:       test.err,
			}
			provider := &authorizingSnapshotProviderStub{
				snapshot: testPermissionStateSnapshot(t, version, "ApiEnabled"),
			}
			middleware := newTestAuthorizingSessionMiddleware(
				t,
				admitter,
				provider,
				&authorizingSessionWriterStub{},
				&authorizingPermissionContextStub{},
			)
			next := false
			router := gin.New()
			router.GET("/", middleware.OptionalAuth(), func(c *gin.Context) {
				next = true
				c.Status(http.StatusNoContent)
			})
			response := httptest.NewRecorder()
			request := httptest.NewRequest(http.MethodGet, "/", nil)
			request.Header.Set(headerAuthorization, "Bearer supplied-token")
			router.ServeHTTP(response, request)

			if response.Code != test.wantStatus || next != test.wantNext {
				t.Fatalf(
					"status/next = %d/%t, want %d/%t",
					response.Code,
					next,
					test.wantStatus,
					test.wantNext,
				)
			}
			if provider.calls != 0 {
				t.Fatalf("snapshot provider calls = %d, want 0", provider.calls)
			}
		})
	}
}

func TestOptionalAuthorizingSessionRejectsUnadmittedControlAttempt(t *testing.T) {
	gin.SetMode(gin.TestMode)
	subject := testPermissionStateSubject(t)
	version := testPermissionStateVersion(t, 1, 1)
	admitter := &authorizingAdmitterStub{
		admission: testAuthorizingAdmission(subject, version),
	}
	provider := &authorizingSnapshotProviderStub{
		snapshot: testPermissionStateSnapshot(t, version, "ApiEnabled"),
	}
	middleware := newTestAuthorizingSessionMiddleware(
		t,
		admitter,
		provider,
		&authorizingSessionWriterStub{},
		&authorizingPermissionContextStub{},
	)
	next := false
	router := gin.New()
	router.GET("/", middleware.OptionalAuth(), func(c *gin.Context) {
		next = true
		c.Status(http.StatusNoContent)
	})
	response := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/", nil)
	request.Header.Set(headerAuthorization, "Bearer supplied-token")
	request = request.WithContext(context.WithValue(
		request.Context(),
		sessionControlProofContextKey{},
		sessionControlRequestState{},
	))
	router.ServeHTTP(response, request)

	if response.Code != http.StatusUnauthorized || next ||
		admitter.calls != 0 || provider.calls != 0 {
		t.Fatalf(
			"status/next/admission/snapshot = %d/%t/%d/%d, want 401/false/0/0",
			response.Code,
			next,
			admitter.calls,
			provider.calls,
		)
	}
}

func TestAuthorizingSessionMiddlewareRejectsStaleAggregateBeforeSessionInstall(t *testing.T) {
	gin.SetMode(gin.TestMode)
	subject := testPermissionStateSubject(t)
	floor := testPermissionStateVersion(t, 1, 2)
	stale := testPermissionStateSnapshot(
		t,
		testPermissionStateVersion(t, 1, 1),
		"ApiEnabled",
	)
	sessionWriter := &authorizingSessionWriterStub{}
	middleware := newTestAuthorizingSessionMiddleware(
		t,
		&authorizingAdmitterStub{admission: testAuthorizingAdmission(subject, floor)},
		&authorizingSnapshotProviderStub{snapshot: stale},
		sessionWriter,
		&authorizingPermissionContextStub{},
	)
	next := false
	router := gin.New()
	router.GET("/", middleware.RequireAuth(), func(c *gin.Context) {
		next = true
		c.Status(http.StatusNoContent)
	})
	response := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/", nil)
	request.Header.Set(headerAuthorization, "Bearer strong-token")
	router.ServeHTTP(response, request)

	if response.Code != http.StatusServiceUnavailable || next || sessionWriter.calls != 0 {
		t.Fatalf(
			"status/next/session writes = %d/%t/%d, want 503/false/0",
			response.Code,
			next,
			sessionWriter.calls,
		)
	}
}

func TestNewAuthorizingSessionMiddlewareRequiresStrongCapabilities(t *testing.T) {
	cache, err := NewPermissionStateCache(DefaultPermissionStateCacheTTL)
	if err != nil {
		t.Fatal(err)
	}
	subject := testPermissionStateSubject(t)
	version := testPermissionStateVersion(t, 1, 1)
	valid := AuthorizingSessionMiddlewareOptions{
		Admitter: &authorizingAdmitterStub{
			admission: testAuthorizingAdmission(subject, version),
		},
		SnapshotProvider: &authorizingSnapshotProviderStub{
			snapshot: testPermissionStateSnapshot(t, version, "ApiEnabled"),
		},
		SnapshotCache:     cache,
		SessionWriter:     &authorizingSessionWriterStub{},
		PermissionContext: &authorizingPermissionContextStub{},
	}
	tests := []struct {
		name string
		edit func(*AuthorizingSessionMiddlewareOptions)
	}{
		{"strong admitter", func(options *AuthorizingSessionMiddlewareOptions) {
			options.Admitter = nil
		}},
		{"aggregate provider", func(options *AuthorizingSessionMiddlewareOptions) {
			options.SnapshotProvider = nil
		}},
		{"configured cache", func(options *AuthorizingSessionMiddlewareOptions) {
			options.SnapshotCache = &PermissionStateCache{}
		}},
		{"session writer", func(options *AuthorizingSessionMiddlewareOptions) {
			options.SessionWriter = nil
		}},
		{"permission context", func(options *AuthorizingSessionMiddlewareOptions) {
			options.PermissionContext = nil
		}},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			options := valid
			test.edit(&options)
			if _, err := NewAuthorizingSessionMiddleware(options); err == nil {
				t.Fatalf("constructor accepted composition without %s", test.name)
			}
		})
	}
}

func newTestAuthorizingSessionMiddleware(
	t *testing.T,
	admitter hostruntime.AuthorizingSessionAdmitter,
	provider hostruntime.EffectivePermissionSnapshotProvider,
	sessionWriter SessionContextWriter,
	permissionContext PermissionSessionContext,
) *AuthorizingSessionMiddleware {
	t.Helper()
	cache, err := NewPermissionStateCache(DefaultPermissionStateCacheTTL)
	if err != nil {
		t.Fatal(err)
	}
	middleware, err := NewAuthorizingSessionMiddleware(
		AuthorizingSessionMiddlewareOptions{
			Admitter:          admitter,
			SnapshotProvider:  provider,
			SnapshotCache:     cache,
			SessionWriter:     sessionWriter,
			PermissionContext: permissionContext,
		},
	)
	if err != nil {
		t.Fatal(err)
	}
	return middleware
}

func testAuthorizingAdmission(
	subject permissionstatev1.Subject,
	floor permissionstatev1.Version,
) hostruntime.AuthorizingSessionAdmission {
	return hostruntime.AuthorizingSessionAdmission{
		Claims: &hostruntime.SessionClaims{
			SessionID: "session-1",
			User: hostruntime.UserSession{
				ID:        subject.UserID(),
				ProfileID: subject.ProfileID(),
			},
		},
		Subject:         subject,
		PermissionFloor: floor,
	}
}
