defmodule SnmpKit.SnmpLib.ErrorHandlerTest do
  use ExUnit.Case, async: true

  alias SnmpKit.SnmpLib.ErrorHandler

  describe "with_retry/2" do
    test "succeeds on first attempt" do
      fun = fn -> {:ok, :success} end

      assert {:ok, :success} = ErrorHandler.with_retry(fun)
    end

    test "retries transient errors" do
      # Simulate transient error then success
      agent = Agent.start_link(fn -> 0 end)
      {:ok, pid} = agent

      fun = fn ->
        count = Agent.get_and_update(pid, fn c -> {c, c + 1} end)

        if count < 2 do
          {:error, :timeout}
        else
          {:ok, :success_after_retry}
        end
      end

      assert {:ok, :success_after_retry} = ErrorHandler.with_retry(fun, max_attempts: 3)

      Agent.stop(pid)
    end

    test "fails after max retries" do
      fun = fn -> {:error, :timeout} end

      assert {:error, {:max_retries_exceeded, :timeout}} =
               ErrorHandler.with_retry(fun, max_attempts: 3)
    end

    test "does not retry permanent errors" do
      fun = fn -> {:error, :authentication_failed} end

      # Custom retry condition that returns false for permanent errors
      retry_condition = fn
        :authentication_failed -> false
        _ -> true
      end

      assert {:error, :authentication_failed} =
               ErrorHandler.with_retry(fun,
                 max_attempts: 3,
                 retry_condition: retry_condition
               )
    end

    test "handles exceptions with retry" do
      agent = Agent.start_link(fn -> 0 end)
      {:ok, pid} = agent

      fun = fn ->
        count = Agent.get_and_update(pid, fn c -> {c, c + 1} end)

        if count < 2 do
          raise "temporary failure"
        else
          {:ok, :recovered}
        end
      end

      assert {:ok, :recovered} = ErrorHandler.with_retry(fun, max_attempts: 3, base_delay: 10)

      Agent.stop(pid)
    end

    test "respects custom base delay" do
      fun = fn -> {:error, :timeout} end

      start_time = System.monotonic_time(:millisecond)

      ErrorHandler.with_retry(fun,
        max_attempts: 2,
        base_delay: 100,
        strategy: :fixed,
        jitter_factor: 0
      )

      end_time = System.monotonic_time(:millisecond)

      # Should have at least one 100ms delay (allow for some timing variance)
      assert end_time - start_time >= 90
    end

    test "uses exponential backoff strategy" do
      attempts = Agent.start_link(fn -> [] end)
      {:ok, pid} = attempts

      fun = fn ->
        Agent.update(pid, fn list -> [System.monotonic_time(:millisecond) | list] end)
        {:error, :timeout}
      end

      ErrorHandler.with_retry(fun,
        max_attempts: 3,
        base_delay: 50,
        strategy: :exponential,
        jitter_factor: 0
      )

      timestamps = Agent.get(pid, fn list -> Enum.reverse(list) end)

      # Verify exponential backoff pattern (roughly)
      assert length(timestamps) == 3
      Agent.stop(pid)
    end

    test "uses linear backoff strategy" do
      fun = fn -> {:error, :timeout} end

      result =
        ErrorHandler.with_retry(fun,
          max_attempts: 3,
          base_delay: 10,
          strategy: :linear
        )

      assert {:error, {:max_retries_exceeded, :timeout}} = result
    end
  end

  describe "classify_error/1" do
    test "classifies transient network errors" do
      assert :transient = ErrorHandler.classify_error(:timeout)
      assert :transient = ErrorHandler.classify_error(:nxdomain)
      assert :transient = ErrorHandler.classify_error(:network_unreachable)
      assert :transient = ErrorHandler.classify_error(:connection_refused)
    end

    test "classifies transient device errors" do
      assert :transient = ErrorHandler.classify_error(:device_busy)
      assert :transient = ErrorHandler.classify_error(:too_big)
      assert :transient = ErrorHandler.classify_error(:resource_unavailable)
    end

    test "classifies permanent configuration errors" do
      assert :permanent = ErrorHandler.classify_error(:authentication_failed)
      assert :permanent = ErrorHandler.classify_error(:community_mismatch)
      assert :permanent = ErrorHandler.classify_error(:unsupported_version)
      assert :permanent = ErrorHandler.classify_error(:no_such_name)
      assert :permanent = ErrorHandler.classify_error(:bad_value)
      assert :permanent = ErrorHandler.classify_error(:read_only)
    end

    test "classifies degraded performance errors" do
      assert :degraded = ErrorHandler.classify_error(:slow_response)
      assert :degraded = ErrorHandler.classify_error(:partial_failure)
      assert :degraded = ErrorHandler.classify_error(:high_error_rate)
    end

    test "classifies unknown errors" do
      assert :unknown = ErrorHandler.classify_error(:unknown_error)
      assert :unknown = ErrorHandler.classify_error(:something_weird)
    end

    test "classifies network errors" do
      assert :transient = ErrorHandler.classify_error({:network_error, "connection reset"})
    end
  end

  describe "circuit breaker" do
    test "starts circuit breaker successfully" do
      assert {:ok, pid} = ErrorHandler.start_circuit_breaker("test-device-1")
      assert Process.alive?(pid)
      GenServer.stop(pid)
    end

    test "executes function when circuit is closed" do
      {:ok, breaker} = ErrorHandler.start_circuit_breaker("test-device-2")

      fun = fn -> {:ok, :result} end

      assert {:ok, :result} = ErrorHandler.call_through_breaker(breaker, fun)

      GenServer.stop(breaker)
    end

    test "opens circuit after failure threshold" do
      {:ok, breaker} =
        ErrorHandler.start_circuit_breaker("test-device-3", failure_threshold: 2)

      failing_fun = fn -> {:error, :device_down} end

      # First failure
      assert {:error, :device_down} = ErrorHandler.call_through_breaker(breaker, failing_fun)

      # Second failure - should open circuit
      assert {:error, :device_down} = ErrorHandler.call_through_breaker(breaker, failing_fun)

      # Circuit should be open now
      assert {:error, :circuit_open} = ErrorHandler.call_through_breaker(breaker, failing_fun)

      GenServer.stop(breaker)
    end

    test "transitions from closed to half-open after recovery timeout" do
      {:ok, breaker} =
        ErrorHandler.start_circuit_breaker("test-device-4",
          failure_threshold: 1,
          recovery_timeout: 100
        )

      failing_fun = fn -> {:error, :timeout} end

      # Trigger failure to open circuit
      assert {:error, :timeout} = ErrorHandler.call_through_breaker(breaker, failing_fun)

      # Circuit should be open
      assert {:error, :circuit_open} = ErrorHandler.call_through_breaker(breaker, failing_fun)

      # Wait for recovery timeout
      Process.sleep(150)

      # Should allow limited calls in half-open state
      success_fun = fn -> {:ok, :recovered} end
      assert {:ok, :recovered} = ErrorHandler.call_through_breaker(breaker, success_fun)

      GenServer.stop(breaker)
    end

    test "resets circuit breaker" do
      {:ok, breaker} = ErrorHandler.start_circuit_breaker("test-device-5")

      # Get initial state
      state = GenServer.call(breaker, :get_state)
      assert state == :closed

      # Reset
      assert :ok = GenServer.call(breaker, :reset)

      # Verify still closed
      state = GenServer.call(breaker, :get_state)
      assert state == :closed

      GenServer.stop(breaker)
    end
  end

  describe "adaptive_timeout/2" do
    test "returns base timeout for unknown device" do
      timeout = ErrorHandler.adaptive_timeout("invalid.device", base_timeout: 5000)
      assert timeout == 5000
    end

    test "returns base timeout for nil device" do
      timeout = ErrorHandler.adaptive_timeout(nil, base_timeout: 5000)
      assert timeout == 5000
    end

    test "returns base timeout for empty device" do
      timeout = ErrorHandler.adaptive_timeout("", base_timeout: 5000)
      assert timeout == 5000
    end

    test "calculates adaptive timeout for known device" do
      timeout = ErrorHandler.adaptive_timeout("192.168.1.1", base_timeout: 1000, max_timeout: 60_000)

      # Should be at least base timeout
      assert timeout >= 1000
      # Should not exceed max timeout
      assert timeout <= 60_000
    end

    test "respects custom safety factor" do
      timeout1 = ErrorHandler.adaptive_timeout("192.168.1.1", base_timeout: 1000, safety_factor: 1.0)
      timeout2 = ErrorHandler.adaptive_timeout("192.168.1.1", base_timeout: 1000, safety_factor: 3.0)

      # Higher safety factor should give higher timeout (or at minimum be >= base)
      assert timeout2 >= timeout1
    end
  end

  describe "get_device_stats/1" do
    test "returns placeholder stats for valid device" do
      assert {:ok, stats} = ErrorHandler.get_device_stats("192.168.1.1")
      assert stats.device_id == "192.168.1.1"
      assert stats.success_count == 100
      assert stats.failure_count == 5
      assert stats.circuit_state == :closed
      assert stats.quarantine_until == nil
    end

    test "returns error for nil device" do
      assert {:error, :not_found} = ErrorHandler.get_device_stats(nil)
    end

    test "returns error for empty device" do
      assert {:error, :not_found} = ErrorHandler.get_device_stats("")
    end

    test "returns error for invalid device" do
      assert {:error, :not_found} = ErrorHandler.get_device_stats("invalid.device")
    end
  end

  describe "quarantine_device/2" do
    test "quarantines device successfully" do
      assert :ok = ErrorHandler.quarantine_device("192.168.1.1", 300_000)
    end
  end

  describe "quarantined?/1" do
    test "returns false for valid device" do
      refute ErrorHandler.quarantined?("192.168.1.1")
    end

    test "returns false for unknown device" do
      refute ErrorHandler.quarantined?("invalid.device")
    end

    test "returns false for nil device" do
      refute ErrorHandler.quarantined?(nil)
    end
  end
end
