Code.require_file("test_helper.exs", __DIR__)

defmodule KeywordTest do
  use ExUnit.Case, async: true

  doctest Keyword

  test "has a literal syntax" do
    assert [B: 1] == [{:B, 1}]
    assert [foo?: :bar] == [{:foo?, :bar}]
    assert [||: 2, +: 1] == [{:||, 2}, {:+, 1}]
    assert [1, 2, three: :four] == [1, 2, {:three, :four}]
  end

  test "is a :: operator on ambiguity" do
    assert [{:"::", _, [{:a, _, _}, {:b, _, _}]}] = quote(do: [a :: b])
  end

  test "supports optional comma" do
    assert Code.eval_string("[a: 1, b: 2, c: 3,]") == {[a: 1, b: 2, c: 3], []}
  end

  test "implements (almost) all functions in Map" do
    assert Map.__info__(:functions) -- Keyword.__info__(:functions) == [
             from_struct: 1,
             intersect: 2,
             intersect: 3
           ]
  end

  test "get_and_update/3 raises on bad return value from the argument function" do
    message = "the given function must return a two-element tuple or :pop, got: 1"

    assert_raise RuntimeError, message, fn ->
      Keyword.get_and_update([a: 1], :a, fn value -> value end)
    end

    message = "the given function must return a two-element tuple or :pop, got: nil"

    assert_raise RuntimeError, message, fn ->
      Keyword.get_and_update([], :a, fn value -> value end)
    end
  end

  test "get_and_update!/3 raises on bad return value from the argument function" do
    message = "the given function must return a two-element tuple or :pop, got: 1"

    assert_raise RuntimeError, message, fn ->
      Keyword.get_and_update!([a: 1], :a, fn value -> value end)
    end
  end

  test "update!" do
    assert Keyword.update!([a: 1, b: 2, a: 3], :a, &(&1 * 2)) == [a: 2, b: 2]
    assert Keyword.update!([a: 1, b: 2, c: 3], :b, &(&1 * 2)) == [a: 1, b: 4, c: 3]
  end

  test "replace" do
    assert Keyword.replace([a: 1, b: 2, a: 3], :a, :new) == [a: :new, b: 2]
    assert Keyword.replace([a: 1, b: 2, a: 3], :a, 1) == [a: 1, b: 2]
    assert Keyword.replace([a: 1, b: 2, a: 3, b: 4], :a, 1) == [a: 1, b: 2, b: 4]
    assert Keyword.replace([a: 1, b: 2, c: 3, b: 4], :b, :new) == [a: 1, b: :new, c: 3]
    assert Keyword.replace([], :b, :new) == []
    assert Keyword.replace([a: 1, b: 2, a: 3], :c, :new) == [a: 1, b: 2, a: 3]
  end

  test "replace!" do
    assert Keyword.replace!([a: 1, b: 2, a: 3], :a, :new) == [a: :new, b: 2]
    assert Keyword.replace!([a: 1, b: 2, a: 3], :a, 1) == [a: 1, b: 2]
    assert Keyword.replace!([a: 1, b: 2, a: 3, b: 4], :a, 1) == [a: 1, b: 2, b: 4]
    assert Keyword.replace!([a: 1, b: 2, c: 3, b: 4], :b, :new) == [a: 1, b: :new, c: 3]

    assert_raise KeyError, "key :b not found in: []", fn ->
      Keyword.replace!([], :b, :new)
    end

    assert_raise KeyError, "key :c not found in: [a: 1, b: 2, a: 3]", fn ->
      Keyword.replace!([a: 1, b: 2, a: 3], :c, :new)
    end
  end

  test "merge/2" do
    assert Keyword.merge([a: 1, b: 2], c: 11, d: 12) == [a: 1, b: 2, c: 11, d: 12]
    assert Keyword.merge([], c: 11, d: 12) == [c: 11, d: 12]
    assert Keyword.merge([a: 1, b: 2], []) == [a: 1, b: 2]

    message = "expected a keyword list as the first argument, got: [1, 2]"

    assert_raise ArgumentError, message, fn ->
      Keyword.merge([1, 2], c: 11, d: 12)
    end

    message = "expected a keyword list as the first argument, got: [1 | 2]"

    assert_raise ArgumentError, message, fn ->
      Keyword.merge([1 | 2], c: 11, d: 12)
    end

    message = "expected a keyword list as the second argument, got: [11, 12, 0]"

    assert_raise ArgumentError, message, fn ->
      Keyword.merge([a: 1, b: 2], [11, 12, 0])
    end

    message = "expected a keyword list as the second argument, got: [11 | 12]"

    assert_raise ArgumentError, message, fn ->
      Keyword.merge([a: 1, b: 2], [11 | 12])
    end

    # duplicate keys in keywords1 are kept if key is not present in keywords2
    result = [a: 1, b: 2, a: 3, c: 11, d: 12]
    assert Keyword.merge([a: 1, b: 2, a: 3], c: 11, d: 12) == result

    result = [b: 2, a: 11]
    assert Keyword.merge([a: 1, b: 2, a: 3], a: 11) == result

    # duplicate keys in keywords2 are always kept
    result = [a: 1, b: 2, c: 11, c: 12, d: 13]
    assert Keyword.merge([a: 1, b: 2], c: 11, c: 12, d: 13) == result

    # any key in keywords1 is removed if key is present in keyword2
    result = [a: 1, b: 2, c: 11, c: 12, d: 13]
    assert Keyword.merge([a: 1, b: 2, c: 3, c: 4], c: 11, c: 12, d: 13) == result
  end

  test "merge/3" do
    fun = fn _key, value1, value2 -> value1 + value2 end

    assert Keyword.merge([a: 1, b: 2], [c: 11, d: 12], fun) == [a: 1, b: 2, c: 11, d: 12]
    assert Keyword.merge([], [c: 11, d: 12], fun) == [c: 11, d: 12]
    assert Keyword.merge([a: 1, b: 2], [], fun) == [a: 1, b: 2]

    message = "expected a keyword list as the first argument, got: [1, 2]"

    assert_raise ArgumentError, message, fn ->
      Keyword.merge([1, 2], [c: 11, d: 12], fun)
    end

    message = "expected a keyword list as the first argument, got: [1 | 2]"

    assert_raise ArgumentError, message, fn ->
      Keyword.merge([1 | 2], [c: 11, d: 12], fun)
    end

    message = "expected a keyword list as the second argument, got: [{:x, 1}, :y, :z]"

    assert_raise ArgumentError, message, fn ->
      Keyword.merge([a: 1, b: 2], [{:x, 1}, :y, :z], fun)
    end

    message = "expected a keyword list as the second argument, got: [:x | :y]"

    assert_raise ArgumentError, message, fn ->
      Keyword.merge([a: 1, b: 2], [:x | :y], fun)
    end

    message = "expected a keyword list as the second argument, got: [{:x, 1} | :y]"

    assert_raise ArgumentError, message, fn ->
      Keyword.merge([a: 1, b: 2], [{:x, 1} | :y], fun)
    end

    # duplicate keys in keywords1 are left untouched if key is not present in keywords2
    result = [a: 1, b: 2, a: 3, c: 11, d: 12]
    assert Keyword.merge([a: 1, b: 2, a: 3], [c: 11, d: 12], fun) == result

    result = [b: 2, a: 12]
    assert Keyword.merge([a: 1, b: 2, a: 3], [a: 11], fun) == result

    # duplicate keys in keywords2 are always kept
    result = [a: 1, b: 2, c: 11, c: 12, d: 13]
    assert Keyword.merge([a: 1, b: 2], [c: 11, c: 12, d: 13], fun) == result

    # every key in keywords1 is replaced with fun result if key is present in keyword2
    result = [a: 1, b: 2, c: 14, c: 54, d: 13]
    assert Keyword.merge([a: 1, b: 2, c: 3, c: 4], [c: 11, c: 50, d: 13], fun) == result
  end

  test "merge/2 and merge/3 behave exactly the same way" do
    fun = fn _key, _value1, value2 -> value2 end

    args = [
      {[a: 1, b: 2], [c: 11, d: 12]},
      {[], [c: 11, d: 12]},
      {[a: 1, b: 2], []},
      {[a: 1, b: 2, a: 3], [c: 11, d: 12]},
      {[a: 1, b: 2, a: 3], [a: 11]},
      {[a: 1, b: 2], [c: 11, c: 12, d: 13]},
      {[a: 1, b: 2, c: 3, c: 4], [c: 11, c: 12, d: 13]}
    ]

    args_error = [
      {[1, 2], [c: 11, d: 12]},
      {[1 | 2], [c: 11, d: 12]},
      {[a: 1, b: 2], [11, 12, 0]},
      {[a: 1, b: 2], [11 | 12]},
      {[a: 1, b: 2], [{:x, 1}, :y, :z]},
      {[a: 1, b: 2], [:x | :y]},
      {[a: 1, b: 2], [{:x, 1} | :y]}
    ]

    for {arg1, arg2} <- args do
      assert Keyword.merge(arg1, arg2) == Keyword.merge(arg1, arg2, fun)
    end

    for {arg1, arg2} <- args_error do
      error = assert_raise ArgumentError, fn -> Keyword.merge(arg1, arg2) end
      assert_raise ArgumentError, error.message, fn -> Keyword.merge(arg1, arg2, fun) end
    end
  end

  test "validate/2 raises on invalid arguments" do
    assert_raise ArgumentError,
                 "expected a keyword list as first argument, got invalid entry: :three",
                 fn -> Keyword.validate([:three], one: 1, two: 2) end

    assert_raise ArgumentError,
                 "expected the second argument to be a list of atoms or tuples, got: 3",
                 fn -> Keyword.validate([three: 3], [:three, 3, :two]) end
  end

  test "split_with/2" do
    assert Keyword.split_with([], fn {_k, v} -> rem(v, 2) == 0 end) == {[], []}

    assert Keyword.split_with([a: "1", a: 1, b: 2], fn {k, _v} -> k in [:a, :b] end) ==
             {[a: "1", a: 1, b: 2], []}

    assert Keyword.split_with([a: "1", a: 1, b: 2], fn {_k, v} -> v == 5 end) ==
             {[], [a: "1", a: 1, b: 2]}

    assert Keyword.split_with([a: "1", a: 1, b: 2], fn {k, v} -> k in [:a] and is_integer(v) end) ==
             {[a: 1], [a: "1", b: 2]}
  end
end
