defmodule Xema.Cast.KeywordTest do
  use ExUnit.Case, async: true

  import AssertBlame
  import Xema, only: [cast: 2, cast!: 2, validate: 2]

  alias Xema.{CastError, ValidationError}

  @set [:atom, "str", 1.1, 1, {:tuple}]

  #
  # Xema.cast/2
  #

  describe "cast/2 with a minimal keyword schema" do
    setup do
      %{
        schema: Xema.new(:keyword)
      }
    end

    test "from an empty list", %{schema: schema} do
      data = []
      assert validate(schema, data) == :ok
      assert cast(schema, data) == {:ok, data}
    end

    test "from a keyword list", %{schema: schema} do
      data = [foo: 42]
      assert validate(schema, data) == :ok
      assert cast(schema, data) == {:ok, data}
    end

    test "from a map with atom keys", %{schema: schema} do
      data = %{foo: 42}
      expected = [foo: 42]

      assert {:error,
              %ValidationError{
                reason: %{
                  type: :keyword,
                  value: data
                }
              }} = validate(schema, data)

      assert cast(schema, data) == {:ok, expected}
    end

    test "from a map with string keys", %{schema: schema} do
      data = %{"foo" => 42}
      expected = [foo: 42]
      assert cast(schema, data) == {:ok, expected}
    end

    test "from a map with string keys and unknown atom", %{schema: schema} do
      data = %{"xyz" => 55}
      expected = {:error, CastError.exception(key: "xyz", path: [], to: :keyword)}

      assert cast(schema, data) == expected
    end

    test "from a map with mixed keys", %{schema: schema} do
      data = %{"bla" => "foo", foo: "bla"}
      assert cast(schema, data) == {:ok, [foo: "bla", bla: "foo"]}
    end

    test "from a map with the same key as string and atom", %{schema: schema} do
      data = %{"foo" => "bar", foo: "baz"}
      assert {:error, error} = cast(schema, data)
      assert Exception.message(error) == "cannot cast foo to :keyword key, the key is ambiguous"
    end

    test "from a list", %{schema: schema} do
      data = [4]
      expected = {:error, %ValidationError{reason: %{type: :keyword, value: [4]}}}

      assert cast(schema, data) == expected
    end

    test "from an invalid type", %{schema: schema} do
      Enum.each(@set, fn data ->
        expected = {:error, CastError.exception(path: [], to: :keyword, value: data)}

        assert cast(schema, data) == expected
      end)
    end

    test "from a type without protocol implementation", %{schema: schema} do
      assert_raise Protocol.UndefinedError, fn -> cast(schema, ~r/.*/) end
    end
  end

  describe "cast/2 with a keyword schema" do
    setup do
      %{schema: Xema.new({:keyword, properties: %{str: :string, num: :integer}})}
    end

    test "from an empty list", %{schema: schema} do
      data = []
      assert cast(schema, data) == {:ok, data}
    end

    test "from a keyword list", %{schema: schema} do
      data = [foo: 42, str: "foo"]
      assert validate(schema, data) == :ok
      assert {:ok, casted} = cast(schema, data)
      assert Keyword.get(casted, :foo) == 42
      assert Keyword.get(casted, :str) == "foo"
    end

    test "from a keyword list with castable values", %{schema: schema} do
      assert cast(schema, str: 5, num: "6") == {:ok, [str: "5", num: 6]}
    end

    test "from a map with string keys", %{schema: schema} do
      assert {:ok, cast} = cast(schema, %{"foo" => 42, "str" => 6, "num" => "4"})
      assert Keyword.equal?(cast, foo: 42, str: "6", num: 4)
    end

    test "from a map with string keys and unknown atom", %{schema: schema} do
      data = %{"xyz" => 55}
      expected = {:error, CastError.exception(key: "xyz", path: [], to: :keyword)}

      assert cast(schema, data) == expected
    end

    test "from a map with atom keys", %{schema: schema} do
      assert {:ok, cast} = cast(schema, %{foo: 42, str: 6, num: "4"})
      assert Keyword.equal?(cast, foo: 42, str: "6", num: 4)
    end
  end

  describe "cast/2 with a nested keyword schema" do
    setup do
      %{
        schema:
          Xema.new(
            {:keyword,
             properties: %{
               foo: {
                 :keyword,
                 properties: %{str: :string, num: :integer}
               }
             }}
          )
      }
    end

    test "from a keyword list", %{schema: schema} do
      assert cast(schema, foo: [str: 5, num: "7"]) == {:ok, [foo: [str: "5", num: 7]]}
    end

    test "from a keyword list with an invalid value", %{schema: schema} do
      data = [foo: [str: 5, num: "x"]]
      expected = {:error, CastError.exception(path: [:foo, :num], to: :integer, value: "x")}

      assert cast(schema, data) == expected
    end

    test "from a map with string keys", %{schema: schema} do
      assert {:ok, [foo: data]} = cast(schema, %{"foo" => %{"str" => 6, "num" => "8"}})
      assert Keyword.get(data, :str) == "6"
      assert Keyword.get(data, :num) == 8
    end

    test "from a map with string keys and an invalid value", %{schema: schema} do
      data = %{"foo" => %{"str" => 6, "num" => "z"}}
      expected = {:error, CastError.exception(path: ["foo", "num"], to: :integer, value: "z")}

      assert cast(schema, data) == expected
    end

    test "from a map with string keys and an unknown atom", %{schema: schema} do
      data = %{"foo" => %{"str" => 6, "xyz" => "z"}}
      expected = {:error, CastError.exception(path: ["foo"], to: :keyword, key: "xyz")}

      assert cast(schema, data) == expected
    end

    test "from a map with atom keys", %{schema: schema} do
      assert {:ok, [foo: data]} = cast(schema, %{"foo" => %{str: 16, num: "18"}})
      assert Keyword.get(data, :str) == "16"
      assert Keyword.get(data, :num) == 18
    end

    test "from a map with atom keys and an invalid value", %{schema: schema} do
      data = %{foo: %{str: 6, num: "z"}}
      expected = {:error, CastError.exception(path: [:foo, :num], to: :integer, value: "z")}

      assert cast(schema, data) == expected
    end
  end

  #
  # Xema.cast!/2
  #

  describe "cast!/2 with a minimal keyword schema" do
    setup do
      %{
        schema: Xema.new(:keyword)
      }
    end

    test "from an empty list", %{schema: schema} do
      data = []
      assert cast!(schema, data) == data
    end

    test "from a keyword list", %{schema: schema} do
      data = [foo: 42]
      assert cast!(schema, data) == data
    end

    test "from a map with atom keys", %{schema: schema} do
      data = %{foo: 42}
      expected = [foo: 42]
      assert cast!(schema, data) == expected
    end

    test "from a map with string keys", %{schema: schema} do
      data = %{"foo" => 42}
      expected = [foo: 42]
      assert cast!(schema, data) == expected
    end

    test "from a map with string keys and unknown atom", %{schema: schema} do
      data = %{"xyz" => 55}
      msg = ~s|cannot cast "xyz" to :keyword key, the atom is unknown|

      assert_blame CastError, msg, fn -> cast!(schema, data) end
    end

    test "from a list", %{schema: schema} do
      assert_raise ValidationError, "Expected :keyword, got [4].", fn ->
        cast!(schema, [4])
      end
    end

    test "from an invalid type", %{schema: schema} do
      Enum.each(@set, fn data ->
        msg = "cannot cast #{inspect(data)} to :keyword"
        assert_blame CastError, msg, fn -> cast!(schema, data) end
      end)
    end

    test "from a type without protocol implementation", %{schema: schema} do
      assert_raise(Protocol.UndefinedError, fn ->
        cast!(schema, ~r/.*/)
      end)
    end
  end

  describe "cast!/2 with a keyword schema" do
    setup do
      %{
        schema: Xema.new({:keyword, properties: %{str: :string, num: :integer}})
      }
    end

    test "from an empty list", %{schema: schema} do
      data = []
      assert cast!(schema, data) == data
    end

    test "from a keyword list", %{schema: schema} do
      data = [foo: 42, str: "foo"]
      assert cast!(schema, data) == data
    end

    test "from a keyword list with castable values", %{schema: schema} do
      assert cast!(schema, str: 5, num: "6") == [str: "5", num: 6]
    end

    test "from a map with string keys", %{schema: schema} do
      assert cast = cast!(schema, %{"foo" => 42, "str" => 6, "num" => "4"})
      assert Keyword.equal?(cast, foo: 42, str: "6", num: 4)
    end

    test "from a map with string keys and unknown atom", %{schema: schema} do
      data = %{"xyz" => 55}
      msg = ~s|cannot cast "xyz" to :keyword key, the atom is unknown|

      assert_blame CastError, msg, fn -> cast!(schema, data) end
    end

    test "from a map with atom keys", %{schema: schema} do
      assert cast = cast!(schema, %{foo: 42, str: 6, num: "4"})
      assert Keyword.equal?(cast, foo: 42, str: "6", num: 4)
    end
  end

  describe "cast!/2 with a nested keyword schema" do
    setup do
      %{
        schema:
          Xema.new(
            {:keyword,
             properties: %{
               foo: {
                 :keyword,
                 properties: %{str: :string, num: :integer}
               }
             }}
          )
      }
    end

    test "from a keyword list", %{schema: schema} do
      assert cast!(schema, foo: [str: 5, num: "7"]) == [foo: [str: "5", num: 7]]
    end

    test "from a keyword list with an invalid value", %{schema: schema} do
      data = [foo: [str: 5, num: "x"]]
      msg = ~s|cannot cast "x" to :integer at [:foo, :num]|

      assert_blame CastError, msg, fn -> cast!(schema, data) end
    end

    test "from a map with string keys", %{schema: schema} do
      assert [foo: data] = cast!(schema, %{"foo" => %{"str" => 6, "num" => "8"}})
      assert Keyword.get(data, :str) == "6"
      assert Keyword.get(data, :num) == 8
    end

    test "from a map with string keys and an invalid value", %{schema: schema} do
      data = %{"foo" => %{"str" => 6, "num" => "z"}}
      msg = ~s|cannot cast "z" to :integer at ["foo", "num"]|

      assert_blame CastError, msg, fn -> cast!(schema, data) end
    end

    test "from a map with string keys and an unknown atom", %{schema: schema} do
      data = %{"foo" => %{"str" => 6, "xyz" => "z"}}
      msg = ~s|cannot cast "xyz" to :keyword key at ["foo"], the atom is unknown|

      assert_blame CastError, msg, fn -> cast!(schema, data) end
    end

    test "from a map with atom keys", %{schema: schema} do
      assert [foo: data] = cast!(schema, %{"foo" => %{str: 16, num: "18"}})
      assert Keyword.get(data, :str) == "16"
      assert Keyword.get(data, :num) == 18
    end

    test "from a map with atom keys and an invalid value", %{schema: schema} do
      data = %{foo: %{str: 6, num: "z"}}
      msg = ~s|cannot cast "z" to :integer at [:foo, :num]|

      assert_blame CastError, msg, fn -> cast!(schema, data) end
    end
  end
end
