# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec

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

defmodule AccessTest do
  use ExUnit.Case, async: true

  doctest Access

  # Test nil at compilation time does not fail
  # and that @config[:foo] has proper precedence.
  @config nil
  nil = @config[:foo]

  @config [foo: :bar]
  :bar = @config[:foo]

  @mod :lists
  [1, 2, 3] = @mod.flatten([1, [2], 3])

  @mod -13
  -13 = @mod

  test "for nil" do
    assert nil[:foo] == nil
    assert Access.fetch(nil, :foo) == :error
    assert Access.get(nil, :foo) == nil

    assert_raise ArgumentError, "could not put/update key :foo on a nil value", fn ->
      Access.get_and_update(nil, :foo, fn nil -> {:ok, :bar} end)
    end
  end

  test "for keywords" do
    assert [foo: :bar][:foo] == :bar
    assert [foo: [bar: :baz]][:foo][:bar] == :baz
    assert [foo: [bar: :baz]][:fuu][:bar] == nil

    assert Access.fetch([foo: :bar], :foo) == {:ok, :bar}
    assert Access.fetch([foo: :bar], :bar) == :error

    msg = ~r/the Access calls for keywords expect the key to be an atom/

    assert_raise ArgumentError, msg, fn ->
      Access.fetch([], "foo")
    end

    assert Access.get([foo: :bar], :foo) == :bar
    assert Access.get_and_update([], :foo, fn nil -> {:ok, :baz} end) == {:ok, [foo: :baz]}

    assert Access.get_and_update([foo: :bar], :foo, fn :bar -> {:ok, :baz} end) ==
             {:ok, [foo: :baz]}

    assert Access.pop([foo: :bar], :foo) == {:bar, []}
    assert Access.pop([], :foo) == {nil, []}
  end

  test "for maps" do
    assert %{foo: :bar}[:foo] == :bar
    assert %{1 => 1}[1] == 1
    assert %{1.0 => 1.0}[1.0] == 1.0
    assert %{1 => 1}[1.0] == nil

    assert Access.fetch(%{foo: :bar}, :foo) == {:ok, :bar}
    assert Access.fetch(%{foo: :bar}, :bar) == :error

    assert Access.get(%{foo: :bar}, :foo) == :bar
    assert Access.get_and_update(%{}, :foo, fn nil -> {:ok, :baz} end) == {:ok, %{foo: :baz}}

    assert Access.get_and_update(%{foo: :bar}, :foo, fn :bar -> {:ok, :baz} end) ==
             {:ok, %{foo: :baz}}

    assert Access.pop(%{foo: :bar}, :foo) == {:bar, %{}}
    assert Access.pop(%{}, :foo) == {nil, %{}}
  end

  test "for struct" do
    defmodule Sample do
      defstruct [:name]
    end

    message =
      ~r"function AccessTest.Sample.fetch/2 is undefined \(AccessTest.Sample does not implement the Access behaviour"

    assert_raise UndefinedFunctionError, message, fn ->
      Access.fetch(struct(Sample, []), :name)
    end

    message =
      ~r"function AccessTest.Sample.get_and_update/3 is undefined \(AccessTest.Sample does not implement the Access behaviour"

    assert_raise UndefinedFunctionError, message, fn ->
      Access.get_and_update(struct(Sample, []), :name, fn nil -> {:ok, :baz} end)
    end

    message =
      ~r"function AccessTest.Sample.pop/2 is undefined \(AccessTest.Sample does not implement the Access behaviour"

    assert_raise UndefinedFunctionError, message, fn ->
      Access.pop(struct(Sample, []), :name)
    end
  end

  describe "fetch!/2" do
    assert Access.fetch!(%{foo: :bar}, :foo) == :bar

    assert_raise ArgumentError,
                 ~r/the Access calls for keywords expect the key to be an atom/,
                 fn -> Access.fetch!([], "foo") end

    assert_raise KeyError,
                 ~r/key \"foo\" not found/,
                 fn -> Access.fetch!(nil, "foo") end
  end

  describe "filter/1" do
    @test_list [1, 2, 3, 4, 5, 6]

    test "filters in get_in" do
      assert get_in(@test_list, [Access.filter(&(&1 > 3))]) == [4, 5, 6]
    end

    test "retains order in get_and_update_in" do
      assert get_and_update_in(@test_list, [Access.filter(&(&1 == 3 || &1 == 2))], &{&1 * 2, &1}) ==
               {[4, 6], [1, 2, 3, 4, 5, 6]}
    end

    test "retains order in pop_in" do
      assert pop_in(@test_list, [Access.filter(&(&1 == 3 || &1 == 2))]) == {[2, 3], [1, 4, 5, 6]}
    end

    test "chains with other access functions" do
      mixed_map_and_list = %{foo: Enum.map(@test_list, &%{value: &1})}

      assert get_in(mixed_map_and_list, [:foo, Access.filter(&(&1.value <= 3)), :value]) ==
               [1, 2, 3]
    end
  end

  describe "slice/1" do
    @test_list [1, 2, 3, 4, 5, 6, 7]

    test "retrieves a range from the start of the list" do
      assert [2, 3] == get_in(@test_list, [Access.slice(1..2)])
    end

    test "retrieves a range from the end of the list" do
      assert [6, 7] == get_in(@test_list, [Access.slice(-2..-1)])
    end

    test "retrieves a range from positive first and negative last" do
      assert [2, 3, 4, 5, 6] == get_in(@test_list, [Access.slice(1..-2//1)])
    end

    test "retrieves a range from negative first and positive last" do
      assert [6, 7] == get_in(@test_list, [Access.slice(-2..7//1)])
    end

    test "retrieves a range with steps" do
      assert [1, 3] == get_in(@test_list, [Access.slice(0..2//2)])
      assert [2, 5] == get_in(@test_list, [Access.slice(1..4//3)])
      assert [2] == get_in(@test_list, [Access.slice(1..2//3)])
      assert [1, 3, 5, 7] == get_in(@test_list, [Access.slice(0..6//2)])
    end

    test "pops a range from the start of the list" do
      assert {[2, 3], [1, 4, 5, 6, 7]} == pop_in(@test_list, [Access.slice(1..2)])
    end

    test "pops a range from the end of the list" do
      assert {[6, 7], [1, 2, 3, 4, 5]} == pop_in(@test_list, [Access.slice(-2..-1)])
    end

    test "pops a range from positive first and negative last" do
      assert {[2, 3, 4, 5, 6], [1, 7]} == pop_in(@test_list, [Access.slice(1..-2//1)])
    end

    test "pops a range from negative first and positive last" do
      assert {[6, 7], [1, 2, 3, 4, 5]} == pop_in(@test_list, [Access.slice(-2..7//1)])
    end

    test "pops a range with steps" do
      assert {[1, 3, 5], [2, 4, 6, 7]} == pop_in(@test_list, [Access.slice(0..4//2)])
      assert {[2], [1, 3, 4, 5, 6, 7]} == pop_in(@test_list, [Access.slice(1..2//2)])
      assert {[1, 4], [1, 2, 5, 6, 7]} == pop_in([1, 2, 1, 4, 5, 6, 7], [Access.slice(2..3)])
    end

    test "updates range from the start of the list" do
      assert [-1, 2, 3, 4, 5, 6, 7] == update_in(@test_list, [Access.slice(0..0)], &(&1 * -1))

      assert [1, -2, -3, 4, 5, 6, 7] == update_in(@test_list, [Access.slice(1..2)], &(&1 * -1))
    end

    test "updates range from the end of the list" do
      assert [1, 2, 3, 4, 5, -6, -7] == update_in(@test_list, [Access.slice(-2..-1)], &(&1 * -1))

      assert [-1, -2, 3, 4, 5, 6, 7] == update_in(@test_list, [Access.slice(-7..-6)], &(&1 * -1))
    end

    test "updates a range from positive first and negative last" do
      assert [1, -2, -3, -4, -5, -6, 7] ==
               update_in(@test_list, [Access.slice(1..-2//1)], &(&1 * -1))
    end

    test "updates a range from negative first and positive last" do
      assert [1, 2, 3, 4, 5, -6, -7] ==
               update_in(@test_list, [Access.slice(-2..7//1)], &(&1 * -1))
    end

    test "updates a range with steps" do
      assert [-1, 2, -3, 4, -5, 6, 7] ==
               update_in(@test_list, [Access.slice(0..4//2)], &(&1 * -1))
    end

    test "returns empty when the start of the range is greater than the end" do
      assert [] == get_in(@test_list, [Access.slice(2..1//1)])
    end
  end

  describe "at/1" do
    @test_list [1, 2, 3, 4, 5, 6]

    test "returns element from the end if index is negative" do
      assert get_in(@test_list, [Access.at(-2)]) == 5
    end

    test "returns nil if index is out of bounds counting from the end" do
      assert get_in(@test_list, [Access.at(-10)]) == nil
    end

    test "updates the element counting from the end if index is negative" do
      assert get_and_update_in(@test_list, [Access.at(-2)], fn prev ->
               {prev, :foo}
             end) == {5, [1, 2, 3, 4, :foo, 6]}
    end

    test "returns nil and does not update if index is out of bounds" do
      assert get_and_update_in(@test_list, [Access.at(-10)], fn prev ->
               {prev, :foo}
             end) == {nil, [1, 2, 3, 4, 5, 6]}
    end
  end

  describe "at!/1" do
    @test_list [1, 2, 3, 4, 5, 6]

    test "returns a list element when the index is within bounds, with get_in" do
      assert get_in(@test_list, [Access.at!(5)]) == 6
      assert get_in(@test_list, [Access.at!(-6)]) == 1
    end

    test "updates a list element when the index is within bounds, with get_and_update_in" do
      assert get_and_update_in(@test_list, [Access.at!(5)], fn prev ->
               {prev, :foo}
             end) == {6, [1, 2, 3, 4, 5, :foo]}

      assert get_and_update_in(@test_list, [Access.at!(-6)], fn prev ->
               {prev, :foo}
             end) == {1, [:foo, 2, 3, 4, 5, 6]}
    end

    test "raises OutOfBoundsError when out of bounds, with get_in" do
      assert_raise Enum.OutOfBoundsError, fn ->
        get_in(@test_list, [Access.at!(6)])
      end

      assert_raise Enum.OutOfBoundsError, fn ->
        get_in(@test_list, [Access.at!(-7)])
      end
    end

    test "raises OutOfBoundsError when out of bounds, with get_and_update_in" do
      assert_raise Enum.OutOfBoundsError, fn ->
        get_and_update_in(@test_list, [Access.at!(6)], fn prev -> {prev, :foo} end)
      end

      assert_raise Enum.OutOfBoundsError, fn ->
        get_and_update_in(@test_list, [Access.at!(-7)], fn prev -> {prev, :foo} end)
      end
    end

    test "raises when not given a list" do
      assert_raise RuntimeError, "Access.at!/1 expected a list, got: %{}", fn ->
        get_in(%{}, [Access.at!(0)])
      end
    end

    test "chains" do
      input = %{list: [%{greeting: "hi"}]}
      assert get_in(input, [:list, Access.at!(0), :greeting]) == "hi"
    end
  end

  describe "values/0" do
    @test_map %{a: 1, b: 2, c: 3, d: 4}
    @test_list [a: 1, b: 2, c: 3, d: 4]

    test "retrieves values in a map" do
      assert [1, 2, 3, 4] = get_in(@test_map, [Access.values()]) |> Enum.sort()
    end

    test "retrieves values in a keyword list" do
      assert [1, 2, 3, 4] = get_in(@test_list, [Access.values()])
    end

    test "gets and updates values in a map" do
      assert {gets, %{a: 3, b: 4, c: 5, d: 6}} =
               get_and_update_in(@test_map, [Access.values()], fn n -> {n + 1, n + 2} end)

      assert [2, 3, 4, 5] = Enum.sort(gets)
    end

    test "gets and updates values in a keyword list" do
      assert {[2, 3, 4, 5], [a: 3, b: 4, c: 5, d: 6]} =
               get_and_update_in(@test_list, [Access.values()], fn n -> {n + 1, n + 2} end)
    end

    test "pops values from a map" do
      assert {gets, %{c: 4, d: 5}} =
               get_and_update_in(@test_map, [Access.values()], fn n ->
                 if(n > 2, do: {-n, n + 1}, else: :pop)
               end)

      assert [-4, -3, 1, 2] = Enum.sort(gets)
    end

    test "pops values from a keyword list" do
      assert {[1, 2, -3, -4], [c: 4, d: 5]} =
               get_and_update_in(@test_list, [Access.values()], fn n ->
                 if(n > 2, do: {-n, n + 1}, else: :pop)
               end)
    end

    test "raises when not given a map or a keyword list" do
      message = ~r[^Access.values/0 expected a map or a keyword list, got: .*]

      assert_raise RuntimeError, message, fn ->
        get_in(123, [Access.values()])
      end

      assert_raise RuntimeError, message, fn ->
        get_and_update_in(:some_atom, [Access.values()], fn x -> {x, x} end)
      end

      assert_raise RuntimeError, message, fn ->
        get_in([:a, :b, :c], [Access.values()])
      end

      assert_raise RuntimeError, message, fn ->
        get_in([{:a, :b, :c}, {:d, :e, :f}], [Access.values()])
      end

      assert_raise RuntimeError, message, fn ->
        get_in([{1, 2}, {3, 4}], [Access.values()])
      end
    end
  end
end
