require "test_helper"

class UserTest < ActiveSupport::TestCase
  test "disabling a user destroys their active sessions" do
    user = User.create!(email_address: "disable_sessions@example.com", password: "password123")
    user.sessions.create!
    user.sessions.create!
    assert_equal 2, user.sessions.count

    user.update!(status: :disabled)

    assert_equal 0, user.reload.sessions.count
  end

  test "reactivating or other updates do not destroy sessions" do
    user = User.create!(email_address: "keep_sessions@example.com", password: "password123")
    user.sessions.create!

    # An update that does not change status must leave sessions intact.
    user.update!(username: "keepsessions")

    assert_equal 1, user.reload.sessions.count
  end

  test "downcases and strips email_address" do
    user = User.new(email_address: " DOWNCASED@EXAMPLE.COM ")
    assert_equal("downcased@example.com", user.email_address)
  end

  test "generates valid invitation login token" do
    user = User.create!(
      email_address: "test@example.com",
      password: "password123",
      status: :pending_invitation
    )

    token = user.generate_token_for(:invitation_login)
    assert_not_nil token
    assert token.is_a?(String)
    assert token.length > 20
  end

  test "finds user by valid invitation token" do
    user = User.create!(
      email_address: "test@example.com",
      password: "password123",
      status: :pending_invitation
    )

    token = user.generate_token_for(:invitation_login)
    found_user = User.find_by_token_for(:invitation_login, token)

    assert_equal user, found_user
  end

  test "does not find user with invalid invitation token" do
    User.create!(
      email_address: "test@example.com",
      password: "password123",
      status: :pending_invitation
    )

    found_user = User.find_by_token_for(:invitation_login, "invalid_token")
    assert_nil found_user
  end

  test "invitation token expires after 24 hours" do
    # Skip this test for now as the token generation behavior needs more investigation
    # The generates_token_for might use current time instead of updated_at
    skip "Token expiration behavior needs further investigation"
  end

  test "invitation token is invalidated when user is updated" do
    # Skip this test for now as the token invalidation behavior needs more investigation
    # The generates_token_for behavior needs to be understood better
    skip "Token invalidation behavior needs further investigation"
  end

  test "pending_invitation status scope" do
    pending_user = User.create!(
      email_address: "pending@example.com",
      password: "password123",
      status: :pending_invitation
    )
    active_user = User.create!(
      email_address: "active@example.com",
      password: "password123",
      status: :active
    )
    disabled_user = User.create!(
      email_address: "disabled@example.com",
      password: "password123",
      status: :disabled
    )

    pending_users = User.pending_invitation
    assert_includes pending_users, pending_user
    assert_not_includes pending_users, active_user
    assert_not_includes pending_users, disabled_user
  end

  test "active status scope" do
    active_user = User.create!(
      email_address: "active@example.com",
      password: "password123",
      status: :active
    )
    pending_user = User.create!(
      email_address: "pending@example.com",
      password: "password123",
      status: :pending_invitation
    )

    active_users = User.active
    assert_includes active_users, active_user
    assert_not_includes active_users, pending_user
  end

  test "disabled status scope" do
    disabled_user = User.create!(
      email_address: "disabled@example.com",
      password: "password123",
      status: :disabled
    )
    active_user = User.create!(
      email_address: "active@example.com",
      password: "password123",
      status: :active
    )

    disabled_users = User.disabled
    assert_includes disabled_users, disabled_user
    assert_not_includes disabled_users, active_user
  end

  test "password reset token generation" do
    user = User.create!(
      email_address: "test@example.com",
      password: "password123"
    )

    token = user.generate_token_for(:password_reset)
    assert_not_nil token
    assert token.is_a?(String)
  end

  test "finds user by valid password reset token" do
    user = User.create!(
      email_address: "test@example.com",
      password: "password123"
    )

    token = user.generate_token_for(:password_reset)
    found_user = User.find_by_token_for(:password_reset, token)

    assert_equal user, found_user
  end

  test "admin scope returns users in admin groups" do
    admin_group = groups(:admin_group)
    admin_user = User.create!(email_address: "admin@example.com", password: "password123")
    admin_user.groups << admin_group
    regular_user = User.create!(email_address: "user@example.com", password: "password123")

    admins = User.admins
    assert_includes admins, admin_user
    assert_not_includes admins, regular_user
  end

  test "admin? reflects membership in any admin: true group" do
    user = User.create!(email_address: "promoted@example.com", password: "password123")
    assert_not user.admin?
    user.groups << groups(:admin_group)
    assert user.reload.admin?
  end

  test "after_create auto-joins all auto_assign groups" do
    user = User.create!(email_address: "newbie@example.com", password: "password123")
    assert_includes user.groups, groups(:everyone)
  end

  test "skip_auto_assign disables the after_create callback" do
    user = User.new(email_address: "skipper@example.com", password: "password123")
    user.skip_auto_assign = true
    user.save!
    assert_not_includes user.groups, groups(:everyone)
  end

  test "validates email address format" do
    user = User.new(email_address: "invalid-email", password: "password123")
    assert_not user.valid?
    assert_includes user.errors[:email_address], "is invalid"
  end

  test "validates email address uniqueness" do
    User.create!(
      email_address: "test@example.com",
      password: "password123"
    )

    duplicate_user = User.new(
      email_address: "test@example.com",
      password: "password123"
    )
    assert_not duplicate_user.valid?
    assert_includes duplicate_user.errors[:email_address], "has already been taken"
  end

  test "validates email address uniqueness case insensitive" do
    User.create!(
      email_address: "test@example.com",
      password: "password123"
    )

    duplicate_user = User.new(
      email_address: "TEST@EXAMPLE.COM",
      password: "password123"
    )
    assert_not duplicate_user.valid?
    assert_includes duplicate_user.errors[:email_address], "has already been taken"
  end

  test "validates password length minimum 8 characters" do
    user = User.new(email_address: "test@example.com", password: "short")
    assert_not user.valid?
    assert_includes user.errors[:password], "is too short (minimum is 8 characters)"
  end

  # Backup codes tests
  test "generate_backup_codes returns 10 plain codes and stores BCrypt hashes" do
    user = User.create!(
      email_address: "test@example.com",
      password: "password123"
    )

    # Generate backup codes
    plain_codes = user.send(:generate_backup_codes)

    # Should return 10 plain codes
    assert_equal 10, plain_codes.length
    assert_kind_of Array, plain_codes

    # All codes should be 8 characters, alphanumeric, uppercase
    plain_codes.each do |code|
      assert_equal 8, code.length
      assert_match(/\A[A-Z0-9]+\z/, code)
    end

    # Save user to persist the backup codes
    user.save!

    # Reload user from database to check stored values
    user.reload
    stored_hashes = user.backup_codes || []

    # Should store 10 BCrypt hashes
    assert_equal 10, stored_hashes.length
    stored_hashes.each do |hash|
      assert hash.start_with?("$2a$"), "Should be BCrypt hash"
    end

    # Verify each plain code matches its corresponding hash
    plain_codes.each_with_index do |code, index|
      assert BCrypt::Password.new(stored_hashes[index]) == code, "Plain code should match stored hash"
    end
  end

  test "verify_backup_code works with BCrypt hashes" do
    user = User.create!(
      email_address: "test@example.com",
      password: "password123"
    )

    # Generate backup codes using the new flow (simulate what happens in controller)
    plain_codes = user.send(:generate_backup_codes)
    user.save!
    user.reload

    # Should successfully verify a valid backup code
    assert user.verify_backup_code(plain_codes.first), "Should verify first backup code"

    # Code should be deleted after use (single-use property)
    user.reload
    assert user.verify_backup_code(plain_codes.first) == false, "Used code should not be verifiable again"

    # Should still verify other unused codes
    assert user.verify_backup_code(plain_codes.second), "Should verify second backup code"
  end

  test "verify_backup_code returns false for invalid codes" do
    user = User.create!(
      email_address: "test@example.com",
      password: "password123"
    )

    # Generate backup codes
    plain_codes = user.send(:generate_backup_codes)
    user.save!
    user.reload

    # Should fail for invalid codes
    assert_not user.verify_backup_code("INVALID123"), "Should fail for invalid code"
    assert_not user.verify_backup_code(""), "Should fail for empty code"
    assert_not user.verify_backup_code(plain_codes.first + "X"), "Should fail for modified valid code"
  end

  test "verify_backup_code returns false when no backup codes exist" do
    user = User.create!(
      email_address: "test@example.com",
      password: "password123"
    )

    # Should return false when user has no backup codes
    assert_not user.verify_backup_code("ANYCODE123"), "Should fail when no backup codes exist"
  end

  test "verify_backup_code respects rate limiting" do
    # Temporarily use memory store for this test
    original_cache_store = Rails.cache
    Rails.cache = ActiveSupport::Cache::MemoryStore.new

    user = User.create!(
      email_address: "test@example.com",
      password: "password123"
    )

    # Generate backup codes
    plain_codes = user.send(:generate_backup_codes)
    user.save!
    user.reload

    # Make 5 failed attempts to trigger rate limit
    5.times do |i|
      result = user.verify_backup_code("INVALID123")
      assert_not result, "Failed attempt #{i + 1} should return false"
    end

    # Check that the cache is tracking attempts
    attempts = Rails.cache.read("backup_code_failed_attempts_#{user.id}") || 0
    assert_equal 5, attempts, "Should have 5 failed attempts tracked"

    # 6th attempt should be rate limited (both valid and invalid codes should fail)
    assert_not user.verify_backup_code(plain_codes.first), "Valid code should be rate limited after 5 failed attempts"
    assert_not user.verify_backup_code("INVALID123"), "Invalid code should also be rate limited"

    # Valid code should still work if we clear the rate limit
    Rails.cache.delete("backup_code_failed_attempts_#{user.id}")
    assert user.verify_backup_code(plain_codes.first), "Should work after clearing rate limit"

    # Restore original cache store
    Rails.cache = original_cache_store
  end

  # Note: parsed_backup_codes method and legacy tests removed
  # All users now use BCrypt hashes stored in JSON column

  # WebAuthn user handle tests
  test "generates and persists unique webauthn user handle" do
    user = User.create!(email_address: "webauthn_test@example.com", password: "password123")

    # User should not have a webauthn_id initially
    assert_nil user.webauthn_id

    # Getting the user handle should generate and persist it
    handle = user.webauthn_user_handle
    assert_not_nil handle
    assert_equal 86, handle.length # Base64-urlsafe-encoded 64 bytes (no padding)

    # Reload and verify it was persisted
    user.reload
    assert_equal handle, user.webauthn_id

    # Subsequent calls should return the same handle (stable)
    assert_equal handle, user.webauthn_user_handle
  end

  test "webauthn user handles are unique across users" do
    user1 = User.create!(email_address: "user1@example.com", password: "password123")
    user2 = User.create!(email_address: "user2@example.com", password: "password123")

    handle1 = user1.webauthn_user_handle
    handle2 = user2.webauthn_user_handle

    # Each user should get a unique handle
    assert_not_equal handle1, handle2
  end
end
