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

  alias Aprsme.Accounts.User

  describe "registration_changeset/2" do
    test "requires callsign" do
      changeset = User.registration_changeset(%User{}, %{})
      errors = errors_on(changeset)
      assert Map.has_key?(errors, :callsign)
      assert "can't be blank" in errors.callsign
    end

    test "validates callsign format" do
      invalid_callsigns = ["123", "TOOLONGCALLSIGN", "NO SPACES", "nodigit"]

      for callsign <- invalid_callsigns do
        changeset =
          User.registration_changeset(%User{}, %{
            email: "test@example.com",
            password: "valid_password123",
            callsign: callsign
          })

        errors = errors_on(changeset)
        assert Map.has_key?(errors, :callsign), "Expected callsign error for: #{callsign}"
        assert "must be a valid amateur radio callsign" in errors.callsign
      end
    end

    test "validates empty callsign" do
      changeset =
        User.registration_changeset(%User{}, %{
          email: "test@example.com",
          password: "valid_password123",
          callsign: ""
        })

      errors = errors_on(changeset)
      assert Map.has_key?(errors, :callsign)
      assert "can't be blank" in errors.callsign
    end

    test "accepts valid callsigns" do
      valid_callsigns = ["K1ABC", "W2XYZ", "AA3BB", "KG4AAA", "N7PQR", "VE3ABC"]

      for callsign <- valid_callsigns do
        changeset =
          User.registration_changeset(%User{}, %{
            email: "test@example.com",
            password: "valid_password123",
            callsign: callsign
          })

        assert changeset.valid?
      end
    end

    test "converts callsign to uppercase" do
      changeset =
        User.registration_changeset(%User{}, %{
          email: "test@example.com",
          password: "valid_password123",
          callsign: "k1abc"
        })

      assert get_change(changeset, :callsign) == "K1ABC"
    end
  end

  describe "callsign_changeset/2" do
    test "requires callsign to change" do
      user = %User{callsign: "K1ABC"}
      changeset = User.callsign_changeset(user, %{callsign: "K1ABC"})
      errors = errors_on(changeset)
      assert Map.has_key?(errors, :callsign)
      assert "did not change" in errors.callsign
    end

    test "validates new callsign format" do
      user = %User{callsign: "K1ABC"}

      changeset = User.callsign_changeset(user, %{callsign: "INVALID"})
      errors = errors_on(changeset)
      assert Map.has_key?(errors, :callsign)
      assert "must be a valid amateur radio callsign" in errors.callsign
    end

    test "accepts valid callsign change" do
      user = %User{callsign: "K1ABC"}
      changeset = User.callsign_changeset(user, %{callsign: "W2XYZ"})
      assert changeset.valid?
      assert get_change(changeset, :callsign) == "W2XYZ"
    end
  end

  describe "email_changeset/2" do
    test "requires email to change" do
      user = %User{email: "a@b.com"}
      changeset = User.email_changeset(user, %{email: "a@b.com"})
      errors = errors_on(changeset)
      assert Map.has_key?(errors, :email)
      assert "did not change" in errors.email
    end

    test "validates format of the new email" do
      user = %User{email: "a@b.com"}
      changeset = User.email_changeset(user, %{email: "bad email"})
      errors = errors_on(changeset)
      assert "must have the @ sign and no spaces" in errors.email
    end
  end

  describe "password_changeset/2" do
    test "requires matching confirmation" do
      user = %User{}

      changeset =
        User.password_changeset(user, %{
          "password" => "abcdefghijkl",
          "password_confirmation" => "different"
        })

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

    test "hashes password when valid" do
      user = %User{}

      changeset =
        User.password_changeset(user, %{
          "password" => "abcdefghijkl",
          "password_confirmation" => "abcdefghijkl"
        })

      assert changeset.valid?
      assert byte_size(get_change(changeset, :hashed_password)) > 0
      # Plaintext password is dropped after hashing.
      refute get_change(changeset, :password)
    end
  end

  describe "confirm_changeset/1" do
    test "sets confirmed_at to the current time truncated to seconds" do
      changeset = User.confirm_changeset(%User{})
      confirmed_at = get_change(changeset, :confirmed_at)
      assert %NaiveDateTime{} = confirmed_at
      # No microseconds — truncated.
      assert confirmed_at.microsecond == {0, 0}
    end
  end

  describe "valid_password?/2" do
    test "returns false when the user has no hashed password" do
      refute User.valid_password?(%User{hashed_password: nil}, "any")
    end

    test "returns false when the provided password is empty" do
      user = %User{hashed_password: Bcrypt.hash_pwd_salt("hello world!")}
      refute User.valid_password?(user, "")
    end

    test "returns true for the correct password" do
      user = %User{hashed_password: Bcrypt.hash_pwd_salt("hello world!")}
      assert User.valid_password?(user, "hello world!")
    end

    test "returns false for the wrong password" do
      user = %User{hashed_password: Bcrypt.hash_pwd_salt("hello world!")}
      refute User.valid_password?(user, "wrong")
    end
  end

  describe "validate_current_password/2" do
    test "adds an error when the password is wrong" do
      user = %User{hashed_password: Bcrypt.hash_pwd_salt("hello world!")}
      changeset = Ecto.Changeset.change(user)
      changeset = User.validate_current_password(changeset, "wrong")
      errors = errors_on(changeset)
      assert "is not valid" in errors.current_password
    end

    test "does not add an error when the password is correct" do
      user = %User{hashed_password: Bcrypt.hash_pwd_salt("hello world!")}
      changeset = Ecto.Changeset.change(user)
      changeset = User.validate_current_password(changeset, "hello world!")
      assert changeset.valid?
    end
  end

  describe "Inspect protocol" do
    test "redacts the hashed and plaintext passwords" do
      user = %User{
        email: "a@b.com",
        hashed_password: "super-secret-hash",
        password: "super-secret"
      }

      output = inspect(user)
      refute output =~ "super-secret-hash"
      refute output =~ "super-secret"
      # The email is non-redacted, so it should still appear.
      assert output =~ "a@b.com"

      changeset =
        User.password_changeset(%User{}, %{"password" => "newpass12345", "password_confirmation" => "newpass12345"})

      assert changeset.valid?
      assert byte_size(get_change(changeset, :hashed_password)) > 0
    end
  end
end
