defmodule TarakanWeb.Plugs.ClientIpTest do
  use ExUnit.Case, async: false

  import Plug.Conn
  import Plug.Test

  alias Tarakan.TrustedProxies
  alias TarakanWeb.Plugs.ClientIp

  setup do
    previous = Application.get_env(:tarakan, :trusted_proxies, [])

    on_exit(fn ->
      Application.put_env(:tarakan, :trusted_proxies, previous)
    end)

    :ok
  end

  test "does not trust X-Forwarded-For without configured proxies" do
    Application.put_env(:tarakan, :trusted_proxies, [])

    conn =
      conn(:get, "/")
      |> Map.put(:remote_ip, {10, 0, 0, 5})
      |> put_req_header("x-forwarded-for", "203.0.113.9")
      |> ClientIp.call([])

    assert conn.remote_ip == {10, 0, 0, 5}
  end

  test "rewrites remote_ip when the peer is a trusted proxy" do
    Application.put_env(:tarakan, :trusted_proxies, TrustedProxies.parse("10.0.0.0/8"))

    conn =
      conn(:get, "/")
      |> Map.put(:remote_ip, {10, 0, 0, 5})
      |> put_req_header("x-forwarded-for", "203.0.113.9, 10.0.0.5")
      |> ClientIp.call([])

    assert conn.remote_ip == {203, 0, 113, 9}
  end

  test "ignores forwarded headers from untrusted peers" do
    Application.put_env(:tarakan, :trusted_proxies, TrustedProxies.parse("10.0.0.0/8"))

    conn =
      conn(:get, "/")
      |> Map.put(:remote_ip, {198, 51, 100, 1})
      |> put_req_header("x-forwarded-for", "203.0.113.9")
      |> ClientIp.call([])

    assert conn.remote_ip == {198, 51, 100, 1}
  end

  test "picks the rightmost untrusted hop, not a client-spoofed leftmost entry" do
    Application.put_env(:tarakan, :trusted_proxies, TrustedProxies.parse("10.0.0.0/8"))

    # Attacker sends a forged leftmost value; the trusted proxy appends the real
    # peer it observed. remote_ip must be the real hop, never the forged one.
    conn =
      conn(:get, "/")
      |> Map.put(:remote_ip, {10, 0, 0, 5})
      |> put_req_header("x-forwarded-for", "203.0.113.9, 198.51.100.7")
      |> ClientIp.call([])

    assert conn.remote_ip == {198, 51, 100, 7}
  end

  test "strips multiple trailing trusted proxies down to the client" do
    Application.put_env(:tarakan, :trusted_proxies, TrustedProxies.parse("10.0.0.0/8"))

    conn =
      conn(:get, "/")
      |> Map.put(:remote_ip, {10, 0, 0, 9})
      |> put_req_header("x-forwarded-for", "203.0.113.9, 10.0.0.5, 10.0.0.9")
      |> ClientIp.call([])

    assert conn.remote_ip == {203, 0, 113, 9}
  end

  test "matches an IPv4 trusted proxy against an IPv6-mapped IPv4 peer" do
    # On a dual-stack (`::`) listener, an IPv4 proxy arrives as ::ffff:10.0.0.5.
    Application.put_env(:tarakan, :trusted_proxies, TrustedProxies.parse("10.0.0.0/8"))

    conn =
      conn(:get, "/")
      |> Map.put(:remote_ip, {0, 0, 0, 0, 0, 0xFFFF, 0x0A00, 0x0005})
      |> put_req_header("x-forwarded-for", "203.0.113.9")
      |> ClientIp.call([])

    assert conn.remote_ip == {203, 0, 113, 9}
  end

  test "normalizes an IPv6-mapped remote_ip to plain IPv4 for rate-limit keys" do
    conn = %Plug.Conn{remote_ip: {0, 0, 0, 0, 0, 0xFFFF, 0xCB00, 0x7109}}
    assert ClientIp.remote_ip_string(conn) == "203.0.113.9"
  end

  test "remote_ip_bucket keeps the full IPv4 address" do
    conn = %Plug.Conn{remote_ip: {203, 0, 113, 9}}
    assert ClientIp.remote_ip_bucket(conn) == {:v4, 203, 0, 113, 9}
  end

  test "remote_ip_bucket truncates IPv6 to the /64 prefix" do
    conn = %Plug.Conn{remote_ip: {0x2001, 0x0DB8, 0x0001, 0x0002, 1, 2, 3, 4}}
    assert ClientIp.remote_ip_bucket(conn) == {:v6, 0x2001, 0x0DB8, 0x0001, 0x0002}
  end

  test "remote_ip_bucket folds IPv4-mapped IPv6 into the IPv4 bucket" do
    conn = %Plug.Conn{remote_ip: {0, 0, 0, 0, 0, 0xFFFF, 0xCB00, 0x7109}}
    assert ClientIp.remote_ip_bucket(conn) == {:v4, 203, 0, 113, 9}
  end
end
