package auth

// session_lifecycle_test.go — Test plan for Section 4: Session Lifecycle (Fix for RC3)
//
// Design ref: SECURITY_REFACTOR_DESIGN.md §4
// Coverage target: CreateSessionForUser lockout check, ChangePassword lockout + revocation,
//   IncrementFailedAndLockIfNeeded, Login session revocation on lockout, admin password reset.
//
// All tests are pure unit tests using the mockUserRepository / mockSessionRepository
// helpers defined in auth_service_test.go (same package).
//
// HOW TO RUN:
//   go test ./internal/features/auth/ -run TestSessionLifecycle -v

import (
	"context"
	"testing"
	"time"

	"github.com/nexuscrm/backend/internal/domain/ports"
	"github.com/nexuscrm/backend/pkg/errors"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// ---------------------------------------------------------------------------
// Section 4.2 — CreateSessionForUser: lockout check on ALL auth paths (H1 fix)
// ---------------------------------------------------------------------------

// TestCreateSessionForUser_RejectsLockedAccount verifies that CreateSessionForUser
// returns an unauthorized error (not a session) when the account's LockedUntil
// is set to a time in the future.
func TestCreateSessionForUser_RejectsLockedAccount(t *testing.T) {
	lockTime := time.Now().Add(30 * time.Minute)
	userRepo := &mockUserRepository{
		getUserByIDFn: func(ctx context.Context, userID string) (*models.User, error) {
			return &models.User{
				ID:          "u1",
				IsActive:    true,
				LockedUntil: &lockTime,
			}, nil
		},
	}
	svc := NewAuthService(userRepo, &mockSessionRepository{}, &mockPermissionRepository{})

	token, err := svc.CreateSessionForUser(context.Background(), "u1", "127.0.0.1", "test-agent", nil)

	require.Error(t, err, "expected error for locked account")
	assert.Nil(t, token, "expected nil token for locked account")
	// Verify it is an unauthorized error, not an internal error
	if e, ok := err.(interface{ StatusCode() int }); ok {
		assert.Equal(t, 401, e.StatusCode(), "expected 401 Unauthorized for locked account")
	}
	_ = errors.NewUnauthorizedError // verify import used
}

// TestCreateSessionForUser_AllowsExpiredLock verifies that when LockedUntil is
// set but in the past, CreateSessionForUser succeeds (lock has expired).
func TestCreateSessionForUser_AllowsExpiredLock(t *testing.T) {
	expiredLock := time.Now().Add(-5 * time.Minute)
	userRepo := &mockUserRepository{
		getUserByIDFn: func(ctx context.Context, userID string) (*models.User, error) {
			return &models.User{
				ID:          "u1",
				Username:    "testuser",
				Email:       "test@example.com",
				IsActive:    true,
				LockedUntil: &expiredLock,
			}, nil
		},
		getUserRoleIDFn: func(ctx context.Context, userID string) (*string, error) {
			return nil, nil
		},
	}
	sessionRepo := &mockSessionRepository{
		insertSessionFn: func(ctx context.Context, session *models.SystemSession) error {
			return nil
		},
	}
	svc := NewAuthService(userRepo, sessionRepo, &mockPermissionRepository{})

	token, err := svc.CreateSessionForUser(context.Background(), "u1", "127.0.0.1", "test-agent", nil)

	require.NoError(t, err, "expected no error for expired lock")
	assert.NotNil(t, token, "expected token for expired lock")
}

// TestCreateSessionForUser_RejectsInactiveUser verifies that an inactive user
// (IsActive=false) cannot obtain a session via CreateSessionForUser.
func TestCreateSessionForUser_RejectsInactiveUser(t *testing.T) {
	userRepo := &mockUserRepository{
		getUserByIDFn: func(ctx context.Context, userID string) (*models.User, error) {
			return &models.User{ID: "u1", IsActive: false}, nil
		},
	}
	svc := NewAuthService(userRepo, &mockSessionRepository{}, &mockPermissionRepository{})

	token, err := svc.CreateSessionForUser(context.Background(), "u1", "127.0.0.1", "agent", nil)

	require.Error(t, err)
	assert.Nil(t, token)
}

// ---------------------------------------------------------------------------
// Section 4.4 — ChangePassword: lockout check (H2 fix)
// ---------------------------------------------------------------------------

// TestChangePassword_RejectsLockedAccount verifies that ChangePassword returns
// an unauthorized error when the account is currently locked.
func TestChangePassword_RejectsLockedAccount(t *testing.T) {
	lockTime := time.Now().Add(30 * time.Minute)
	userRepo := &mockUserRepository{
		findUserByIDWithPasswordFn: func(ctx context.Context, userID string) (*ports.UserWithPassword, error) {
			return &ports.UserWithPassword{
				User: &models.User{
					ID:          "u1",
					IsActive:    true,
					LockedUntil: &lockTime,
				},
				PasswordHash: "somehash",
			}, nil
		},
	}
	svc := NewAuthService(userRepo, &mockSessionRepository{}, &mockPermissionRepository{})

	err := svc.ChangePassword(context.Background(), "u1", "old-password", "NewValidPass1!")

	require.Error(t, err, "expected error for locked account")
	assert.Contains(t, err.Error(), "locked", "expected error message to contain 'locked'")
}

// TestChangePassword_AllowsExpiredLock verifies that ChangePassword succeeds
// when LockedUntil is in the past.
func TestChangePassword_AllowsExpiredLock(t *testing.T) {
	expiredLock := time.Now().Add(-5 * time.Minute)
	const correctPassword = "OldCorrectPass1!"
	hash := hashForTest(t, correctPassword)

	userRepo := &mockUserRepository{
		findUserByIDWithPasswordFn: func(ctx context.Context, userID string) (*ports.UserWithPassword, error) {
			return &ports.UserWithPassword{
				User: &models.User{
					ID:          "u1",
					IsActive:    true,
					LockedUntil: &expiredLock,
				},
				PasswordHash: hash,
			}, nil
		},
	}
	svc := NewAuthService(userRepo, &mockSessionRepository{}, &mockPermissionRepository{})

	err := svc.ChangePassword(context.Background(), "u1", correctPassword, "NewValidPass1!")

	require.NoError(t, err, "expected no error for expired lock")
}

// ---------------------------------------------------------------------------
// Section 4.1 — ChangePassword: revokes all sessions on success (H3 fix)
// ---------------------------------------------------------------------------

// TestChangePassword_RevokesAllSessionsOnSuccess verifies that after a
// successful password change, RevokeAllSessionsForUser is called with the
// user's ID so that old sessions (e.g., on other devices) cannot be reused.
func TestChangePassword_RevokesAllSessionsOnSuccess(t *testing.T) {
	const correctPassword = "CorrectPass1!"
	hash := hashForTest(t, correctPassword)

	revokeCalledFor := ""
	sessionRepo := &mockSessionRepository{
		revokeAllSessionsForUserFn: func(ctx context.Context, userID string) error {
			revokeCalledFor = userID
			return nil
		},
	}

	userRepo := &mockUserRepository{
		findUserByIDWithPasswordFn: func(ctx context.Context, userID string) (*ports.UserWithPassword, error) {
			return &ports.UserWithPassword{
				User:         &models.User{ID: "u1", IsActive: true},
				PasswordHash: hash,
			}, nil
		},
	}
	svc := NewAuthService(userRepo, sessionRepo, &mockPermissionRepository{})

	err := svc.ChangePassword(context.Background(), "u1", correctPassword, "NewValidPass1!")

	require.NoError(t, err)
	assert.Equal(t, "u1", revokeCalledFor, "RevokeAllSessionsForUser must be called with 'u1'")
}

// ---------------------------------------------------------------------------
// Section 4.3 — IncrementFailedAndLockIfNeeded: atomic lockout
// ---------------------------------------------------------------------------

// TestLogin_LocksAccountAtThreshold_RevokesExistingSessions verifies that when
// IncrementFailedAndLockIfNeeded returns locked=true, Login calls
// RevokeAllSessionsForUser for the locked user.
func TestLogin_LocksAccountAtThreshold_RevokesExistingSessions(t *testing.T) {
	const wrongPassword = "wrong-password"
	// Use a bcrypt hash of a different string so VerifyPassword always fails.
	hash := hashForTest(t, "correct-password")

	revokeCalledFor := ""
	sessionRepo := &mockSessionRepository{
		revokeAllSessionsForUserFn: func(ctx context.Context, userID string) error {
			revokeCalledFor = userID
			return nil
		},
	}

	userRepo := &mockUserRepository{
		findUserByEmailWithPasswordFn: func(ctx context.Context, email string) (*ports.UserWithPassword, error) {
			return &ports.UserWithPassword{
				User:         &models.User{ID: "u1", Email: email, IsActive: true},
				PasswordHash: hash,
			}, nil
		},
		incrementFailedAndLockIfNeededFn: func(ctx context.Context, userID string, maxAttempts int, lockDuration time.Duration) (bool, error) {
			// Simulate hitting the threshold — account is now locked.
			return true, nil
		},
	}
	svc := NewAuthService(userRepo, sessionRepo, &mockPermissionRepository{})

	_, err := svc.Login(context.Background(), "user@example.com", wrongPassword, "127.0.0.1", "agent", nil)

	require.Error(t, err, "expected error for wrong password")
	assert.Equal(t, "u1", revokeCalledFor, "RevokeAllSessionsForUser must be called when account is locked")
}

// TestLogin_DoesNotLockBelowThreshold verifies that Login does NOT call
// RevokeAllSessionsForUser when IncrementFailedAndLockIfNeeded returns locked=false.
func TestLogin_DoesNotLockBelowThreshold(t *testing.T) {
	hash := hashForTest(t, "correct-password")

	revokeCalled := false
	sessionRepo := &mockSessionRepository{
		revokeAllSessionsForUserFn: func(ctx context.Context, userID string) error {
			revokeCalled = true
			return nil
		},
	}

	userRepo := &mockUserRepository{
		findUserByEmailWithPasswordFn: func(ctx context.Context, email string) (*ports.UserWithPassword, error) {
			return &ports.UserWithPassword{
				User:         &models.User{ID: "u1", Email: email, IsActive: true},
				PasswordHash: hash,
			}, nil
		},
		incrementFailedAndLockIfNeededFn: func(ctx context.Context, userID string, maxAttempts int, lockDuration time.Duration) (bool, error) {
			// Below threshold — not locked yet.
			return false, nil
		},
	}
	svc := NewAuthService(userRepo, sessionRepo, &mockPermissionRepository{})

	_, err := svc.Login(context.Background(), "user@example.com", "wrong-password", "127.0.0.1", "agent", nil)

	require.Error(t, err, "expected error for wrong password")
	assert.False(t, revokeCalled, "RevokeAllSessionsForUser must NOT be called below threshold")
}

