"""Tests for per-user account lockout."""

from datetime import datetime, timedelta, timezone
from unittest.mock import patch

from local_deep_research.security.account_lockout import AccountLockoutManager


class TestAccountLockout:
    """Unit tests for AccountLockoutManager."""

    def _make_manager(self, threshold=3, lockout_minutes=15):
        return AccountLockoutManager(
            threshold=threshold, lockout_minutes=lockout_minutes
        )

    def test_not_locked_initially(self):
        mgr = self._make_manager()
        assert mgr.is_locked("alice") is False

    def test_not_locked_below_threshold(self):
        mgr = self._make_manager(threshold=3)
        mgr.record_failure("alice")
        mgr.record_failure("alice")
        assert mgr.is_locked("alice") is False

    def test_locked_at_threshold(self):
        mgr = self._make_manager(threshold=3)
        for _ in range(3):
            mgr.record_failure("alice")
        assert mgr.is_locked("alice") is True

    def test_success_resets_lockout(self):
        mgr = self._make_manager(threshold=3)
        for _ in range(3):
            mgr.record_failure("alice")
        assert mgr.is_locked("alice") is True
        mgr.record_success("alice")
        assert mgr.is_locked("alice") is False

    def test_counter_resets_after_success(self):
        mgr = self._make_manager(threshold=10)
        for _ in range(9):
            mgr.record_failure("alice")
        mgr.record_success("alice")
        # After reset, 9 more failures should not lock (threshold is 10)
        for _ in range(9):
            mgr.record_failure("alice")
        assert mgr.is_locked("alice") is False

    def test_independent_users(self):
        mgr = self._make_manager(threshold=3)
        for _ in range(3):
            mgr.record_failure("alice")
        assert mgr.is_locked("alice") is True
        assert mgr.is_locked("bob") is False

    def test_success_on_nonexistent_user_is_noop(self):
        mgr = self._make_manager()
        # Should not raise
        mgr.record_success("nonexistent")
        assert mgr.is_locked("nonexistent") is False

    def test_lockout_expires_after_duration(self):
        mgr = self._make_manager(threshold=3, lockout_minutes=15)
        now = datetime(2026, 1, 1, 12, 0, 0, tzinfo=timezone.utc)

        with patch(
            "local_deep_research.security.account_lockout.datetime"
        ) as mock_dt:
            mock_dt.now.return_value = now
            mock_dt.side_effect = lambda *a, **kw: datetime(*a, **kw)
            for _ in range(3):
                mgr.record_failure("alice")

        # Still locked 14 minutes later
        with patch(
            "local_deep_research.security.account_lockout.datetime"
        ) as mock_dt:
            mock_dt.now.return_value = now + timedelta(minutes=14)
            assert mgr.is_locked("alice") is True

        # Unlocked at exactly 15 minutes
        with patch(
            "local_deep_research.security.account_lockout.datetime"
        ) as mock_dt:
            mock_dt.now.return_value = now + timedelta(minutes=15)
            assert mgr.is_locked("alice") is False

    def test_eviction_removes_unlocked_entries(self):
        """Eviction should remove unlocked/expired entries, not blanket clear."""
        mgr = self._make_manager(threshold=5)
        mgr._MAX_STATE_ENTRIES = 100
        now = datetime.now(timezone.utc)

        # Add unlocked entries (count < threshold, no locked_until)
        for i in range(90):
            mgr._state[f"unlocked_{i}"] = {"count": 1, "locked_until": None}

        # Add expired locked entries
        for i in range(10):
            mgr._state[f"expired_{i}"] = {
                "count": 5,
                "locked_until": now - timedelta(minutes=1),
            }

        # Add actively locked entries
        for i in range(5):
            mgr._state[f"locked_{i}"] = {
                "count": 5,
                "locked_until": now + timedelta(minutes=10),
            }

        assert len(mgr._state) == 105

        # Trigger eviction via record_failure
        mgr.record_failure("attacker")

        # Actively locked entries should survive
        for i in range(5):
            assert f"locked_{i}" in mgr._state
        # New entry should exist
        assert "attacker" in mgr._state
        # Unlocked/expired entries should be gone
        assert not any(k.startswith("unlocked_") for k in mgr._state)
        assert not any(k.startswith("expired_") for k in mgr._state)

    def test_blanket_clear_as_last_resort(self):
        """If eviction can't reduce below limit, blanket clear."""
        mgr = self._make_manager(threshold=3)
        mgr._MAX_STATE_ENTRIES = 10
        now = datetime.now(timezone.utc)

        # Fill with actively locked entries (can't be evicted)
        for i in range(15):
            mgr._state[f"locked_{i}"] = {
                "count": 5,
                "locked_until": now + timedelta(hours=1),
            }

        mgr.record_failure("attacker")
        # Should have blanket-cleared then added new entry
        assert len(mgr._state) == 1
        assert "attacker" in mgr._state
