defmodule Microwaveprop.Accounts.UserTest do
  use Microwaveprop.DataCase, async: true

  import Microwaveprop.AccountsFixtures

  alias Microwaveprop.Accounts.User

  @valid_attrs %{
    callsign: "W5ABC",
    name: "Alice",
    email: "alice@example.com",
    password: "hunter2!!",
    password_confirmation: "hunter2!!"
  }

  describe "admin_email/0" do
    test "exposes the hard-coded admin email constant" do
      assert User.admin_email() == "graham@mcintire.me"
    end
  end

  describe "registration_changeset/3" do
    test "valid attrs produce a valid changeset" do
      changeset = User.registration_changeset(%User{}, @valid_attrs)
      assert changeset.valid?
    end

    test "requires callsign, name, email, and password" do
      changeset = User.registration_changeset(%User{}, %{})
      errors = errors_on(changeset)

      assert errors[:callsign]
      assert errors[:name]
      assert errors[:email]
      assert errors[:password]
    end

    test "upcases callsign" do
      attrs = Map.put(@valid_attrs, :callsign, "w5abc")
      changeset = User.registration_changeset(%User{}, attrs)
      assert changeset.valid?
      assert Ecto.Changeset.get_change(changeset, :callsign) == "W5ABC"
    end

    test "rejects callsigns shorter than 3 chars" do
      attrs = Map.put(@valid_attrs, :callsign, "AB")
      changeset = User.registration_changeset(%User{}, attrs)
      assert "must be 3-10 letters and digits" in errors_on(changeset).callsign
    end

    test "rejects callsigns longer than 10 chars" do
      attrs = Map.put(@valid_attrs, :callsign, "ABCDEFGHIJK")
      changeset = User.registration_changeset(%User{}, attrs)
      assert "must be 3-10 letters and digits" in errors_on(changeset).callsign
    end

    test "rejects callsigns with non-alphanumeric chars" do
      attrs = Map.put(@valid_attrs, :callsign, "W5-AB")
      changeset = User.registration_changeset(%User{}, attrs)
      assert "must be 3-10 letters and digits" in errors_on(changeset).callsign
    end

    test "rejects name longer than 100 chars" do
      attrs = Map.put(@valid_attrs, :name, String.duplicate("a", 101))
      changeset = User.registration_changeset(%User{}, attrs)
      assert errors_on(changeset).name
    end

    test "rejects email without @ sign" do
      attrs = Map.put(@valid_attrs, :email, "not-an-email")
      changeset = User.registration_changeset(%User{}, attrs)
      assert "must have the @ sign and no spaces" in errors_on(changeset).email
    end

    test "rejects email with whitespace" do
      attrs = Map.put(@valid_attrs, :email, "bad @example.com")
      changeset = User.registration_changeset(%User{}, attrs)
      assert "must have the @ sign and no spaces" in errors_on(changeset).email
    end

    test "rejects email longer than 160 chars" do
      long_email = String.duplicate("a", 161) <> "@example.com"
      attrs = Map.put(@valid_attrs, :email, long_email)
      changeset = User.registration_changeset(%User{}, attrs)
      assert errors_on(changeset).email
    end

    test "rejects password shorter than 8 chars" do
      attrs = %{@valid_attrs | password: "short", password_confirmation: "short"}
      changeset = User.registration_changeset(%User{}, attrs)
      assert errors_on(changeset).password
    end

    test "rejects password longer than 72 chars" do
      long_password = String.duplicate("a", 73)
      attrs = %{@valid_attrs | password: long_password, password_confirmation: long_password}
      changeset = User.registration_changeset(%User{}, attrs)
      assert errors_on(changeset).password
    end

    test "requires password confirmation to match" do
      attrs = %{@valid_attrs | password_confirmation: "different!"}
      changeset = User.registration_changeset(%User{}, attrs)
      assert "does not match password" in errors_on(changeset).password_confirmation
    end

    test "hashes password by default and clears virtual field" do
      changeset = User.registration_changeset(%User{}, @valid_attrs)
      assert changeset.valid?
      hashed = Ecto.Changeset.get_change(changeset, :hashed_password)
      assert is_binary(hashed)
      assert String.starts_with?(hashed, "$2b$") or String.starts_with?(hashed, "$2a$")
      refute Ecto.Changeset.get_change(changeset, :password)
    end

    test "does not hash password when hash_password: false" do
      changeset = User.registration_changeset(%User{}, @valid_attrs, hash_password: false)
      assert changeset.valid?
      refute Ecto.Changeset.get_change(changeset, :hashed_password)
      assert Ecto.Changeset.get_change(changeset, :password) == @valid_attrs.password
    end

    test "final persisted struct contains hashed_password and no plaintext" do
      {:ok, user} =
        %User{}
        |> User.registration_changeset(@valid_attrs)
        |> Repo.insert()

      assert is_binary(user.hashed_password)
      assert user.password == nil
    end

    test "enforces email uniqueness via unsafe_validate_unique" do
      {:ok, _first} =
        %User{}
        |> User.registration_changeset(@valid_attrs)
        |> Repo.insert()

      duplicate = User.registration_changeset(%User{}, %{@valid_attrs | callsign: "W5XYZ"})
      refute duplicate.valid?
      assert "has already been taken" in errors_on(duplicate).email
    end

    test "enforces callsign uniqueness via unsafe_validate_unique" do
      {:ok, _first} =
        %User{}
        |> User.registration_changeset(@valid_attrs)
        |> Repo.insert()

      duplicate = User.registration_changeset(%User{}, %{@valid_attrs | email: "other@example.com"})
      refute duplicate.valid?
      assert "has already been taken" in errors_on(duplicate).callsign
    end

    test "surfaces unique constraint error on email at insert time (default opts)" do
      {:ok, _first} =
        %User{}
        |> User.registration_changeset(@valid_attrs)
        |> Repo.insert()

      # With the default opts, unique_constraint/3 is attached so the DB
      # violation comes back as a changeset error rather than raising.
      changeset = User.registration_changeset(%User{}, %{@valid_attrs | callsign: "W5XYZ"})
      # unsafe_validate_unique already flags it at changeset-build time.
      assert "has already been taken" in errors_on(changeset).email
    end

    test "validate_unique: false skips callsign and email uniqueness checks" do
      {:ok, _first} =
        %User{}
        |> User.registration_changeset(@valid_attrs)
        |> Repo.insert()

      changeset = User.registration_changeset(%User{}, @valid_attrs, validate_unique: false)
      # Uniqueness is not pre-validated, so the changeset is otherwise valid.
      assert changeset.valid?
    end

    test "grants admin when email matches the admin_email constant (case-insensitive)" do
      attrs = Map.put(@valid_attrs, :email, "GRAHAM@MCINTIRE.ME")
      changeset = User.registration_changeset(%User{}, attrs)
      assert changeset.valid?
      assert Ecto.Changeset.get_change(changeset, :is_admin) == true
    end

    test "grants admin for exact admin email match" do
      attrs = Map.put(@valid_attrs, :email, "graham@mcintire.me")
      changeset = User.registration_changeset(%User{}, attrs)
      assert Ecto.Changeset.get_change(changeset, :is_admin) == true
    end

    test "does not grant admin for other emails" do
      changeset = User.registration_changeset(%User{}, @valid_attrs)
      refute Ecto.Changeset.get_change(changeset, :is_admin)
    end

    test "does not set is_admin when email is missing" do
      changeset = User.registration_changeset(%User{}, Map.delete(@valid_attrs, :email))
      refute Ecto.Changeset.get_change(changeset, :is_admin)
    end
  end

  describe "admin_changeset/2" do
    setup do
      {:ok, user} = Repo.insert(User.registration_changeset(%User{}, @valid_attrs))
      %{user: user}
    end

    test "accepts valid updates and casts is_admin", %{user: user} do
      changeset =
        User.admin_changeset(user, %{
          callsign: "K5NEW",
          name: "New Name",
          email: "new@example.com",
          is_admin: true
        })

      assert changeset.valid?
      assert Ecto.Changeset.get_change(changeset, :is_admin) == true
      assert Ecto.Changeset.get_change(changeset, :callsign) == "K5NEW"
    end

    test "trims and upcases callsign", %{user: user} do
      changeset = User.admin_changeset(user, %{callsign: "  k5abc  "})
      assert Ecto.Changeset.get_change(changeset, :callsign) == "K5ABC"
    end

    test "requires callsign, name, and email", %{user: user} do
      changeset =
        User.admin_changeset(user, %{callsign: nil, name: nil, email: nil})

      errors = errors_on(changeset)
      assert errors[:callsign]
      assert errors[:name]
      assert errors[:email]
    end

    test "rejects invalid callsign format", %{user: user} do
      changeset = User.admin_changeset(user, %{callsign: "bad-call"})
      assert "must be 3-10 letters and digits" in errors_on(changeset).callsign
    end

    test "rejects invalid email", %{user: user} do
      changeset = User.admin_changeset(user, %{email: "bad email"})
      assert "must have the @ sign and no spaces" in errors_on(changeset).email
    end

    test "does not cast password", %{user: user} do
      changeset = User.admin_changeset(user, %{password: "something1234"})
      refute Ecto.Changeset.get_change(changeset, :password)
      refute Ecto.Changeset.get_change(changeset, :hashed_password)
    end

    test "enforces email uniqueness", %{user: user} do
      {:ok, _other} =
        Repo.insert(
          User.registration_changeset(%User{}, %{
            @valid_attrs
            | callsign: "K5OTH",
              email: "other@example.com"
          })
        )

      changeset = User.admin_changeset(user, %{email: "other@example.com"})
      refute changeset.valid?
      assert "has already been taken" in errors_on(changeset).email
    end

    test "enforces callsign uniqueness", %{user: user} do
      {:ok, _other} =
        Repo.insert(
          User.registration_changeset(%User{}, %{
            @valid_attrs
            | callsign: "K5OTH",
              email: "other@example.com"
          })
        )

      changeset = User.admin_changeset(user, %{callsign: "K5OTH"})
      refute changeset.valid?
      assert "has already been taken" in errors_on(changeset).callsign
    end
  end

  describe "email_changeset/3" do
    setup do
      {:ok, user} = Repo.insert(User.registration_changeset(%User{}, @valid_attrs))
      %{user: user}
    end

    test "accepts a new valid email", %{user: user} do
      changeset = User.email_changeset(user, %{email: "new-address@example.com"})
      assert changeset.valid?
      assert Ecto.Changeset.get_change(changeset, :email) == "new-address@example.com"
    end

    test "requires email", %{user: user} do
      changeset = User.email_changeset(user, %{email: nil})
      assert errors_on(changeset).email
    end

    test "rejects malformed email", %{user: user} do
      changeset = User.email_changeset(user, %{email: "no-at-sign"})
      assert "must have the @ sign and no spaces" in errors_on(changeset).email
    end

    test "rejects email longer than 160 chars", %{user: user} do
      long = String.duplicate("a", 161) <> "@example.com"
      changeset = User.email_changeset(user, %{email: long})
      assert errors_on(changeset).email
    end

    test "errors if email did not change", %{user: user} do
      changeset = User.email_changeset(user, %{email: user.email})
      assert "did not change" in errors_on(changeset).email
    end

    test "enforces email uniqueness against existing users", %{user: user} do
      {:ok, _other} =
        Repo.insert(
          User.registration_changeset(%User{}, %{
            @valid_attrs
            | callsign: "K5OTH",
              email: "taken@example.com"
          })
        )

      changeset = User.email_changeset(user, %{email: "taken@example.com"})
      refute changeset.valid?
      assert "has already been taken" in errors_on(changeset).email
    end

    test "validate_unique: false skips the uniqueness check", %{user: user} do
      {:ok, _other} =
        Repo.insert(
          User.registration_changeset(%User{}, %{
            @valid_attrs
            | callsign: "K5OTH",
              email: "taken@example.com"
          })
        )

      changeset = User.email_changeset(user, %{email: "taken@example.com"}, validate_unique: false)
      assert changeset.valid?
    end
  end

  describe "password_changeset/3" do
    setup do
      {:ok, user} = Repo.insert(User.registration_changeset(%User{}, @valid_attrs))
      %{user: user}
    end

    test "accepts a valid new password and hashes it by default", %{user: user} do
      changeset =
        User.password_changeset(user, %{
          password: "new-password-123",
          password_confirmation: "new-password-123"
        })

      assert changeset.valid?
      hashed = Ecto.Changeset.get_change(changeset, :hashed_password)
      assert is_binary(hashed)
      refute Ecto.Changeset.get_change(changeset, :password)
    end

    test "requires the password", %{user: user} do
      changeset = User.password_changeset(user, %{})
      assert errors_on(changeset).password
    end

    test "rejects password shorter than 8 chars", %{user: user} do
      changeset =
        User.password_changeset(user, %{password: "short", password_confirmation: "short"})

      assert errors_on(changeset).password
    end

    test "rejects password longer than 72 chars", %{user: user} do
      pw = String.duplicate("a", 73)
      changeset = User.password_changeset(user, %{password: pw, password_confirmation: pw})
      assert errors_on(changeset).password
    end

    test "requires password_confirmation to match", %{user: user} do
      changeset =
        User.password_changeset(user, %{
          password: "new-password-123",
          password_confirmation: "other-password-123"
        })

      assert "does not match password" in errors_on(changeset).password_confirmation
    end

    test "hash_password: false keeps plaintext and skips hashing", %{user: user} do
      changeset =
        User.password_changeset(
          user,
          %{password: "new-password-123", password_confirmation: "new-password-123"},
          hash_password: false
        )

      assert changeset.valid?
      refute Ecto.Changeset.get_change(changeset, :hashed_password)
      assert Ecto.Changeset.get_change(changeset, :password) == "new-password-123"
    end
  end

  describe "confirm_changeset/1" do
    test "sets confirmed_at to the current UTC time (truncated to seconds)" do
      user = %User{}
      changeset = User.confirm_changeset(user)
      confirmed_at = Ecto.Changeset.get_change(changeset, :confirmed_at)

      assert %DateTime{} = confirmed_at
      assert confirmed_at.microsecond == {0, 0}
      assert DateTime.diff(DateTime.utc_now(), confirmed_at, :second) in 0..5
    end
  end

  describe "valid_password?/2" do
    test "returns true when the password matches the hash" do
      user = user_fixture()
      assert User.valid_password?(user, valid_user_password())
    end

    test "returns false for an incorrect password" do
      user = user_fixture()
      refute User.valid_password?(user, "wrong password")
    end

    test "returns false when hashed_password is nil" do
      refute User.valid_password?(%User{hashed_password: nil}, "anything")
    end

    test "returns false for empty password input" do
      user = user_fixture()
      refute User.valid_password?(user, "")
    end

    test "returns false for a nil user-like value" do
      refute User.valid_password?(nil, "anything")
    end
  end
end
