defmodule Cloak.Ciphers.AES.GCMTest do
  use ExUnit.Case

  alias Cloak.Ciphers.AES.GCM, as: Cipher

  @key <<185, 191, 128, 199, 124, 82, 140, 75, 225, 243, 14, 54, 231, 203, 78, 141, 131, 45, 141,
         53, 241, 234, 6, 30, 148, 102, 31, 157, 75, 249, 42, 119>>

  # This hard-coded ciphertext is used for regression tests to ensure
  # that future versions of Cloak can still decrypt ciphertexts generated
  # by v1.0.3, prior to the :crypto changes in Erlang 24
  @v1_0_3_ciphertext <<1, 10, 65, 69, 83, 46, 71, 67, 77, 46, 86, 49, 45, 243, 186, 103, 115, 108,
                       238, 73, 251, 75, 125, 14, 61, 229, 5, 3, 129, 184, 188, 166, 102, 119, 8,
                       200, 77, 44, 248, 164, 147, 51, 162, 220, 42, 127, 129, 148, 79, 164, 65,
                       43, 126, 31, 51, 240, 132, 28, 216, 223, 235, 243, 67, 240, 39, 241>>

  @opts [tag: "AES.GCM.V1", key: @key, iv_length: 12]

  describe ".encrypt/2" do
    test "encrypts binaries" do
      assert Cipher.encrypt("hello world", @opts) != {:ok, "hello world"}
      assert Cipher.encrypt(~S[{"hello": "world"}], @opts) != {:ok, ~S[{"hello": "world"}]}
    end

    test "returns ciphertext in the format key_tag <> iv <> ciphertag <> ciphertext" do
      iv_length = @opts[:iv_length]

      assert {:ok,
              <<_type::binary-1, _length::binary-1, "AES.GCM.V1", iv::binary-size(iv_length),
                ciphertag::binary-16, ciphertext::binary>>} = Cipher.encrypt("plaintext", @opts)

      assert byte_size(iv) == iv_length
      assert byte_size(ciphertag) == 16
      assert String.length(ciphertext) > 0
    end

    test "does not produce the same ciphertext twice" do
      assert Cipher.encrypt("hello world", @opts) != Cipher.encrypt("hello world", @opts)
    end

    test "raises error if :key not passed" do
      assert_raise KeyError, fn ->
        Cipher.encrypt("plaintext", tag: @opts[:tag])
      end
    end

    test "raises error if :tag not passed" do
      assert_raise KeyError, fn ->
        Cipher.encrypt("plaintext", key: @opts[:key])
      end
    end
  end

  describe ".decrypt/2" do
    setup :create_ciphertext

    test "can decrypt a value", %{ciphertext: ciphertext} do
      assert Cipher.decrypt(ciphertext, @opts) == {:ok, "plaintext"}
    end

    test "returns error when decrypting with wrong key", %{ciphertext: ciphertext} do
      assert :error ==
               Cipher.decrypt(ciphertext, tag: "OtherTag", key: :crypto.strong_rand_bytes(32))
    end

    test "returns error if given invalid ciphertext" do
      assert :error == Cipher.decrypt(<<0, 1>>, @opts)
    end

    test "raises error if :key not passed", %{ciphertext: ciphertext} do
      assert_raise KeyError, fn ->
        Cipher.decrypt(ciphertext, tag: @opts[:tag])
      end
    end

    test "raises error if :tag not passed", %{ciphertext: ciphertext} do
      assert_raise KeyError, fn ->
        Cipher.decrypt(ciphertext, key: @opts[:key])
      end
    end

    test "can decrypt a ciphertext generated by Cloak v1.0.3" do
      assert Cipher.decrypt(@v1_0_3_ciphertext, @opts) == {:ok, "Lorem ipsum dolor sit amet"}
    end
  end

  describe ".can_decrypt?/2" do
    setup :create_ciphertext

    test "returns true if tag and format matches", %{ciphertext: ciphertext} do
      assert Cipher.can_decrypt?(ciphertext, @opts)
    end

    test "returns false if tag does not match", %{ciphertext: ciphertext} do
      refute Cipher.can_decrypt?(ciphertext, tag: "OtherTag", key: @opts[:key])
    end

    test "returns false if tag matches but format does not" do
      refute Cipher.can_decrypt?(<<1, 10, "AES.CTR.V1", 0, 0>>, @opts)
    end
  end

  defp create_ciphertext(_) do
    {:ok, ciphertext} = Cipher.encrypt("plaintext", @opts)
    [ciphertext: ciphertext]
  end
end
