defmodule TarakanWeb.Plugs.ApiRateLimitTest do
  use Tarakan.DataCase, async: true

  import Plug.Conn
  import Plug.Test

  alias Tarakan.Accounts.Scope
  alias TarakanWeb.Plugs.ApiRateLimit

  defp call_ip(conn, opts) do
    ApiRateLimit.call(conn, ApiRateLimit.init([mode: :ip] ++ opts))
  end

  defp call_actor(conn, scope, opts) do
    conn
    |> assign(:current_scope, scope)
    |> ApiRateLimit.call(ApiRateLimit.init([mode: :actor] ++ opts))
  end

  defp credential_scope(account, token_id) do
    Scope.for_account(account, token_id: token_id, authentication_method: :api_credential)
  end

  test "requests carrying a Bearer token count against the IP bucket" do
    opts = [request_limit: 2, mutation_limit: 2, window_seconds: 60]

    for _ <- 1..2 do
      conn =
        conn(:get, "/api/repositories")
        |> put_req_header("authorization", "Bearer garbage-token")
        |> call_ip(opts)

      refute conn.halted
    end

    conn =
      conn(:get, "/api/repositories")
      |> put_req_header("authorization", "Bearer garbage-token")
      |> call_ip(opts)

    assert conn.halted
    assert conn.status == 429
    assert Jason.decode!(conn.resp_body)["error"] == "rate limit exceeded"
  end

  test "IPv6 clients in the same /64 share one IP bucket" do
    opts = [request_limit: 1, mutation_limit: 1, window_seconds: 60]

    first =
      conn(:get, "/api/repositories")
      |> Map.put(:remote_ip, {0x2001, 0x0DB8, 0, 0, 0, 0, 0, 1})
      |> call_ip(opts)

    refute first.halted

    second =
      conn(:get, "/api/repositories")
      |> Map.put(:remote_ip, {0x2001, 0x0DB8, 0, 0, 0xAAAA, 0xBBBB, 0xCCCC, 0xDDDD})
      |> call_ip(opts)

    assert second.halted
    assert second.status == 429
  end

  test "plain requests still count against the IP bucket" do
    opts = [request_limit: 1, mutation_limit: 1, window_seconds: 60]

    refute conn(:get, "/api/repositories") |> call_ip(opts) |> Map.fetch!(:halted)
    assert conn(:get, "/api/repositories") |> call_ip(opts) |> Map.fetch!(:halted)
  end

  test "authenticated actors get the multiplied actor budget" do
    opts = [request_limit: 2, mutation_limit: 2, window_seconds: 60]
    scope = credential_scope(account_fixture(), 11_001)

    # 2 × @actor_multiplier (4) = 8 requests fit; the 9th is rejected.
    for _ <- 1..8 do
      refute conn(:get, "/api/jobs") |> call_actor(scope, opts) |> Map.fetch!(:halted)
    end

    limited = conn(:get, "/api/jobs") |> call_actor(scope, opts)
    assert limited.halted
    assert limited.status == 429
  end
end
