defmodule ToweropsWeb.Plugs.RateLimitTest do
  use ToweropsWeb.ConnCase, async: false

  alias ToweropsWeb.Plugs.RateLimit

  # Use async: false because we're testing Hammer state

  setup do
    # Enable rate limiting for this test module (disabled by default in test env)
    Application.put_env(:towerops, :rate_limiting_enabled, true)

    on_exit(fn ->
      # Restore default test config (disabled)
      Application.put_env(:towerops, :rate_limiting_enabled, false)
    end)

    :ok
  end

  describe "init/1" do
    test "accepts :auth type" do
      assert :auth = RateLimit.init(type: :auth)
    end

    test "accepts :api type" do
      assert :api = RateLimit.init(type: :api)
    end

    test "defaults to :api type" do
      assert :api = RateLimit.init([])
    end

    test "raises on invalid type" do
      assert_raise ArgumentError, ~r/must be :auth or :api/, fn ->
        RateLimit.init(type: :invalid)
      end
    end
  end

  describe "call/2 with :auth type" do
    test "allows requests under the limit", %{conn: conn} do
      # Use a unique IP for this test
      unique_ip = "10.0.0.#{:rand.uniform(255)}"

      conn =
        conn
        |> put_req_header("x-forwarded-for", unique_ip)
        |> RateLimit.call(:auth)

      refute conn.halted
    end

    test "blocks requests over the limit", %{conn: conn} do
      # Use a unique IP for this test
      unique_ip = "10.1.0.#{:rand.uniform(255)}"

      # Make 10 allowed requests (auth limit is 10)
      Enum.each(1..10, fn _ ->
        conn =
          build_conn()
          |> put_req_header("x-forwarded-for", unique_ip)
          |> RateLimit.call(:auth)

        refute conn.halted
      end)

      # 11th request should be blocked
      conn =
        conn
        |> put_req_header("x-forwarded-for", unique_ip)
        |> RateLimit.call(:auth)

      assert conn.halted
      assert conn.status == 429
      assert get_resp_header(conn, "retry-after") != []

      body = Jason.decode!(conn.resp_body)
      assert body["error"] =~ "Too many requests"
    end
  end

  describe "call/2 with :api type" do
    test "allows requests under the limit", %{conn: conn} do
      unique_ip = "10.2.0.#{:rand.uniform(255)}"

      conn =
        conn
        |> put_req_header("x-forwarded-for", unique_ip)
        |> RateLimit.call(:api)

      refute conn.halted
    end

    test "allows higher limit than auth", %{conn: _conn} do
      unique_ip = "10.3.0.#{:rand.uniform(255)}"

      # Make 50 requests - should all be allowed (API limit is 100)
      Enum.each(1..50, fn _ ->
        conn =
          build_conn()
          |> put_req_header("x-forwarded-for", unique_ip)
          |> RateLimit.call(:api)

        refute conn.halted
      end)
    end
  end

  describe "IP extraction" do
    test "uses X-Forwarded-For header first", %{conn: conn} do
      unique_ip = "192.168.1.#{:rand.uniform(255)}"

      # Exhaust the limit for this IP
      Enum.each(1..10, fn _ ->
        build_conn()
        |> put_req_header("x-forwarded-for", unique_ip)
        |> RateLimit.call(:auth)
      end)

      # Different IP should still work
      different_ip = "192.168.2.#{:rand.uniform(255)}"

      conn =
        conn
        |> put_req_header("x-forwarded-for", different_ip)
        |> RateLimit.call(:auth)

      refute conn.halted
    end

    test "handles comma-separated X-Forwarded-For", %{conn: conn} do
      unique_ip = "203.0.113.#{:rand.uniform(255)}"

      # X-Forwarded-For can have multiple IPs (client, proxy1, proxy2)
      conn =
        conn
        |> put_req_header("x-forwarded-for", "#{unique_ip}, 10.0.0.1, 10.0.0.2")
        |> RateLimit.call(:auth)

      refute conn.halted
    end

    test "falls back to X-Real-IP", %{conn: conn} do
      unique_ip = "198.51.100.#{:rand.uniform(255)}"

      conn =
        conn
        |> put_req_header("x-real-ip", unique_ip)
        |> RateLimit.call(:auth)

      refute conn.halted
    end

    test "falls back to remote_ip when no headers", %{conn: conn} do
      # No forwarding headers, uses conn.remote_ip (127.0.0.1)
      # Since other tests might have used this IP, we just verify the plug works
      # by checking it doesn't crash - actual rate limiting is tested elsewhere
      result = RateLimit.call(conn, :auth)
      # Either allowed or blocked is fine - we're testing the IP extraction path
      assert is_struct(result, Plug.Conn)
    end
  end
end
