defmodule MicrowavepropWeb.Plugs.RemoteIpTest do
  use ExUnit.Case, async: true

  import Plug.Conn
  import Plug.Test

  alias MicrowavepropWeb.Plugs.RemoteIp

  # A trusted peer that looks like a k8s cluster pod or ingress.
  @trusted_peer {10, 42, 0, 1}
  # An external peer: must NOT be able to spoof headers.
  @untrusted_peer {8, 8, 8, 8}

  defp call_plug(conn, opts \\ []) do
    RemoteIp.call(conn, RemoteIp.init(opts))
  end

  defp with_peer(conn, ip) do
    Map.put(conn, :remote_ip, ip)
  end

  describe "call/2 with trusted peer" do
    test "uses cf-connecting-ip when immediate peer is trusted" do
      conn =
        :get
        |> conn("/")
        |> with_peer(@trusted_peer)
        |> put_req_header("cf-connecting-ip", "1.2.3.4")
        |> call_plug()

      assert conn.remote_ip == {1, 2, 3, 4}
    end

    test "falls back to x-forwarded-for when no cf-connecting-ip (trusted peer)" do
      conn =
        :get
        |> conn("/")
        |> with_peer(@trusted_peer)
        |> put_req_header("x-forwarded-for", "5.6.7.8")
        |> call_plug()

      assert conn.remote_ip == {5, 6, 7, 8}
    end

    test "prefers cf-connecting-ip over x-forwarded-for (trusted peer)" do
      conn =
        :get
        |> conn("/")
        |> with_peer(@trusted_peer)
        |> put_req_header("cf-connecting-ip", "1.2.3.4")
        |> put_req_header("x-forwarded-for", "5.6.7.8")
        |> call_plug()

      assert conn.remote_ip == {1, 2, 3, 4}
    end

    test "takes first IP from comma-separated x-forwarded-for (trusted peer)" do
      conn =
        :get
        |> conn("/")
        |> with_peer(@trusted_peer)
        |> put_req_header("x-forwarded-for", "10.0.0.1, 10.0.0.2, 10.0.0.3")
        |> call_plug()

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

    test "preserves original remote_ip when no headers present (trusted peer)" do
      conn =
        :get
        |> conn("/")
        |> with_peer(@trusted_peer)
        |> call_plug()

      assert conn.remote_ip == @trusted_peer
    end

    test "handles IPv6 header value (trusted peer)" do
      conn =
        :get
        |> conn("/")
        |> with_peer(@trusted_peer)
        |> put_req_header("cf-connecting-ip", "2001:db8::1")
        |> call_plug()

      assert conn.remote_ip == {8193, 3512, 0, 0, 0, 0, 0, 1}
    end

    test "handles loopback peer (IPv4)" do
      conn =
        :get
        |> conn("/")
        |> with_peer({127, 0, 0, 1})
        |> put_req_header("cf-connecting-ip", "1.2.3.4")
        |> call_plug()

      assert conn.remote_ip == {1, 2, 3, 4}
    end

    test "handles loopback peer (IPv6 ::1)" do
      conn =
        :get
        |> conn("/")
        |> with_peer({0, 0, 0, 0, 0, 0, 0, 1})
        |> put_req_header("cf-connecting-ip", "1.2.3.4")
        |> call_plug()

      assert conn.remote_ip == {1, 2, 3, 4}
    end

    test "ignores malformed IP in header (trusted peer)" do
      conn =
        :get
        |> conn("/")
        |> with_peer(@trusted_peer)
        |> put_req_header("cf-connecting-ip", "not-an-ip")
        |> call_plug()

      assert conn.remote_ip == @trusted_peer
    end
  end

  describe "call/2 with untrusted peer" do
    test "ignores cf-connecting-ip from untrusted peer (spoof attempt)" do
      conn =
        :get
        |> conn("/")
        |> with_peer(@untrusted_peer)
        |> put_req_header("cf-connecting-ip", "1.2.3.4")
        |> call_plug()

      assert conn.remote_ip == @untrusted_peer
    end

    test "ignores x-forwarded-for from untrusted peer (spoof attempt)" do
      conn =
        :get
        |> conn("/")
        |> with_peer(@untrusted_peer)
        |> put_req_header("x-forwarded-for", "1.2.3.4, 5.6.7.8")
        |> call_plug()

      assert conn.remote_ip == @untrusted_peer
    end

    test "ignores both spoofed headers from untrusted peer" do
      conn =
        :get
        |> conn("/")
        |> with_peer(@untrusted_peer)
        |> put_req_header("cf-connecting-ip", "1.2.3.4")
        |> put_req_header("x-forwarded-for", "9.9.9.9")
        |> call_plug()

      assert conn.remote_ip == @untrusted_peer
    end
  end

  describe "call/2 with IPv6 peers" do
    test "trusts IPv6 ULA peer when configured" do
      # fc00::/7 ULA range — included in default trusted list.
      peer = {0xFC00, 0, 0, 0, 0, 0, 0, 1}

      conn =
        :get
        |> conn("/")
        |> with_peer(peer)
        |> put_req_header("cf-connecting-ip", "1.2.3.4")
        |> call_plug(trusted_proxies: ["fc00::/7"])

      assert conn.remote_ip == {1, 2, 3, 4}
    end

    test "does not trust a public IPv6 peer when only ULA is trusted" do
      peer = {0x2001, 0xDB8, 0, 0, 0, 0, 0, 1}

      conn =
        :get
        |> conn("/")
        |> with_peer(peer)
        |> put_req_header("cf-connecting-ip", "1.2.3.4")
        |> call_plug(trusted_proxies: ["fc00::/7"])

      assert conn.remote_ip == peer
    end
  end

  describe "call/2 with IPv4-mapped IPv6 peers (Bandit dual-stack listener)" do
    # Bandit serves IPv4 connections on a dual-stack socket as IPv4-mapped
    # IPv6 tuples, e.g. cloudflared pod 10.244.0.101 → {0,0,0,0,0,0xffff,
    # 0x0AF4, 0x0065}. Without normalization the trust check sees an IPv6
    # peer and never matches the IPv4 trusted_proxies CIDRs, so
    # cf-connecting-ip stays unread and every request looks like the
    # cloudflared sidecar.

    test "treats ::ffff:10.244.0.101 as the IPv4 cluster pod and trusts it" do
      mapped_peer = {0, 0, 0, 0, 0, 0xFFFF, 0x0AF4, 0x0065}

      conn =
        :get
        |> conn("/")
        |> with_peer(mapped_peer)
        |> put_req_header("cf-connecting-ip", "1.2.3.4")
        |> call_plug()

      assert conn.remote_ip == {1, 2, 3, 4}
    end

    test "normalizes a remaining IPv4-mapped peer to plain IPv4 in the conn" do
      # No forwarded headers — the conn should still walk away with a
      # plain IPv4 remote_ip rather than the mapped IPv6 form so logs
      # and downstream code see consistent IPv4 tuples.
      mapped_peer = {0, 0, 0, 0, 0, 0xFFFF, 0x0AF4, 0x0065}

      conn =
        :get
        |> conn("/")
        |> with_peer(mapped_peer)
        |> call_plug()

      assert conn.remote_ip == {10, 244, 0, 101}
    end

    test "an IPv4-mapped public peer is NOT trusted (no header spoofing)" do
      # ::ffff:8.8.8.8 — public IPv4 reached us directly somehow.
      mapped_public = {0, 0, 0, 0, 0, 0xFFFF, 0x0808, 0x0808}

      conn =
        :get
        |> conn("/")
        |> with_peer(mapped_public)
        |> put_req_header("cf-connecting-ip", "1.2.3.4")
        |> call_plug()

      # The spoof attempt is rejected; the normalized IPv4 peer is used.
      assert conn.remote_ip == {8, 8, 8, 8}
    end
  end

  describe "call/2 with custom trusted_proxies option" do
    test "explicit empty list trusts nothing — even loopback is ignored" do
      conn =
        :get
        |> conn("/")
        |> with_peer({127, 0, 0, 1})
        |> put_req_header("cf-connecting-ip", "1.2.3.4")
        |> call_plug(trusted_proxies: [])

      assert conn.remote_ip == {127, 0, 0, 1}
    end

    test "logs IPv6 peer in canonical hex form, not decimal" do
      # Regression: a public IPv6 client like 2a14:7c1:400:2d::1 was being
      # logged as "10772:1985:1024:45:0:0:0:1" because we stringified the
      # 8-tuple in decimal. Logger metadata should hold the canonical hex.
      peer = {0x2A14, 0x07C1, 0x0400, 0x002D, 0, 0, 0, 1}

      :get
      |> conn("/")
      |> with_peer(peer)
      |> call_plug()

      assert {:remote_ip, ip_str} = List.keyfind(Logger.metadata(), :remote_ip, 0)
      assert ip_str =~ "2a14"
      refute ip_str =~ "10772"
    end

    test "single-host CIDR (/32) trusts only that exact IP" do
      conn =
        :get
        |> conn("/")
        |> with_peer({203, 0, 113, 5})
        |> put_req_header("cf-connecting-ip", "1.2.3.4")
        |> call_plug(trusted_proxies: ["203.0.113.5/32"])

      assert conn.remote_ip == {1, 2, 3, 4}

      conn2 =
        :get
        |> conn("/")
        |> with_peer({203, 0, 113, 6})
        |> put_req_header("cf-connecting-ip", "1.2.3.4")
        |> call_plug(trusted_proxies: ["203.0.113.5/32"])

      assert conn2.remote_ip == {203, 0, 113, 6}
    end
  end
end
