defmodule Towerops.Security.BruteForceTest do
  use Towerops.DataCase, async: true

  alias Towerops.Security.BruteForce
  alias Towerops.Security.IpBlock

  describe "whitelist management" do
    test "check_whitelist/1 returns false for non-whitelisted IP" do
      refute BruteForce.check_whitelist("192.168.1.100")
    end

    test "check_whitelist/1 returns true for exact IP match" do
      user = user_fixture()
      {:ok, _} = BruteForce.add_to_whitelist("192.168.1.100", "Test IP", user)

      assert BruteForce.check_whitelist("192.168.1.100")
    end

    test "check_whitelist/1 returns true for IP in CIDR range" do
      user = user_fixture()
      {:ok, _} = BruteForce.add_to_whitelist("192.168.1.0/24", "Test network", user)

      assert BruteForce.check_whitelist("192.168.1.50")
      assert BruteForce.check_whitelist("192.168.1.1")
      assert BruteForce.check_whitelist("192.168.1.254")
      refute BruteForce.check_whitelist("192.168.2.1")
    end

    test "add_to_whitelist/3 creates whitelist entry" do
      user = user_fixture()

      {:ok, entry} = BruteForce.add_to_whitelist("10.0.0.0/8", "Private network", user)

      assert entry.ip_or_cidr == "10.0.0.0/8"
      assert entry.description == "Private network"
      assert entry.added_by_id == user.id
    end

    test "add_to_whitelist/3 validates IP format" do
      user = user_fixture()

      {:error, changeset} = BruteForce.add_to_whitelist("invalid-ip", "Bad IP", user)

      assert "is not a valid IP address or CIDR range" in errors_on(changeset).ip_or_cidr
    end

    test "add_to_whitelist/3 validates CIDR format" do
      user = user_fixture()

      {:error, changeset} = BruteForce.add_to_whitelist("192.168.1.0/99", "Invalid CIDR", user)

      assert "is not a valid CIDR range" in errors_on(changeset).ip_or_cidr
    end

    test "remove_from_whitelist/1 deletes entry" do
      user = user_fixture()
      {:ok, entry} = BruteForce.add_to_whitelist("192.168.1.100", "Test", user)

      assert :ok = BruteForce.remove_from_whitelist(entry.id)
      refute BruteForce.check_whitelist("192.168.1.100")
    end

    test "list_whitelist/0 returns all entries" do
      user = user_fixture()
      {:ok, _} = BruteForce.add_to_whitelist("192.168.1.1", "IP 1", user)
      {:ok, _} = BruteForce.add_to_whitelist("192.168.1.0/24", "Network", user)

      entries = BruteForce.list_whitelist()

      assert length(entries) == 2
    end
  end

  describe "ban management" do
    test "check_ban_status/1 returns :allowed for non-banned IP" do
      assert :allowed = BruteForce.check_ban_status("192.168.1.100")
    end

    test "check_ban_status/1 returns :blocked for currently banned IP" do
      # Create a temporary ban
      banned_until = DateTime.add(DateTime.utc_now(), 300, :second)

      %IpBlock{}
      |> IpBlock.changeset(%{
        ip_address: "192.168.1.100",
        offense_count: 1,
        banned_until: banned_until,
        last_violation_at: DateTime.utc_now(),
        reason: "Test ban"
      })
      |> Repo.insert!()

      assert {:blocked, returned_until} = BruteForce.check_ban_status("192.168.1.100")
      # Check that ban is active (within 1 second of expected time due to DB precision)
      assert DateTime.diff(returned_until, banned_until, :second) in -1..1
    end

    test "check_ban_status/1 returns :allowed for expired ban" do
      # Create an expired ban
      banned_until = DateTime.add(DateTime.utc_now(), -60, :second)

      %IpBlock{}
      |> IpBlock.changeset(%{
        ip_address: "192.168.1.100",
        offense_count: 1,
        banned_until: banned_until,
        last_violation_at: DateTime.add(DateTime.utc_now(), -120, :second),
        reason: "Test ban"
      })
      |> Repo.insert!()

      assert :allowed = BruteForce.check_ban_status("192.168.1.100")
    end

    test "create_or_escalate_ban/1 creates first offense with 5-minute ban" do
      {:ok, block} = BruteForce.create_or_escalate_ban("192.168.1.100")

      assert block.ip_address == "192.168.1.100"
      assert block.offense_count == 1
      assert DateTime.diff(block.banned_until, DateTime.utc_now(), :second) in 295..305
    end

    test "create_or_escalate_ban/1 escalates to 1-hour ban on second offense" do
      # Create first offense
      {:ok, _} = BruteForce.create_or_escalate_ban("192.168.1.100")

      # Trigger escalation
      {:ok, block} = BruteForce.create_or_escalate_ban("192.168.1.100")

      assert block.offense_count == 2
      assert DateTime.diff(block.banned_until, DateTime.utc_now(), :second) in 3595..3605
    end

    test "create_or_escalate_ban/1 creates permanent ban on third offense" do
      # Create first two offenses
      {:ok, _} = BruteForce.create_or_escalate_ban("192.168.1.100")
      {:ok, _} = BruteForce.create_or_escalate_ban("192.168.1.100")

      # Trigger permanent ban
      {:ok, block} = BruteForce.create_or_escalate_ban("192.168.1.100")

      assert block.offense_count == 3
      assert is_nil(block.banned_until)
    end

    test "create_or_escalate_ban/1 resets after 30 days" do
      # Create old violation (31 days ago)
      %IpBlock{}
      |> IpBlock.changeset(%{
        ip_address: "192.168.1.100",
        offense_count: 2,
        banned_until: DateTime.add(DateTime.utc_now(), -86_400 * 30, :second),
        last_violation_at: DateTime.add(DateTime.utc_now(), -86_400 * 31, :second),
        reason: "Old violation"
      })
      |> Repo.insert!()

      # New violation should reset to first offense
      {:ok, block} = BruteForce.create_or_escalate_ban("192.168.1.100")

      assert block.offense_count == 1
      assert DateTime.diff(block.banned_until, DateTime.utc_now(), :second) in 295..305
    end

    test "manually_unblock/1 removes ban" do
      {:ok, _} = BruteForce.create_or_escalate_ban("192.168.1.100")

      {:ok, _} = BruteForce.manually_unblock("192.168.1.100")

      assert :allowed = BruteForce.check_ban_status("192.168.1.100")
    end

    test "list_blocked_ips/1 filters by permanent status" do
      # Create temporary ban
      {:ok, _} = BruteForce.create_or_escalate_ban("192.168.1.1")

      # Create permanent ban
      {:ok, _} = BruteForce.create_or_escalate_ban("192.168.1.2")
      {:ok, _} = BruteForce.create_or_escalate_ban("192.168.1.2")
      {:ok, _} = BruteForce.create_or_escalate_ban("192.168.1.2")

      permanent = BruteForce.list_blocked_ips(:permanent)
      temporary = BruteForce.list_blocked_ips(:temporary)

      assert length(permanent) == 1
      assert length(temporary) == 1
      assert hd(permanent).ip_address == "192.168.1.2"
      assert hd(temporary).ip_address == "192.168.1.1"
    end
  end

  describe "cleanup" do
    test "delete_expired_bans/0 removes expired temporary bans" do
      # Create expired ban
      banned_until = DateTime.add(DateTime.utc_now(), -60, :second)

      %IpBlock{}
      |> IpBlock.changeset(%{
        ip_address: "192.168.1.100",
        offense_count: 1,
        banned_until: banned_until,
        last_violation_at: DateTime.add(DateTime.utc_now(), -120, :second),
        reason: "Test"
      })
      |> Repo.insert!()

      BruteForce.delete_expired_bans()

      assert [] = Repo.all(IpBlock)
    end

    test "delete_expired_bans/0 keeps active bans" do
      # Create active ban
      banned_until = DateTime.add(DateTime.utc_now(), 300, :second)

      %IpBlock{}
      |> IpBlock.changeset(%{
        ip_address: "192.168.1.100",
        offense_count: 1,
        banned_until: banned_until,
        last_violation_at: DateTime.utc_now(),
        reason: "Test"
      })
      |> Repo.insert!()

      BruteForce.delete_expired_bans()

      assert [_] = Repo.all(IpBlock)
    end

    test "delete_stale_violations/0 removes old non-permanent bans" do
      # Create old violation (91 days ago)
      %IpBlock{}
      |> IpBlock.changeset(%{
        ip_address: "192.168.1.100",
        offense_count: 2,
        banned_until: DateTime.add(DateTime.utc_now(), -86_400 * 90, :second),
        last_violation_at: DateTime.add(DateTime.utc_now(), -86_400 * 91, :second),
        reason: "Old"
      })
      |> Repo.insert!()

      BruteForce.delete_stale_violations()

      assert [] = Repo.all(IpBlock)
    end

    test "delete_stale_violations/0 keeps permanent bans" do
      # Create old permanent ban
      %IpBlock{}
      |> IpBlock.changeset(%{
        ip_address: "192.168.1.100",
        offense_count: 3,
        banned_until: nil,
        last_violation_at: DateTime.add(DateTime.utc_now(), -86_400 * 91, :second),
        reason: "Permanent"
      })
      |> Repo.insert!()

      BruteForce.delete_stale_violations()

      assert [_] = Repo.all(IpBlock)
    end
  end

  defp user_fixture do
    Towerops.AccountsFixtures.user_fixture()
  end
end
