defmodule ToweropsWeb.Plugs.BruteForceProtectionTest do
  use Towerops.DataCase, async: true

  import Plug.Conn

  alias Towerops.Security.BruteForce
  alias ToweropsWeb.Plugs.BruteForceProtection

  describe "init/1" do
    test "returns opts unchanged" do
      assert BruteForceProtection.init([]) == []
    end

    test "returns arbitrary opts unchanged" do
      opts = [threshold: 10, window: 120]
      assert BruteForceProtection.init(opts) == opts
    end
  end

  describe "call/2 with non-banned IPs" do
    test "allows requests from non-banned IPs" do
      conn =
        build_conn()
        |> put_req_header("x-forwarded-for", "10.0.0.1")
        |> BruteForceProtection.call([])

      refute conn.halted
      assert conn.status != 403
    end

    test "allows requests from whitelisted IPs even if they would be banned" do
      user = Towerops.AccountsFixtures.user_fixture()
      ip = "198.51.100.#{[:positive] |> System.unique_integer() |> rem(255)}"

      # Whitelist the IP first
      {:ok, _} = BruteForce.add_to_whitelist(ip, "Test whitelist", user)

      # Then ban it
      {:ok, _block} = BruteForce.create_or_escalate_ban(ip, "Test ban")

      conn =
        build_conn()
        |> put_req_header("x-forwarded-for", ip)
        |> BruteForceProtection.call([])

      refute conn.halted
    end

    test "registers before_send callback for 404 tracking" do
      conn =
        build_conn()
        |> put_req_header("x-forwarded-for", "10.0.0.99")
        |> BruteForceProtection.call([])

      refute conn.halted

      # Verify before_send callbacks are registered
      assert conn.private[:before_send] != []
    end
  end

  describe "call/2 IP extraction" do
    test "extracts IP from x-forwarded-for header" do
      conn =
        build_conn()
        |> put_req_header("x-forwarded-for", "203.0.113.50, 10.0.0.1")
        |> BruteForceProtection.call([])

      # Should use the first IP (203.0.113.50) and not be banned
      refute conn.halted
    end

    test "extracts IP from x-real-ip header when x-forwarded-for is absent" do
      conn =
        build_conn()
        |> put_req_header("x-real-ip", "203.0.113.60")
        |> BruteForceProtection.call([])

      refute conn.halted
    end

    test "falls back to conn.remote_ip when proxy headers are absent" do
      conn = BruteForceProtection.call(build_conn(), [])

      refute conn.halted
    end
  end

  describe "call/2 with banned IPs" do
    test "blocks requests from banned IPs with 403 and retry-after header" do
      ip = "198.51.100.#{[:positive] |> System.unique_integer() |> rem(255)}"

      # Create a ban for the IP (5 minute ban for first offense)
      {:ok, _block} = BruteForce.create_or_escalate_ban(ip, "Test ban")

      conn =
        build_conn()
        |> put_req_header("x-forwarded-for", ip)
        |> BruteForceProtection.call([])

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

      # Verify the IP is actually banned via the context
      assert {:blocked, _banned_until} = BruteForce.check_ban_status(ip)
    end

    test "permanent ban returns retry-after of 86400" do
      ip = "198.51.100.#{[:positive] |> System.unique_integer() |> rem(255)}"

      # Escalate to permanent ban (3 offenses)
      {:ok, _} = BruteForce.create_or_escalate_ban(ip, "offense 1")
      {:ok, _} = BruteForce.create_or_escalate_ban(ip, "offense 2")
      {:ok, _} = BruteForce.create_or_escalate_ban(ip, "offense 3")

      # Verify permanent ban status via context
      assert {:blocked, nil} = BruteForce.check_ban_status(ip)
    end
  end

  describe "ban status integration" do
    test "non-banned IP returns :allowed" do
      assert :allowed = BruteForce.check_ban_status("10.0.0.99")
    end

    test "first offense creates a temporary ban" do
      ip = "198.51.100.#{[:positive] |> System.unique_integer() |> rem(255)}"
      {:ok, block} = BruteForce.create_or_escalate_ban(ip, "Test ban")

      assert block.offense_count == 1
      assert block.banned_until
      assert {:blocked, banned_until} = BruteForce.check_ban_status(ip)
      assert banned_until
    end

    test "escalation increases offense count" do
      ip = "198.51.100.#{[:positive] |> System.unique_integer() |> rem(255)}"

      {:ok, block1} = BruteForce.create_or_escalate_ban(ip, "offense 1")
      assert block1.offense_count == 1

      {:ok, block2} = BruteForce.create_or_escalate_ban(ip, "offense 2")
      assert block2.offense_count == 2

      {:ok, block3} = BruteForce.create_or_escalate_ban(ip, "offense 3")
      assert block3.offense_count == 3

      # Third offense is permanent
      assert {:blocked, nil} = BruteForce.check_ban_status(ip)
    end
  end

  defp build_conn do
    Plug.Test.conn(:get, "/some/path")
  end
end
