package records

import (
	"context"
	"reflect"
	"testing"
	"time"

	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
)

func TestHandlePostUpdateSessionInvalidations(t *testing.T) {
	tests := []struct {
		name            string
		objectName      string
		id              string
		oldRecord       models.SObject
		effectiveUpdate models.SObject
		wantUsers       []string
		wantProfiles    []string
		wantRoles       []string
		wantApps        []string
		wantHookApps    []string
		wantDeactivated []string
	}{
		{
			name:       "user deactivation revokes user sessions and calls hook",
			objectName: constants.TableUser,
			id:         "user-1",
			oldRecord: models.SObject{
				constants.FieldIsActive: true,
			},
			effectiveUpdate: models.SObject{
				constants.FieldIsActive: false,
			},
			wantUsers:       []string{"user-1"},
			wantDeactivated: []string{"user-1"},
		},
		{
			name:       "user profile reassignment revokes user sessions",
			objectName: constants.TableUser,
			id:         "user-2",
			oldRecord: models.SObject{
				constants.FieldProfileID: "profile-old",
			},
			effectiveUpdate: models.SObject{
				constants.FieldProfileID: "profile-new",
			},
			wantUsers: []string{"user-2"},
		},
		{
			name:       "user role reassignment revokes user sessions",
			objectName: constants.TableUser,
			id:         "user-3",
			oldRecord: models.SObject{
				constants.FieldUserRoleID: "role-old",
			},
			effectiveUpdate: models.SObject{
				constants.FieldUserRoleID: "role-new",
			},
			wantUsers: []string{"user-3"},
		},
		{
			name:       "profile rename revokes profile sessions",
			objectName: constants.TableProfile,
			id:         "profile-1",
			oldRecord: models.SObject{
				constants.FieldName: "Old Profile",
			},
			effectiveUpdate: models.SObject{
				constants.FieldName: "New Profile",
			},
			wantProfiles: []string{"profile-1"},
		},
		{
			name:       "user role rename revokes role sessions",
			objectName: constants.TableUserRole,
			id:         "role-1",
			oldRecord: models.SObject{
				constants.FieldName: "Old Role",
			},
			effectiveUpdate: models.SObject{
				constants.FieldName: "New Role",
			},
			wantRoles: []string{"role-1"},
		},
		{
			name:       "connected app security update revokes app sessions and invalidates cache",
			objectName: constants.TableConnectedApp,
			id:         "app-1",
			effectiveUpdate: models.SObject{
				constants.ColSysConnectedApp_Scopes: "api refresh_token",
			},
			wantApps:     []string{"app-1"},
			wantHookApps: []string{"app-1"},
		},
		{
			name:       "connected app non security update does not revoke",
			objectName: constants.TableConnectedApp,
			id:         "app-2",
			effectiveUpdate: models.SObject{
				constants.FieldName: "Renamed only",
			},
		},
		{
			name:       "connected app access update revokes parent app sessions",
			objectName: constants.TableConnectedAppAccess,
			id:         "access-1",
			oldRecord: models.SObject{
				constants.ColSysConnectedAppAccess_ConnectedAppID: "app-parent",
			},
			effectiveUpdate: models.SObject{
				constants.FieldName: "updated grant",
			},
			wantApps: []string{"app-parent"},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			sessionRepo := &postUpdateSessionRepoRecorder{}
			deactivationHook := &postUpdateDeactivationHookRecorder{}
			appHook := &postUpdateConnectedAppHookRecorder{}
			ps := &PersistenceService{
				sessionRepo:      sessionRepo,
				deactivationHook: deactivationHook,
				connectedAppHook: appHook,
			}

			if err := ps.handlePostUpdateSessionInvalidations(context.Background(), tc.objectName, tc.id, tc.oldRecord, tc.effectiveUpdate); err != nil {
				t.Fatalf("handlePostUpdateSessionInvalidations() error = %v", err)
			}
			assertStringSliceEqual(t, "user revokes", sessionRepo.userIDs, tc.wantUsers)
			assertStringSliceEqual(t, "profile revokes", sessionRepo.profileIDs, tc.wantProfiles)
			assertStringSliceEqual(t, "role revokes", sessionRepo.roleIDs, tc.wantRoles)
			assertStringSliceEqual(t, "app revokes", sessionRepo.appIDs, tc.wantApps)
			assertStringSliceEqual(t, "app hook", appHook.appIDs, tc.wantHookApps)
			assertStringSliceEqual(t, "deactivation hook", deactivationHook.userIDs, tc.wantDeactivated)
		})
	}
}

func TestConnectedAppSessionInvalidationRequired(t *testing.T) {
	tests := []struct {
		name   string
		before map[string]any
		after  map[string]any
		want   bool
	}{
		{
			name:   "security policy changed",
			before: map[string]any{constants.ColSysConnectedApp_Scopes: "api"},
			after:  map[string]any{constants.ColSysConnectedApp_Scopes: "api refresh_token"},
			want:   true,
		},
		{
			name:   "presentation only changed",
			before: map[string]any{constants.FieldName: "Before"},
			after:  map[string]any{constants.FieldName: "After"},
			want:   false,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			if got := ConnectedAppSessionInvalidationRequired(tc.before, tc.after); got != tc.want {
				t.Fatalf("ConnectedAppSessionInvalidationRequired() = %t, want %t", got, tc.want)
			}
		})
	}
}

func assertStringSliceEqual(t *testing.T, label string, got, want []string) {
	t.Helper()
	if want == nil {
		want = []string{}
	}
	if got == nil {
		got = []string{}
	}
	if !reflect.DeepEqual(got, want) {
		t.Fatalf("%s = %v, want %v", label, got, want)
	}
}

type postUpdateSessionRepoRecorder struct {
	userIDs    []string
	profileIDs []string
	roleIDs    []string
	appIDs     []string
}

func (r *postUpdateSessionRepoRecorder) InsertSession(context.Context, *models.SystemSession) error {
	return nil
}

func (r *postUpdateSessionRepoRecorder) GetSession(context.Context, string) (*models.SystemSession, error) {
	return nil, nil
}

func (r *postUpdateSessionRepoRecorder) AdvanceSessionAuthEpoch(context.Context, string, uint64) (uint64, error) {
	return 0, nil
}

func (r *postUpdateSessionRepoRecorder) RevokeSession(context.Context, string, string) (bool, error) {
	return true, nil
}

func (r *postUpdateSessionRepoRecorder) RevokeAllSessionsForUser(_ context.Context, userID string) error {
	r.userIDs = append(r.userIDs, userID)
	return nil
}

func (r *postUpdateSessionRepoRecorder) RevokeAllSessionsByProfileID(_ context.Context, profileID string) (int64, error) {
	r.profileIDs = append(r.profileIDs, profileID)
	return 1, nil
}

func (r *postUpdateSessionRepoRecorder) RevokeAllSessionsByUserRoleID(_ context.Context, roleID string) (int64, error) {
	r.roleIDs = append(r.roleIDs, roleID)
	return 1, nil
}

func (r *postUpdateSessionRepoRecorder) RevokeAllSessionsForConnectedApp(_ context.Context, appID string) (int64, error) {
	r.appIDs = append(r.appIDs, appID)
	return 1, nil
}

func (r *postUpdateSessionRepoRecorder) RevokeSessionsForUserAndConnectedApp(context.Context, string, string) (int64, error) {
	return 0, nil
}

func (r *postUpdateSessionRepoRecorder) RenewSession(context.Context, string, string, string, time.Time, time.Time) (bool, error) {
	return true, nil
}

func (r *postUpdateSessionRepoRecorder) UpdateLastActivity(context.Context, string) error {
	return nil
}

func (r *postUpdateSessionRepoRecorder) CleanupExpiredSessions(context.Context, time.Duration) (int64, error) {
	return 0, nil
}

func (r *postUpdateSessionRepoRecorder) IsConnectedAppActive(context.Context, string) (bool, error) {
	return true, nil
}

type postUpdateDeactivationHookRecorder struct {
	userIDs []string
}

func (r *postUpdateDeactivationHookRecorder) OnUserDeactivated(userID string) {
	r.userIDs = append(r.userIDs, userID)
}

type postUpdateConnectedAppHookRecorder struct {
	appIDs []string
}

func (r *postUpdateConnectedAppHookRecorder) OnConnectedAppChanged(appID string) {
	r.appIDs = append(r.appIDs, appID)
}
