require "test_helper"

class UserTest < ActiveSupport::TestCase
  test "should be valid with an email and password" do
    user = User.new(email: "new-user@example.com", password: "password123456")
    assert user.valid?
  end

  test "should be invalid without an email" do
    user = User.new(password: "password123456")
    assert_not user.valid?
  end

  test "should be invalid with a duplicate email" do
    existing_user = users(:admin_user)
    user = User.new(email: existing_user.email, password: "password123456")
    assert_not user.valid?
  end

  test "should have a default role of contributor" do
    user = User.new(email: "new-user@example.com", password: "password123456")
    assert user.contributor?
  end

  test "can be a reviewer" do
    user = User.new(email: "new-user@example.com", password: "password123456", role: :reviewer)
    assert user.reviewer?
  end

  test "can be an admin" do
    user = User.new(email: "new-user@example.com", password: "password123456", role: :admin)
    assert user.admin?
  end

  test "should be invalid with a password shorter than 12 characters" do
    user = User.new(email: "test@example.com", password: "short")
    assert_not user.valid?
    assert_includes user.errors[:password], "is too short (minimum is 12 characters)"
  end

  # Association tests
  test "can have invited_by association" do
    inviter = users(:admin_user)
    user = User.create!(email: "invited@example.com", password: "password123456", invited_by: inviter)
    assert_equal inviter, user.invited_by
  end

  test "can have invited_users" do
    inviter = users(:admin_user)
    user = User.create!(email: "invited@example.com", password: "password123456", invited_by: inviter)
    assert_includes inviter.invited_users, user
  end

  test "has many created_entries" do
    user = users(:admin_user)
    assert_respond_to user, :created_entries
  end

  test "has many updated_entries" do
    user = users(:admin_user)
    assert_respond_to user, :updated_entries
  end

  test "has many requested_entries" do
    user = users(:admin_user)
    assert_respond_to user, :requested_entries
  end

  test "has many submitted_suggested_meanings" do
    user = users(:admin_user)
    assert_respond_to user, :submitted_suggested_meanings
  end

  test "has many reviewed_suggested_meanings" do
    user = users(:admin_user)
    assert_respond_to user, :reviewed_suggested_meanings
  end

  test "has many comments" do
    user = users(:admin_user)
    assert_respond_to user, :comments
  end

  # Scope tests
  test "by_role scope filters contributors" do
    contributor_user = users(:contributor_user)
    results = User.by_role(:contributor)
    assert_includes results, contributor_user
  end

  test "by_role scope filters reviewers" do
    reviewer_user = users(:reviewer_user)
    results = User.by_role(:reviewer)
    assert_includes results, reviewer_user
  end

  test "by_role scope filters admins" do
    admin_user = users(:admin_user)
    results = User.by_role(:admin)
    assert_includes results, admin_user
  end

  test "by_role scope returns all when role is blank" do
    results = User.by_role(nil)
    assert_equal User.all, results
  end

  test "search_email scope finds users by email" do
    admin_user = users(:admin_user)
    results = User.search_email("admin")
    assert_includes results, admin_user
  end

  test "search_email scope returns all when query is blank" do
    results = User.search_email(nil)
    assert_equal User.all, results
  end

  test "search_email scope finds partial matches" do
    admin_user = users(:admin_user)
    results = User.search_email("exam")
    assert_includes results, admin_user
  end

  # invitation_expired? tests
  test "invitation_expired? returns false when invitation_sent_at is nil" do
    user = User.new(email: "test@example.com", password: "password123456")
    assert_not user.invitation_expired?
  end

  test "invitation_expired? returns false for recent invitation" do
    user = User.new(
      email: "test@example.com",
      password: "password123456",
      invitation_sent_at: 1.day.ago
    )
    assert_not user.invitation_expired?
  end

  test "invitation_expired? returns true for expired invitation" do
    user = User.new(
      email: "test@example.com",
      password: "password123456",
      invitation_sent_at: 15.days.ago
    )
    assert user.invitation_expired?
  end

  test "invitation_expired? returns false exactly at expiry boundary" do
    user = User.new(
      email: "test@example.com",
      password: "password123456",
      invitation_sent_at: 13.days.ago
    )
    assert_not user.invitation_expired?
  end

  # invitation_pending? tests
  test "invitation_pending? returns true for valid pending invitation" do
    user = User.new(
      email: "test@example.com",
      password: "password123456",
      invitation_token: "valid_token",
      invitation_sent_at: 1.day.ago,
      invitation_accepted_at: nil
    )
    assert user.invitation_pending?
  end

  test "invitation_pending? returns false when invitation is accepted" do
    user = User.new(
      email: "test@example.com",
      password: "password123456",
      invitation_token: "valid_token",
      invitation_sent_at: 1.day.ago,
      invitation_accepted_at: Time.current
    )
    assert_not user.invitation_pending?
  end

  test "invitation_pending? returns false when invitation is expired" do
    user = User.new(
      email: "test@example.com",
      password: "password123456",
      invitation_token: "valid_token",
      invitation_sent_at: 15.days.ago,
      invitation_accepted_at: nil
    )
    assert_not user.invitation_pending?
  end

  test "invitation_pending? returns false when no invitation token" do
    user = User.new(
      email: "test@example.com",
      password: "password123456",
      invitation_token: nil,
      invitation_sent_at: 1.day.ago,
      invitation_accepted_at: nil
    )
    assert_not user.invitation_pending?
  end

  # invite_by tests
  test "invite_by sets invited_by and generates token" do
    inviter = users(:admin_user)
    user = User.new(email: "test@example.com", password: "password123456")
    user.invite_by(inviter)

    assert_equal inviter, user.invited_by
    assert_not_nil user.invitation_token
    assert_not_nil user.invitation_sent_at
  end

  test "invite_by does not override existing invited_by" do
    original_inviter = users(:admin_user)
    new_inviter = users(:reviewer_user)
    user = User.new(email: "test@example.com", password: "password123456", invited_by: original_inviter)
    user.invite_by(new_inviter)

    assert_equal original_inviter, user.invited_by
  end

  test "invite_by handles nil invitee" do
    user = User.new(email: "test@example.com", password: "password123456")
    user.invite_by(nil)

    assert_nil user.invited_by
    assert_not_nil user.invitation_token
    assert_not_nil user.invitation_sent_at
  end

  test "invite_by generates 32-character token" do
    inviter = users(:admin_user)
    user = User.new(email: "test@example.com", password: "password123456")
    user.invite_by(inviter)

    assert user.invitation_token.length >= 32
  end

  # invite_by! tests
  test "invite_by! saves the user" do
    inviter = users(:admin_user)
    user = User.new(email: "test@example.com", password: "password123456")
    user.invite_by!(inviter)

    assert_not_nil user.id
    assert user.persisted?
  end

  test "invite_by! works without invitee parameter" do
    user = User.new(email: "test@example.com", password: "password123456")
    user.invite_by!

    assert_not_nil user.id
    assert_not_nil user.invitation_token
  end

  # find_by_valid_invitation_token tests
  test "find_by_valid_invitation_token finds user with valid token" do
    user = User.create!(
      email: "test@example.com",
      password: "password123456",
      invitation_token: "valid_token_12345",
      invitation_sent_at: 1.day.ago,
      invitation_accepted_at: nil
    )

    found_user = User.find_by_valid_invitation_token("valid_token_12345")
    assert_equal user, found_user
  end

  test "find_by_valid_invitation_token returns nil for expired token" do
    User.create!(
      email: "test@example.com",
      password: "password123456",
      invitation_token: "expired_token",
      invitation_sent_at: 15.days.ago,
      invitation_accepted_at: nil
    )

    found_user = User.find_by_valid_invitation_token("expired_token")
    assert_nil found_user
  end

  test "find_by_valid_invitation_token returns nil for accepted invitation" do
    User.create!(
      email: "test@example.com",
      password: "password123456",
      invitation_token: "accepted_token",
      invitation_sent_at: 1.day.ago,
      invitation_accepted_at: Time.current
    )

    found_user = User.find_by_valid_invitation_token("accepted_token")
    assert_nil found_user
  end

  test "find_by_valid_invitation_token returns nil for non-existent token" do
    found_user = User.find_by_valid_invitation_token("nonexistent_token")
    assert_nil found_user
  end

  # remember_me tests
  test "remember_me generates token and saves" do
    user = users(:admin_user)
    token = user.remember_me

    assert_not_nil token
    assert_not_nil user.remember_token
    assert_not_nil user.remember_created_at
    assert_equal token, user.remember_token
  end

  test "remember_me returns the generated token" do
    user = users(:admin_user)
    token = user.remember_me

    assert_kind_of String, token
    assert token.length >= 32
  end

  test "remember_me updates database immediately" do
    user = users(:admin_user)
    user.remember_me

    user.reload
    assert_not_nil user.remember_token
    assert_not_nil user.remember_created_at
  end

  test "remember_me saves without validation" do
    user = users(:admin_user)
    # Make user invalid (email already taken by changing to duplicate)
    user.email = ""
    token = user.remember_me

    assert_not_nil token
    user.reload
    assert_not_nil user.remember_token
  end

  # forget_me tests
  test "forget_me clears remember token and timestamp" do
    user = users(:admin_user)
    user.remember_me
    assert_not_nil user.remember_token

    user.forget_me
    user.reload

    assert_nil user.remember_token
    assert_nil user.remember_created_at
  end

  test "forget_me works when no remember token exists" do
    user = users(:admin_user)
    user.forget_me

    assert_nil user.remember_token
    assert_nil user.remember_created_at
  end

  # remember_token_expired? tests
  test "remember_token_expired? returns true when remember_created_at is nil" do
    user = users(:admin_user)
    assert user.remember_token_expired?
  end

  test "remember_token_expired? returns false for recent token" do
    user = users(:admin_user)
    user.remember_token = "token"
    user.remember_created_at = 1.day.ago
    user.save(validate: false)

    assert_not user.remember_token_expired?
  end

  test "remember_token_expired? returns true for expired token" do
    user = users(:admin_user)
    user.remember_token = "token"
    user.remember_created_at = 3.weeks.ago
    user.save(validate: false)

    assert user.remember_token_expired?
  end

  test "remember_token_expired? boundary at 2 weeks" do
    user = users(:admin_user)
    user.remember_token = "token"
    user.remember_created_at = 13.days.ago
    user.save(validate: false)

    assert_not user.remember_token_expired?
  end

  # find_by_valid_remember_token tests
  test "find_by_valid_remember_token finds user with valid token" do
    user = users(:admin_user)
    token = user.remember_me

    found_user = User.find_by_valid_remember_token(token)
    assert_equal user, found_user
  end

  test "find_by_valid_remember_token returns nil for expired token" do
    user = users(:admin_user)
    user.remember_token = "expired_token"
    user.remember_created_at = 3.weeks.ago
    user.save(validate: false)

    found_user = User.find_by_valid_remember_token("expired_token")
    assert_nil found_user
  end

  test "find_by_valid_remember_token returns nil for non-existent token" do
    found_user = User.find_by_valid_remember_token("nonexistent_token")
    assert_nil found_user
  end

  test "find_by_valid_remember_token returns nil when user has no remember_created_at" do
    user = users(:admin_user)
    user.remember_token = "token_without_timestamp"
    user.remember_created_at = nil
    user.save(validate: false)

    found_user = User.find_by_valid_remember_token("token_without_timestamp")
    assert_nil found_user
  end

  # Password authentication tests
  test "authenticate returns user for correct password" do
    user = User.create!(email: "test@example.com", password: "password123456")
    assert_equal user, user.authenticate("password123456")
  end

  test "authenticate returns false for incorrect password" do
    user = User.create!(email: "test@example.com", password: "password123456")
    assert_not user.authenticate("wrongpassword")
  end

  # Email normalization tests
  test "email uniqueness is case-insensitive" do
    User.create!(email: "Test@Example.com", password: "password123456")
    duplicate_user = User.new(email: "test@example.com", password: "password123456")

    # Note: This depends on database collation, but should be tested
    assert_not duplicate_user.valid?
  end

  # Constants tests
  test "INVITATION_TOKEN_EXPIRY is 14 days" do
    assert_equal 14.days, User::INVITATION_TOKEN_EXPIRY
  end

  test "REMEMBER_TOKEN_EXPIRY is 2 weeks" do
    assert_equal 2.weeks, User::REMEMBER_TOKEN_EXPIRY
  end
end
