defmodule Aprsme.ErrorHandlerTest do
  use ExUnit.Case, async: true

  alias Aprsme.ErrorHandler

  # ── helpers to build error structs without hitting complex exception/1 ──

  defp cast_error(message \\ "cannot cast value") do
    %Ecto.Query.CastError{type: :string, value: 42, message: message}
  end

  defp constraint_error(constraint \\ "users_email_index") do
    %Ecto.ConstraintError{type: :unique, constraint: constraint, message: "constraint error"}
  end

  defp postgrex_unique_violation do
    %Postgrex.Error{postgres: %{code: :unique_violation}, message: nil, connection_id: nil}
  end

  defp db_connection_error do
    %DBConnection.ConnectionError{message: "connection refused", severity: :error, reason: :error}
  end

  defp query_error(message \\ "invalid query") do
    %Ecto.QueryError{message: message}
  end

  defp transport_error(reason) do
    %Req.TransportError{reason: reason}
  end

  # ── handle_database_error/2 ──

  describe "handle_database_error/2" do
    test "CastError returns {:error, :invalid_data}" do
      assert {:error, :invalid_data} = ErrorHandler.handle_database_error(cast_error())
    end

    test "ConstraintError returns {:error, :constraint_violation}" do
      assert {:error, :constraint_violation} =
               ErrorHandler.handle_database_error(constraint_error())
    end

    test "Postgrex.Error with unique_violation returns {:error, :duplicate_record}" do
      assert {:error, :duplicate_record} =
               ErrorHandler.handle_database_error(postgrex_unique_violation())
    end

    test "DBConnection.ConnectionError returns {:error, :database_unavailable}" do
      assert {:error, :database_unavailable} =
               ErrorHandler.handle_database_error(db_connection_error())
    end

    test "Ecto.QueryError returns {:error, :query_failed}" do
      assert {:error, :query_failed} = ErrorHandler.handle_database_error(query_error())
    end

    test "catch-all returns {:error, :database_error}" do
      assert {:error, :database_error} =
               ErrorHandler.handle_database_error(%RuntimeError{message: "boom"})
    end

    test "passes context through" do
      assert {:error, :invalid_data} =
               ErrorHandler.handle_database_error(cast_error(), %{table: "users"})
    end
  end

  # ── handle_network_error/3 ──

  describe "handle_network_error/3" do
    test "Req.TransportError with :timeout returns {:error, :timeout}" do
      assert {:error, :timeout} =
               ErrorHandler.handle_network_error(transport_error(:timeout), :aprs_is)
    end

    test "Req.TransportError with :econnrefused returns {:error, :service_unavailable}" do
      assert {:error, :service_unavailable} =
               ErrorHandler.handle_network_error(transport_error(:econnrefused), :aprs_is)
    end

    test "Req.TransportError with other reason returns {:error, :network_error}" do
      assert {:error, :network_error} =
               ErrorHandler.handle_network_error(transport_error(:nxdomain), :geocoder)
    end

    test "{:error, :circuit_open} returns {:error, :service_unavailable}" do
      assert {:error, :service_unavailable} =
               ErrorHandler.handle_network_error({:error, :circuit_open}, :weather_api)
    end

    test "catch-all returns {:error, :network_error}" do
      assert {:error, :network_error} =
               ErrorHandler.handle_network_error(:some_unknown_error, :some_service)
    end

    test "passes context through" do
      assert {:error, :timeout} =
               ErrorHandler.handle_network_error(
                 transport_error(:timeout),
                 :aprs_is,
                 %{attempt: 1}
               )
    end
  end

  # ── handle_packet_error/3 ──

  describe "handle_packet_error/3" do
    test "Jason.DecodeError returns {:error, :invalid_json}" do
      error = %Jason.DecodeError{position: 0, token: nil, data: "bad"}

      assert {:error, :invalid_json} =
               ErrorHandler.handle_packet_error(error, %{sender: "N0CALL"})
    end

    test "ArgumentError with 'invalid' in message returns {:error, :invalid_format}" do
      error = %ArgumentError{message: "invalid byte sequence"}

      assert {:error, :invalid_format} =
               ErrorHandler.handle_packet_error(error, %{sender: "N0CALL"})
    end

    test "ArgumentError without 'invalid' in message returns {:error, :processing_failed}" do
      error = %ArgumentError{message: "bad argument"}

      assert {:error, :processing_failed} =
               ErrorHandler.handle_packet_error(error, %{sender: "N0CALL"})
    end

    test "catch-all returns {:error, :processing_failed}" do
      error = %RuntimeError{message: "something went wrong"}

      assert {:error, :processing_failed} =
               ErrorHandler.handle_packet_error(error, %{sender: "N0CALL"})
    end

    test "uses 'unknown' when packet_data has no :sender key" do
      error = %Jason.DecodeError{position: 0, token: nil, data: "bad"}

      assert {:error, :invalid_json} = ErrorHandler.handle_packet_error(error, %{})
    end

    test "passes context through" do
      error = %Jason.DecodeError{position: 0, token: nil, data: "bad"}

      assert {:error, :invalid_json} =
               ErrorHandler.handle_packet_error(error, %{sender: "N0CALL"}, %{source: "is"})
    end
  end

  # ── handle_process_error/3 ──

  describe "handle_process_error/3" do
    test ":timeout returns {:error, :timeout}" do
      assert {:error, :timeout} = ErrorHandler.handle_process_error(:timeout, :my_genserver)
    end

    test "{:noproc, _} returns {:error, :process_unavailable}" do
      assert {:error, :process_unavailable} =
               ErrorHandler.handle_process_error({:noproc, {GenServer, :call, [:pid]}}, :worker)
    end

    test "{:badrpc, reason} returns {:error, :rpc_failed}" do
      assert {:error, :rpc_failed} =
               ErrorHandler.handle_process_error({:badrpc, :nodedown}, :remote_node)
    end

    test "catch-all returns {:error, :process_error}" do
      assert {:error, :process_error} =
               ErrorHandler.handle_process_error(:something_else, :my_process)
    end

    test "passes context through" do
      assert {:error, :timeout} =
               ErrorHandler.handle_process_error(:timeout, :my_genserver, %{call: :get_state})
    end
  end

  # ── with_error_handling/2 ──

  describe "with_error_handling/2" do
    test "success path returns {:ok, value}" do
      assert {:ok, 42} = ErrorHandler.with_error_handling(fn -> 42 end)
    end

    test "success path returns {:ok, value} for complex results" do
      assert {:ok, %{key: "value"}} = ErrorHandler.with_error_handling(fn -> %{key: "value"} end)
    end

    test "raise with no retries goes to categorize_and_handle_error" do
      result =
        ErrorHandler.with_error_handling(fn ->
          raise RuntimeError, "boom"
        end)

      assert {:error, :unknown_error} = result
    end

    test "raise with retries retries before failing" do
      {:ok, counter} = Agent.start_link(fn -> 0 end)

      result =
        ErrorHandler.with_error_handling(
          fn ->
            count = Agent.get_and_update(counter, fn n -> {n, n + 1} end)

            if count < 2 do
              raise RuntimeError, "boom"
            end

            :success
          end,
          max_retries: 2,
          retry_delay: 1
        )

      assert {:ok, :success} = result

      final_count = Agent.get(counter, & &1)
      assert final_count == 3

      Agent.stop(counter)
    end

    test "raise with retries exhausted calls categorize_and_handle_error" do
      {:ok, counter} = Agent.start_link(fn -> 0 end)

      result =
        ErrorHandler.with_error_handling(
          fn ->
            Agent.update(counter, &(&1 + 1))
            raise RuntimeError, "always fails"
          end,
          max_retries: 2,
          retry_delay: 1
        )

      assert {:error, :unknown_error} = result

      # Initial attempt + 2 retries = 3 total calls
      final_count = Agent.get(counter, & &1)
      assert final_count == 3

      Agent.stop(counter)
    end

    test "throw path returns {:error, :unexpected_error}" do
      result =
        ErrorHandler.with_error_handling(fn ->
          throw(:something_bad)
        end)

      assert {:error, :unexpected_error} = result
    end

    test "database error routes through handle_database_error" do
      result =
        ErrorHandler.with_error_handling(fn ->
          raise DBConnection.ConnectionError, "connection refused"
        end)

      assert {:error, :database_unavailable} = result
    end

    test "network error routes through handle_network_error" do
      result =
        ErrorHandler.with_error_handling(fn ->
          raise Req.TransportError, reason: :timeout
        end)

      assert {:error, :timeout} = result
    end

    test "CastError is categorized as a database error" do
      assert {:error, _} =
               ErrorHandler.with_error_handling(fn ->
                 raise Ecto.CastError, type: :integer, value: "x", message: "invalid cast"
               end)
    end

    test "Ecto.ConstraintError is categorized as a database error" do
      assert {:error, _} =
               ErrorHandler.with_error_handling(fn ->
                 raise Ecto.ConstraintError,
                   type: :unique,
                   constraint: "x",
                   action: :insert,
                   changeset: nil,
                   message: "violation"
               end)
    end

    test "Ecto.QueryError is categorized as a database error" do
      assert {:error, _} =
               ErrorHandler.with_error_handling(fn ->
                 raise Ecto.QueryError, query: "fake", message: "bad query"
               end)
    end

    test "Postgrex.Error is categorized as a database error" do
      assert {:error, _} =
               ErrorHandler.with_error_handling(fn ->
                 raise Postgrex.Error, message: "boom"
               end)
    end

    test "circuit_open RuntimeError is categorized as a network error" do
      assert {:error, _} =
               ErrorHandler.with_error_handling(fn ->
                 raise RuntimeError, "circuit_open"
               end)
    end

    test "timeout RuntimeError is categorized as a network error" do
      assert {:error, _} =
               ErrorHandler.with_error_handling(fn ->
                 raise RuntimeError, "timeout"
               end)
    end

    test "map with reason: :circuit_open is categorized as network error via raise" do
      # Raising a struct that pattern-matches the network_error?/1 map clause.
      defmodule CircuitErr do
        @moduledoc false
        defexception [:reason, message: "circuit_open"]
      end

      assert {:error, _} =
               ErrorHandler.with_error_handling(fn ->
                 raise CircuitErr, reason: :circuit_open
               end)
    end

    test "uncategorized error returns {:error, :unknown_error}" do
      result =
        ErrorHandler.with_error_handling(fn ->
          raise ArgumentError, "nope"
        end)

      assert {:error, :unknown_error} = result
    end

    test "accepts context option" do
      assert {:ok, :fine} =
               ErrorHandler.with_error_handling(fn -> :fine end, context: %{source: "test"})
    end
  end
end
