# frozen_string_literal: true

require "minitest/autorun"
require "argon2id"

class StringLike
  def initialize(str)
    @str = str
  end

  def to_s
    @str
  end
end

class TestPassword < Minitest::Test
  def test_valid_hash_with_argon2id_hash_returns_true
    assert Argon2id::Password.valid_hash?(
      "$argon2id$v=19$m=65536,t=2,p=1$c29tZXNhbHQ" \
      "$CTFhFdXPJO1aFaMaO6Mm5c8y7cJHAph8ArZWb2GRPPc"
    )
  end

  def test_valid_hash_with_versionless_argon2id_hash_returns_true
    assert Argon2id::Password.valid_hash?(
      "$argon2id$m=65536,t=2,p=1$c29tZXNhbHQ" \
      "$CTFhFdXPJO1aFaMaO6Mm5c8y7cJHAph8ArZWb2GRPPc"
    )
  end

  def test_valid_hash_with_argon2i_hash_returns_false
    refute Argon2id::Password.valid_hash?(
      "$argon2i$m=65536,t=2,p=1$c29tZXNhbHQ" \
      "$9sTbSlTio3Biev89thdrlKKiCaYsjjYVJxGAL3swxpQ"
    )
  end

  def test_valid_hash_with_partial_argon2id_hash_returns_false
    refute Argon2id::Password.valid_hash?(
      "$argon2id$v=19$m=65536,t=2,p=1$c29tZXNhbHQ"
    )
  end

  def test_valid_hash_with_argon2id_hash_with_null_bytes_returns_false
    refute Argon2id::Password.valid_hash?(
      "$argon2id$v=19$m=65536,t=2,p=1$c29tZXNhbHQ" \
      "$CTFhFdXPJO1aFaMaO6Mm5c8y7cJHAph8ArZWb2GRPPc\x00foo"
    )
  end

  def test_valid_hash_with_bcrypt_hash_returns_false
    refute Argon2id::Password.valid_hash?(
      "$2a$12$stsRn7Mi9r02.keRyF4OK.Aq4UWOU185lWggfUQfcupAi.b7AI/nS"
    )
  end

  def test_valid_hash_with_nil_returns_false
    refute Argon2id::Password.valid_hash?(nil)
  end

  def test_valid_hash_with_coercible_argon2id_hash_returns_true
    assert Argon2id::Password.valid_hash?(
      StringLike.new(
        "$argon2id$v=19$m=65536,t=2,p=1$c29tZXNhbHQ" \
        "$CTFhFdXPJO1aFaMaO6Mm5c8y7cJHAph8ArZWb2GRPPc"
      )
    )
  end

  def test_valid_hash_with_coercible_bcrypt_hash_returns_false
    refute Argon2id::Password.valid_hash?(
      StringLike.new(
        "$2a$12$stsRn7Mi9r02.keRyF4OK.Aq4UWOU185lWggfUQfcupAi.b7AI/nS"
      )
    )
  end

  def test_new_m_65536_t_2_p_1_equals_password
    password = Argon2id::Password.new(
      "$argon2id$v=19$m=65536,t=2,p=1$c29tZXNhbHQ" \
      "$CTFhFdXPJO1aFaMaO6Mm5c8y7cJHAph8ArZWb2GRPPc"
    )

    assert password == "password"
  end

  def test_new_m_262144_t_2_p_1_equals_password
    password = Argon2id::Password.new(
      "$argon2id$v=19$m=262144,t=2,p=1$c29tZXNhbHQ" \
      "$eP4eyR+zqlZX1y5xCFTkw9m5GYx0L5YWwvCFvtlbLow"
    )

    assert password == "password"
  end

  def test_new_m_256_t_2_p_1_equals_password
    password = Argon2id::Password.new(
      "$argon2id$v=19$m=256,t=2,p=1$c29tZXNhbHQ" \
      "$nf65EOgLrQMR/uIPnA4rEsF5h7TKyQwu9U1bMCHGi/4"
    )

    assert password == "password"
  end

  def test_new_m_256_t_2_p_2_equals_password
    password = Argon2id::Password.new(
      "$argon2id$v=19$m=256,t=2,p=2$c29tZXNhbHQ" \
      "$bQk8UB/VmZZF4Oo79iDXuL5/0ttZwg2f/5U52iv1cDc"
    )

    assert password == "password"
  end

  def test_new_m_65536_t_1_p_1_equals_password
    password = Argon2id::Password.new(
      "$argon2id$v=19$m=65536,t=1,p=1$c29tZXNhbHQ" \
      "$9qWtwbpyPd3vm1rB1GThgPzZ3/ydHL92zKL+15XZypg"
    )

    assert password == "password"
  end

  def test_new_m_65536_t_4_p_1_equals_password
    password = Argon2id::Password.new(
      "$argon2id$v=19$m=65536,t=4,p=1$c29tZXNhbHQ" \
      "$kCXUjmjvc5XMqQedpMTsOv+zyJEf5PhtGiUghW9jFyw"
    )

    assert password == "password"
  end

  def test_new_m_65536_t_2_p_1_equals_differentpassword
    password = Argon2id::Password.new(
      "$argon2id$v=19$m=65536,t=2,p=1$c29tZXNhbHQ" \
      "$C4TWUs9rDEvq7w3+J4umqA32aWKB1+DSiRuBfYxFj94"
    )

    assert password == "differentpassword"
  end

  def test_new_m_65536_t_2_p_1_with_diffsalt_equals_password
    password = Argon2id::Password.new(
      "$argon2id$v=19$m=65536,t=2,p=1$ZGlmZnNhbHQ" \
      "$vfMrBczELrFdWP0ZsfhWsRPaHppYdP3MVEMIVlqoFBw"
    )

    assert password == "password"
  end

  def test_new_with_versionless_hash_equals_password
    password = Argon2id::Password.new(
      "$argon2id$m=256,t=2,p=1$c29tZXNhbHQ" \
      "$2gcOV25Q8vOKPIl8vdxsf7QCjocJcf+erntOGHkpXm4"
    )

    assert password == "password"
  end

  def test_new_with_non_argon2id_hash_raises_argument_error
    assert_raises(ArgumentError) do
      Argon2id::Password.new(
        "$argon2i$m=65536,t=2,p=1$c29tZXNhbHQ" \
        "$9sTbSlTio3Biev89thdrlKKiCaYsjjYVJxGAL3swxpQ"
      )
    end
  end

  def test_new_with_invalid_hash_raises_argument_error
    assert_raises(ArgumentError) do
      Argon2id::Password.new("not a valid hash")
    end
  end

  def test_new_with_nil_raises_argument_error
    assert_raises(ArgumentError) do
      Argon2id::Password.new(nil)
    end
  end

  def test_new_with_coercible_equals_password
    password = Argon2id::Password.new(
      StringLike.new(
        "$argon2id$v=19$m=256,t=2,p=1$c29tZXNhbHQ" \
        "$nf65EOgLrQMR/uIPnA4rEsF5h7TKyQwu9U1bMCHGi/4"
      )
    )

    assert password == "password"
  end

  def test_encoded_returns_the_full_encoded_hash
    password = Argon2id::Password.new(
      "$argon2id$v=19$m=256,t=2,p=1$c29tZXNhbHQ" \
      "$nf65EOgLrQMR/uIPnA4rEsF5h7TKyQwu9U1bMCHGi/4"
    )

    assert_equal(
      "$argon2id$v=19$m=256,t=2,p=1$c29tZXNhbHQ$nf65EOgLrQMR/uIPnA4rEsF5h7TKyQwu9U1bMCHGi/4",
      password.encoded
    )
  end

  def test_version_returns_the_version
    password = Argon2id::Password.new(
      "$argon2id$v=19$m=256,t=2,p=1$c29tZXNhbHQ" \
      "$nf65EOgLrQMR/uIPnA4rEsF5h7TKyQwu9U1bMCHGi/4"
    )

    assert_equal(19, password.version)
  end

  def test_version_with_no_version_returns_the_default_version
    password = Argon2id::Password.new(
      "$argon2id$m=256,t=2,p=1$c29tZXNhbHQ" \
      "$nf65EOgLrQMR/uIPnA4rEsF5h7TKyQwu9U1bMCHGi/4"
    )

    assert_equal(16, password.version)
  end

  def test_m_cost_returns_m_cost
    password = Argon2id::Password.new(
      "$argon2id$m=256,t=2,p=1$c29tZXNhbHQ" \
      "$nf65EOgLrQMR/uIPnA4rEsF5h7TKyQwu9U1bMCHGi/4"
    )

    assert_equal(256, password.m_cost)
  end

  def test_t_cost_returns_t_cost
    password = Argon2id::Password.new(
      "$argon2id$m=256,t=2,p=1$c29tZXNhbHQ" \
      "$nf65EOgLrQMR/uIPnA4rEsF5h7TKyQwu9U1bMCHGi/4"
    )

    assert_equal(2, password.t_cost)
  end

  def test_parallelism_returns_parallelism
    password = Argon2id::Password.new(
      "$argon2id$m=256,t=2,p=1$c29tZXNhbHQ" \
      "$nf65EOgLrQMR/uIPnA4rEsF5h7TKyQwu9U1bMCHGi/4"
    )

    assert_equal(1, password.parallelism)
  end

  def test_salt_returns_decoded_salt
    password = Argon2id::Password.new(
      "$argon2id$m=256,t=2,p=1$c29tZXNhbHQ" \
      "$nf65EOgLrQMR/uIPnA4rEsF5h7TKyQwu9U1bMCHGi/4"
    )

    assert_equal("somesalt", password.salt)
  end

  def test_salt_returns_decoded_binary_salt
    password = Argon2id::Password.new(
      "$argon2id$v=19$m=256,t=2,p=1$FImSDfu1p8vf1mZBL2PCkg" \
      "$vG4bIkTJGMx6OvkLuKTeq37DTyAf8gF2Ouf3zSLlYVc"
    )

    assert_equal(
      "\x14\x89\x92\r\xFB\xB5\xA7\xCB\xDF\xD6fA/c\xC2\x92".b,
      password.salt
    )
  end

  def test_output_returns_decoded_output
    password = Argon2id::Password.new(
      "$argon2id$v=19$m=65536,t=1,p=1$c29tZXNhbHQ" \
      "$9qWtwbpyPd3vm1rB1GThgPzZ3/ydHL92zKL+15XZypg"
    )

    assert_equal(
      "\xF6\xA5\xAD\xC1\xBAr=\xDD\xEF\x9BZ\xC1\xD4d\xE1\x80\xFC\xD9\xDF\xFC\x9D\x1C\xBFv\xCC\xA2\xFE\xD7\x95\xD9\xCA\x98".b,
      password.output
    )
  end

  def test_to_s_returns_the_full_encoded_hash
    password = Argon2id::Password.new(
      "$argon2id$v=19$m=256,t=2,p=1$c29tZXNhbHQ" \
      "$nf65EOgLrQMR/uIPnA4rEsF5h7TKyQwu9U1bMCHGi/4"
    )

    assert_equal(
      "$argon2id$v=19$m=256,t=2,p=1$c29tZXNhbHQ$nf65EOgLrQMR/uIPnA4rEsF5h7TKyQwu9U1bMCHGi/4",
      password.to_s
    )
  end

  def test_equals_correct_password_returns_true
    password = Argon2id::Password.new(
      "$argon2id$v=19$m=256,t=2,p=1$c29tZXNhbHQ" \
      "$nf65EOgLrQMR/uIPnA4rEsF5h7TKyQwu9U1bMCHGi/4"
    )

    assert password == "password"
  end

  def test_equals_incorrect_password_returns_false
    password = Argon2id::Password.new(
      "$argon2id$v=19$m=256,t=2,p=1$c29tZXNhbHQ" \
      "$nf65EOgLrQMR/uIPnA4rEsF5h7TKyQwu9U1bMCHGi/4"
    )

    refute password == "differentpassword"
  end

  def test_equals_nil_returns_false
    password = Argon2id::Password.new(
      "$argon2id$v=19$m=256,t=2,p=1$c29tZXNhbHQ" \
      "$nf65EOgLrQMR/uIPnA4rEsF5h7TKyQwu9U1bMCHGi/4"
    )

    refute password == nil
  end

  def test_equals_coercible_correct_password_returns_true
    password = Argon2id::Password.new(
      "$argon2id$v=19$m=256,t=2,p=1$c29tZXNhbHQ" \
      "$nf65EOgLrQMR/uIPnA4rEsF5h7TKyQwu9U1bMCHGi/4"
    )

    assert password == StringLike.new("password")
  end

  def test_equals_coercible_incorrect_password_returns_false
    password = Argon2id::Password.new(
      "$argon2id$v=19$m=256,t=2,p=1$c29tZXNhbHQ" \
      "$nf65EOgLrQMR/uIPnA4rEsF5h7TKyQwu9U1bMCHGi/4"
    )

    refute password == StringLike.new("differentpassword")
  end

  def test_is_password_correct_password_returns_true
    password = Argon2id::Password.new(
      "$argon2id$v=19$m=256,t=2,p=1$c29tZXNhbHQ" \
      "$nf65EOgLrQMR/uIPnA4rEsF5h7TKyQwu9U1bMCHGi/4"
    )

    assert password.is_password?("password")
  end

  def test_is_password_incorrect_password_returns_false
    password = Argon2id::Password.new(
      "$argon2id$v=19$m=256,t=2,p=1$c29tZXNhbHQ" \
      "$nf65EOgLrQMR/uIPnA4rEsF5h7TKyQwu9U1bMCHGi/4"
    )

    refute password.is_password?("differentpassword")
  end

  def test_is_password_nil_returns_false
    password = Argon2id::Password.new(
      "$argon2id$v=19$m=256,t=2,p=1$c29tZXNhbHQ" \
      "$nf65EOgLrQMR/uIPnA4rEsF5h7TKyQwu9U1bMCHGi/4"
    )

    refute password.is_password?(nil)
  end

  def test_is_password_coercible_correct_password_returns_true
    password = Argon2id::Password.new(
      "$argon2id$v=19$m=256,t=2,p=1$c29tZXNhbHQ" \
      "$nf65EOgLrQMR/uIPnA4rEsF5h7TKyQwu9U1bMCHGi/4"
    )

    assert password.is_password?(StringLike.new("password"))
  end

  def test_is_password_coercible_incorrect_password_returns_false
    password = Argon2id::Password.new(
      "$argon2id$v=19$m=256,t=2,p=1$c29tZXNhbHQ" \
      "$nf65EOgLrQMR/uIPnA4rEsF5h7TKyQwu9U1bMCHGi/4"
    )

    refute password.is_password?(StringLike.new("differentpassword"))
  end

  def test_create_password_returns_password
    password = Argon2id::Password.create("password")

    assert_instance_of Argon2id::Password, password
  end

  def test_create_password_uses_version_13
    password = Argon2id::Password.create("password")

    assert_equal 0x13, password.version
  end

  def test_create_password_uses_default_t_cost
    password = Argon2id::Password.create("password")

    assert_equal 2, password.t_cost
  end

  def test_create_password_uses_default_m_cost
    password = Argon2id::Password.create("password")

    assert_equal 19_456, password.m_cost
  end

  def test_create_password_uses_default_parallelism
    password = Argon2id::Password.create("password")

    assert_equal 1, password.parallelism
  end

  def test_create_password_uses_default_salt_len
    password = Argon2id::Password.create("password")

    assert_equal 16, password.salt.bytesize
  end

  def test_create_password_uses_default_output_len
    password = Argon2id::Password.create("password")

    assert_equal 32, password.output.bytesize
  end

  def test_create_password_with_t_cost_changes_t_cost
    password = Argon2id::Password.create("password", t_cost: 1)

    assert_equal(1, password.t_cost)
  end

  def test_create_password_with_too_small_t_cost_raises_error
    assert_raises(Argon2id::Error) do
      Argon2id::Password.create("password", t_cost: 0)
    end
  end

  def test_create_password_with_m_cost_changes_m_cost
    password = Argon2id::Password.create("password", m_cost: 8)

    assert_equal(8, password.m_cost)
  end

  def test_create_password_with_too_small_m_cost_raises_error
    assert_raises(Argon2id::Error) do
      Argon2id::Password.create("password", m_cost: 0)
    end
  end

  def test_create_password_with_parallelism_changes_parallelism
    password = Argon2id::Password.create("password", parallelism: 2)

    assert_equal(2, password.parallelism)
  end

  def test_create_password_with_too_small_parallelism_raises_error
    assert_raises(Argon2id::Error) do
      Argon2id::Password.create("password", parallelism: 0)
    end
  end

  def test_create_password_with_too_small_salt_raises_error
    assert_raises(Argon2id::Error) do
      Argon2id::Password.create("password", salt_len: 0)
    end
  end

  def test_create_password_with_output_len_changes_output_len
    password = Argon2id::Password.create("password", output_len: 8)

    assert_equal 8, password.output.bytesize
  end

  def test_create_password_with_too_output_len_raises_error
    assert_raises(Argon2id::Error) do
      Argon2id::Password.create("password", output_len: 0)
    end
  end

  def test_create_password_inherits_t_cost_from_argon2id
    Argon2id.t_cost = 1

    password = Argon2id::Password.create("password")

    assert_equal(1, password.t_cost)
  ensure
    Argon2id.t_cost = Argon2id::DEFAULT_T_COST
  end

  def test_create_password_inherits_m_cost_from_argon2id
    Argon2id.m_cost = 8

    password = Argon2id::Password.create("password")

    assert_equal(8, password.m_cost)
  ensure
    Argon2id.m_cost = Argon2id::DEFAULT_M_COST
  end

  def test_create_password_inherits_parallelism_from_argon2id
    Argon2id.parallelism = 2

    password = Argon2id::Password.create("password")

    assert_equal(2, password.parallelism)
  ensure
    Argon2id.parallelism = Argon2id::DEFAULT_PARALLELISM
  end

  def test_create_password_inherits_salt_len_from_argon2id
    Argon2id.salt_len = 8

    password = Argon2id::Password.create("password")

    assert_equal(8, password.salt.bytesize)
  ensure
    Argon2id.salt_len = Argon2id::DEFAULT_SALT_LEN
  end

  def test_create_password_inherits_output_len_from_argon2id
    Argon2id.output_len = 8

    password = Argon2id::Password.create("password")

    assert_equal(8, password.output.bytesize)
  ensure
    Argon2id.output_len = Argon2id::DEFAULT_OUTPUT_LEN
  end

  def test_create_password_equals_correct_password
    password = Argon2id::Password.create("password")

    assert password == "password"
  end

  def test_create_password_does_not_equal_incorrect_password
    password = Argon2id::Password.create("password")

    refute password == "differentpassword"
  end

  def test_hashing_password_verifies_correct_password
    hash = Argon2id::Password.create("password").to_s
    password = Argon2id::Password.new(hash)

    assert password == "password"
  end

  def test_hashing_password_does_not_verify_incorrect_password
    hash = Argon2id::Password.create("password").to_s
    password = Argon2id::Password.new(hash)

    refute password == "differentpassword"
  end
end
