defmodule Ash.Test.ErrorTest do
  @moduledoc false
  use ExUnit.Case, async: true

  alias Ash.Test.Domain, as: Domain

  defmodule TestError do
    use Splode.Error, fields: [:some_field], class: :unknown

    def message(_), do: "WHAT"
  end

  defmodule InvalidError do
    use Splode.Error, fields: [], class: :invalid

    def message(_), do: "WHAT"
  end

  defmodule SpecialError do
    @moduledoc "Used when a flow has been halted for some reason"
    use Splode.Error, fields: [], class: :special

    def message(_), do: "Special error"
  end

  defmodule TestResource do
    use Ash.Resource, domain: Domain, data_layer: Ash.DataLayer.Ets

    actions do
      default_accept :*
      defaults [:read, :destroy, create: :*, update: :*]

      create :create_with_error do
        validate attribute_equals(:id, false)
      end
    end

    attributes do
      uuid_primary_key :id
    end
  end

  describe "to_error_class" do
    test "returns exception if it is a map/struct with class: :special" do
      assert %{class: :special} =
               Ash.Error.to_error_class(SpecialError.exception([]), [])
    end

    test "returns exception if it is a map/struct with class: :special wrapped in a list" do
      assert %{class: :special} =
               Ash.Error.to_error_class([SpecialError.exception([])], [])
    end

    test "returns exception if it is a map/struct with class: :special wrapped in an Invalid error" do
      err = Ash.Error.Invalid.exception(errors: [SpecialError.exception([])])

      assert %{class: :special} = Ash.Error.to_error_class(err, [])
    end

    test "returns chosen error if the value argument is a list of values" do
      values = ["foo", "bar"]

      result = Ash.Error.to_error_class(values, [])

      assert match?(%Ash.Error.Unknown{}, result)

      # given the test arrangement, each error in the error class should
      # * be an Ash.Error.Unknown.UnknownError
      # * have a .class == :unknown
      # * have a .error that is a distinct element of the uniq subset of the values provided

      assert same_elements?(Enum.map(result.errors, fn err -> err.error end), values)

      for err <- result.errors do
        assert match?(%Ash.Error.Unknown.UnknownError{}, err)
        assert err.class == :unknown
      end
    end

    test "returns chosen error if the value argument is a list of errors" do
      err1 = Ash.Error.Unknown.UnknownError.exception(error: :an_error, splode: Ash.Error)
      err2 = InvalidError.exception([])

      result = Ash.Error.to_error_class([err1, err2], [])

      # the parent error will be of the invalid class because it take precedence over unknown
      assert match?(%Ash.Error.Invalid{}, result)

      # the parent error's errors field gets prepended to the list of other errors
      assert same_elements?(result.errors, [err1, err2])
    end

    test "has a context field populated when there is a single error" do
      test_error = TestError.exception([])

      err = Ash.Error.to_error_class(test_error, bread_crumbs: "some context")

      assert err.bread_crumbs == ["some context"]
    end

    test "has a context field populated when there is a list of errors" do
      test_error1 = TestError.exception(some_field: :a)
      test_error2 = TestError.exception(some_field: :b)

      err = Ash.Error.to_error_class([test_error1, test_error2], bread_crumbs: "some context")

      assert err.bread_crumbs == ["some context"]
    end

    test "accumulates bread_crumbs field in child errors" do
      error1 = Ash.Error.to_ash_error("whoops!", nil, bread_crumbs: "some context")
      error2 = Ash.Error.to_ash_error("whoops, again!!", nil, bread_crumbs: "some other context")

      error_class =
        Ash.Error.to_error_class([error1, error2], bread_crumbs: "some higher context")

      child_error_1 = Enum.find(error_class.errors, fn err -> err.error == "whoops!" end)
      assert child_error_1.bread_crumbs == ["some higher context", "some context"]

      child_error_2 = Enum.find(error_class.errors, fn err -> err.error == "whoops, again!!" end)
      assert child_error_2.bread_crumbs == ["some higher context", "some other context"]
    end

    test "accumulates bread_crumbs field in child errors who have no bread_crumbs of their own" do
      error1 = Ash.Error.to_ash_error("whoops!", nil, bread_crumbs: "some context")
      error2 = Ash.Error.to_ash_error("whoops, again!!", nil)

      error_class =
        Ash.Error.to_error_class([error1, error2], bread_crumbs: "some higher context")

      child_error_1 = Enum.find(error_class.errors, fn err -> err.error == "whoops!" end)
      assert child_error_1.bread_crumbs == ["some higher context", "some context"]

      child_error_2 = Enum.find(error_class.errors, fn err -> err.error == "whoops, again!!" end)
      assert child_error_2.bread_crumbs == ["some higher context"]
    end

    test "leaves child error contexts unchanged if no bread_crumbs field provided" do
      error1 = Ash.Error.to_ash_error("whoops!", nil, bread_crumbs: "some context")
      error2 = Ash.Error.to_ash_error("whoops, again!!", nil, bread_crumbs: "some other context")

      error_class = Ash.Error.to_error_class([error1, error2])

      child_error_1 = Enum.find(error_class.errors, fn err -> err.error == "whoops!" end)
      assert child_error_1.bread_crumbs == ["some context"]

      child_error_2 = Enum.find(error_class.errors, fn err -> err.error == "whoops, again!!" end)
      assert child_error_2.bread_crumbs == ["some other context"]
    end

    test "error message contains error context breadcrumbs" do
      error1 = Ash.Error.to_ash_error("whoops!", nil, bread_crumbs: "some context")
      error2 = Ash.Error.to_ash_error("whoops, again!!", nil, bread_crumbs: "some other context")

      error_class =
        Ash.Error.to_error_class([error1, error2], bread_crumbs: "some higher context")

      error_message = Ash.Error.Unknown.message(error_class)

      assert error_message =~ "  > some higher context"
      assert error_message =~ "  > some context"
      assert error_message =~ "  > some other context"
    end

    test "error message still renders when there's no error context" do
      error1 = Ash.Error.to_ash_error("whoops!")
      error2 = Ash.Error.to_ash_error("whoops, again!!")

      error_class = Ash.Error.to_error_class([error1, error2])

      error_message = Ash.Error.Unknown.message(error_class)

      assert error_message =~ "Unknown Error\n\n* whoops!"
    end

    test "has a context field populated in changeset" do
      test_error = TestError.exception([])

      cs = Ash.Changeset.for_create(TestResource, :create)

      err = Ash.Error.to_error_class(test_error, changeset: cs, bread_crumbs: "some context")

      assert err.bread_crumbs == ["some context"]

      [cs_error] = err.changeset.errors
      assert cs_error.bread_crumbs == ["some context"]
    end

    test "a changeset can be passed in directly" do
      error1 = Ash.Error.to_ash_error("whoops!", nil, bread_crumbs: "some context")

      error2 =
        Ash.Error.to_ash_error("whoops, again!!", nil, bread_crumbs: "some other context")

      cs = Ash.Changeset.for_create(TestResource, :create) |> Map.put(:errors, [error1, error2])

      Ash.Test.assert_has_error(cs, Ash.Error.Unknown, fn err ->
        err.error == "whoops!"
      end)

      Ash.Test.refute_has_error(cs, fn err -> err.error == "yay!" end)

      assert clean(Ash.Error.to_error_class(cs)) ==
               clean(Ash.Error.to_error_class([error1, error2], changeset: cs))
    end

    test "accumulates bread_crumbs field in changeset's copy of error hierarchy" do
      error1 = Ash.Error.to_ash_error("whoops!", nil, bread_crumbs: "some context")
      error2 = Ash.Error.to_ash_error("whoops, again!!", nil, bread_crumbs: "some other context")

      cs = Ash.Changeset.for_create(TestResource, :create)

      error_class =
        Ash.Error.to_error_class([error1, error2],
          changeset: cs,
          bread_crumbs: "some higher context"
        )

      cs_child_error_1 =
        Enum.find(error_class.changeset.errors, fn err -> err.error == "whoops!" end)

      cs_child_error_2 =
        Enum.find(error_class.changeset.errors, fn err -> err.error == "whoops, again!!" end)

      assert cs_child_error_1.bread_crumbs == ["some higher context", "some context"]
      assert cs_child_error_2.bread_crumbs == ["some higher context", "some other context"]
    end
  end

  describe "to_ash_error" do
    test "populates bread_crumbs field" do
      error = Ash.Error.to_ash_error("whoops!", nil, bread_crumbs: "some context")

      assert error.bread_crumbs == ["some context"]
    end
  end

  describe "assert_has_error" do
    test "raises if the value is :ok" do
      assert_raise ExUnit.AssertionError, ~r/it had no errors/, fn ->
        Ash.Test.assert_has_error(:ok, Ash.Error.Invalid, fn _err -> true end)
      end
    end

    test "raises if the value is an :ok tuple" do
      assert_raise ExUnit.AssertionError, ~r/it had no errors/, fn ->
        Ash.Test.assert_has_error(
          {:ok, :something_successful},
          Ash.Error.Invalid,
          fn _err -> true end
        )
      end
    end

    test "raises if the value doesn't have any errors of the expected type" do
      changeset = Ash.Changeset.for_create(TestResource, :create_with_error)
      error = Ash.create(changeset)

      assert_raise ExUnit.AssertionError,
                   ~r/Expected the changeset to have errors of class Ash.Error.Unknown/,
                   fn ->
                     Ash.Test.assert_has_error(changeset, Ash.Error.Unknown, fn _err -> true end)
                   end

      assert_raise ExUnit.AssertionError,
                   ~r/Expected the value to have errors of class Ash.Error.Unknown/,
                   fn ->
                     Ash.Test.assert_has_error(error, Ash.Error.Unknown, fn _err -> true end)
                   end
    end

    test "raises if the value has an error of the expected type but doesn't pass the callback" do
      changeset = Ash.Changeset.for_create(TestResource, :create_with_error)
      error = Ash.create(changeset)

      assert_raise ExUnit.AssertionError,
                   ~r/Expected at least one error to match the provided callback/,
                   fn ->
                     Ash.Test.assert_has_error(changeset, Ash.Error.Invalid, fn _err -> false end)
                   end

      assert_raise ExUnit.AssertionError,
                   ~r/Expected at least one error to match the provided callback/,
                   fn ->
                     Ash.Test.assert_has_error(error, Ash.Error.Invalid, fn _err -> false end)
                   end
    end

    test "raises if the value doesn't pass the callback" do
      changeset = Ash.Changeset.for_create(TestResource, :create_with_error)
      error = Ash.create(changeset)

      assert_raise ExUnit.AssertionError,
                   ~r/Expected at least one error to match the provided callback/,
                   fn -> Ash.Test.assert_has_error(changeset, fn _err -> false end) end

      assert_raise ExUnit.AssertionError,
                   ~r/Expected at least one error to match the provided callback/,
                   fn -> Ash.Test.assert_has_error(error, fn _err -> false end) end
    end

    test "passes if the value matches the type and passes the callback" do
      changeset = Ash.Changeset.for_create(TestResource, :create_with_error)
      error = Ash.create(changeset)

      assert Ash.Test.assert_has_error(changeset, Ash.Error.Invalid, fn _err -> true end)
      assert Ash.Test.assert_has_error(error, Ash.Error.Invalid, fn _err -> true end)
    end

    test "passes if the value passes the callback" do
      changeset = Ash.Changeset.for_create(TestResource, :create_with_error)
      error = Ash.create(changeset)

      assert Ash.Test.assert_has_error(changeset, fn _err -> true end)
      assert Ash.Test.assert_has_error(error, fn _err -> true end)
    end
  end

  describe "refute_has_error" do
    test "passes if the value is :ok" do
      Ash.Test.refute_has_error(:ok, fn _err -> true end)
    end

    test "passes if the value is an :ok tuple" do
      Ash.Test.refute_has_error({:ok, :something_successful}, fn _err -> true end)
    end

    test "passes if the value doesn't pass the callback" do
      changeset = Ash.Changeset.for_create(TestResource, :create_with_error)
      error = Ash.create(changeset)

      Ash.Test.refute_has_error(changeset, fn _err -> false end)
      Ash.Test.refute_has_error(error, fn _err -> false end)
    end

    test "raises if the value passes the callback" do
      changeset = Ash.Changeset.for_create(TestResource, :create_with_error)
      error = Ash.create(changeset)

      assert_raise ExUnit.AssertionError,
                   ~r/Expected no errors to match the provided callback/,
                   fn ->
                     Ash.Test.refute_has_error(changeset, fn _err -> true end)
                   end

      assert_raise ExUnit.AssertionError,
                   ~r/Expected no errors to match the provided callback/,
                   fn ->
                     Ash.Test.refute_has_error(error, fn _err -> true end)
                   end
    end
  end

  defp same_elements?(xs, ys) when is_list(xs) and is_list(ys) do
    Enum.sort(clean(xs)) == Enum.sort(clean(ys))
  end

  defp same_elements?(_, _), do: false

  defp clean(list) when is_list(list), do: Enum.map(list, &clean/1)

  defp clean(%{stacktrace: _} = value) do
    %{value | stacktrace: nil, splode: nil}
  end

  defp clean(other), do: other
end
