"""Tests for security/rate_limiter.py."""

import importlib
import pytest
from unittest.mock import MagicMock, patch
from flask import Flask


def _reload_rate_limiter_with_limits(user_limit, ip_limit):
    """Reload security.rate_limiter with patched upload limits.

    Decorators bind the limit string at module-import time, so functional
    tests must reload the module after patching `load_server_config` to
    re-bind the upload decorators with the test-controlled values.
    """
    from local_deep_research.web import server_config as sc

    fake_config = {
        "host": "0.0.0.0",
        "port": 5000,
        "debug": False,
        "use_https": True,
        "allow_registrations": True,
        "rate_limit_default": "5000 per hour;50000 per day",
        "rate_limit_login": "5 per 15 minutes",
        "rate_limit_registration": "3 per hour",
        "rate_limit_settings": "30 per minute",
        "rate_limit_upload_user": user_limit,
        "rate_limit_upload_ip": ip_limit,
    }
    with patch.object(sc, "load_server_config", return_value=fake_config):
        from local_deep_research.security import rate_limiter

        importlib.reload(rate_limiter)
        return rate_limiter


@pytest.fixture
def app():
    """Create test Flask application."""
    app = Flask(__name__)
    app.config["TESTING"] = True
    return app


class TestGetClientIp:
    """Tests for get_client_ip function."""

    def test_returns_first_ip_from_x_forwarded_for(self, app):
        """Test that first IP from X-Forwarded-For chain is returned."""
        with app.test_request_context(
            environ_base={
                "HTTP_X_FORWARDED_FOR": "192.168.1.100, 10.0.0.1, 172.16.0.1"
            }
        ):
            from local_deep_research.security.rate_limiter import get_client_ip

            result = get_client_ip()
            assert result == "192.168.1.100"

    def test_strips_whitespace_from_forwarded_ip(self, app):
        """Test that whitespace is stripped from forwarded IP."""
        with app.test_request_context(
            environ_base={"HTTP_X_FORWARDED_FOR": "  192.168.1.100  , 10.0.0.1"}
        ):
            from local_deep_research.security.rate_limiter import get_client_ip

            result = get_client_ip()
            assert result == "192.168.1.100"

    def test_returns_x_real_ip_when_no_forwarded_for(self, app):
        """Test that X-Real-IP is used when X-Forwarded-For is absent."""
        with app.test_request_context(
            environ_base={"HTTP_X_REAL_IP": "10.20.30.40"}
        ):
            from local_deep_research.security.rate_limiter import get_client_ip

            result = get_client_ip()
            assert result == "10.20.30.40"

    def test_strips_whitespace_from_real_ip(self, app):
        """Test that whitespace is stripped from X-Real-IP."""
        with app.test_request_context(
            environ_base={"HTTP_X_REAL_IP": "  10.20.30.40  "}
        ):
            from local_deep_research.security.rate_limiter import get_client_ip

            result = get_client_ip()
            assert result == "10.20.30.40"

    def test_falls_back_to_remote_address(self, app):
        """Test fallback to get_remote_address when no proxy headers."""
        with app.test_request_context():
            from local_deep_research.security.rate_limiter import get_client_ip

            # When no proxy headers are set, get_remote_address returns the remote addr
            result = get_client_ip()
            # Result should be some IP (default is typically 127.0.0.1)
            assert result is not None

    def test_prefers_x_forwarded_for_over_x_real_ip(self, app):
        """Test that X-Forwarded-For takes precedence over X-Real-IP."""
        with app.test_request_context(
            environ_base={
                "HTTP_X_FORWARDED_FOR": "192.168.1.1",
                "HTTP_X_REAL_IP": "10.0.0.1",
            }
        ):
            from local_deep_research.security.rate_limiter import get_client_ip

            result = get_client_ip()
            assert result == "192.168.1.1"


class TestRateLimiterConfiguration:
    """Tests for rate limiter configuration."""

    def test_limiter_uses_get_client_ip_as_key_func(self):
        """Test that limiter is configured with get_client_ip as key function."""
        from local_deep_research.security.rate_limiter import (
            limiter,
            get_client_ip,
        )

        assert limiter._key_func == get_client_ip

    def test_limiter_uses_memory_storage(self):
        """Test that limiter uses in-memory storage by default."""
        from local_deep_research.security.rate_limiter import limiter

        # The storage URI should be memory
        assert limiter._storage_uri == "memory://"

    def test_limiter_has_headers_enabled(self):
        """Test that rate limit headers are enabled."""
        from local_deep_research.security.rate_limiter import limiter

        assert limiter._headers_enabled is True

    def test_login_limit_exists(self):
        """Test that login_limit is defined."""
        from local_deep_research.security.rate_limiter import login_limit

        assert login_limit is not None

    def test_registration_limit_exists(self):
        """Test that registration_limit is defined."""
        from local_deep_research.security.rate_limiter import (
            registration_limit,
        )

        assert registration_limit is not None


class TestRateLimitConstants:
    """Tests for rate limit configuration constants."""

    def test_default_rate_limit_loaded_from_config(self):
        """Test that DEFAULT_RATE_LIMIT is loaded from server config."""
        from local_deep_research.security.rate_limiter import (
            DEFAULT_RATE_LIMIT,
        )

        # Should be a string like "X per hour" or similar
        assert isinstance(DEFAULT_RATE_LIMIT, str)
        assert DEFAULT_RATE_LIMIT  # Not empty

    def test_login_rate_limit_loaded_from_config(self):
        """Test that LOGIN_RATE_LIMIT is loaded from server config."""
        from local_deep_research.security.rate_limiter import LOGIN_RATE_LIMIT

        assert isinstance(LOGIN_RATE_LIMIT, str)
        assert LOGIN_RATE_LIMIT  # Not empty

    def test_registration_rate_limit_loaded_from_config(self):
        """Test that REGISTRATION_RATE_LIMIT is loaded from server config."""
        from local_deep_research.security.rate_limiter import (
            REGISTRATION_RATE_LIMIT,
        )

        assert isinstance(REGISTRATION_RATE_LIMIT, str)
        assert REGISTRATION_RATE_LIMIT  # Not empty

    def test_settings_rate_limit_loaded_from_config(self):
        """Test that SETTINGS_RATE_LIMIT is loaded from config with default fallback."""
        from local_deep_research.security.rate_limiter import (
            SETTINGS_RATE_LIMIT,
        )

        assert isinstance(SETTINGS_RATE_LIMIT, str)
        assert SETTINGS_RATE_LIMIT  # Not empty
        # Should contain a rate expression like "30 per minute"
        assert "per" in SETTINGS_RATE_LIMIT


class TestSettingsLimit:
    """Tests for settings_limit shared rate limiter (PR #2021)."""

    def test_settings_limit_exists(self):
        """Test that settings_limit is defined."""
        from local_deep_research.security.rate_limiter import settings_limit

        assert settings_limit is not None

    def test_settings_limit_is_shared_limit(self):
        """Test that settings_limit is a SharedLimitItem from flask-limiter."""
        from local_deep_research.security.rate_limiter import settings_limit

        # SharedLimitItem has a __call__ method (it's a decorator)
        assert callable(settings_limit)

    def test_settings_limit_can_decorate_function(self):
        """Test that settings_limit can be used as a decorator."""
        from local_deep_research.security.rate_limiter import settings_limit

        @settings_limit
        def dummy_view():
            return "ok"

        # The decorated function should still be callable
        assert callable(dummy_view)

    def test_settings_limit_default_value(self):
        """Test that SETTINGS_RATE_LIMIT defaults to 30 per minute."""
        from local_deep_research.security.rate_limiter import (
            SETTINGS_RATE_LIMIT,
        )

        # The config.get uses "30 per minute" as default
        # If not set in config, it should be "30 per minute"
        assert isinstance(SETTINGS_RATE_LIMIT, str)
        assert SETTINGS_RATE_LIMIT  # Not empty


class TestApiRateLimit:
    """Tests for api_rate_limit shared limiter."""

    def test_api_rate_limit_exists(self):
        """Test that api_rate_limit is defined."""
        from local_deep_research.security.rate_limiter import api_rate_limit

        assert api_rate_limit is not None

    def test_api_rate_limit_is_callable(self):
        """Test that api_rate_limit can be used as a decorator."""
        from local_deep_research.security.rate_limiter import api_rate_limit

        assert callable(api_rate_limit)

    def test_get_user_api_rate_limit_default(self, app):
        """Test that _get_user_api_rate_limit returns default when no user."""

        with app.test_request_context():
            from local_deep_research.security.rate_limiter import (
                _get_user_api_rate_limit,
            )

            result = _get_user_api_rate_limit()
            assert result == 60

    def test_get_api_rate_limit_string_format(self, app):
        """Test that _get_api_rate_limit_string returns correct format."""
        from unittest.mock import patch

        with app.test_request_context():
            with patch(
                "local_deep_research.security.rate_limiter._get_user_api_rate_limit",
                return_value=30,
            ):
                from local_deep_research.security.rate_limiter import (
                    _get_api_rate_limit_string,
                )

                result = _get_api_rate_limit_string()
                assert result == "30 per minute"

    def test_is_api_rate_limit_exempt_no_user(self, app):
        """Test that unauthenticated requests are exempt."""
        with app.test_request_context():
            from local_deep_research.security.rate_limiter import (
                _is_api_rate_limit_exempt,
            )

            assert _is_api_rate_limit_exempt() is True

    def test_is_api_rate_limit_exempt_zero_limit(self, app):
        """Test that rate_limit=0 means exempt."""
        from unittest.mock import patch

        app.config["SECRET_KEY"] = "test-secret"
        with app.test_request_context():
            from flask import g

            g.current_user = "testuser"

            with patch(
                "local_deep_research.security.rate_limiter._get_user_api_rate_limit",
                return_value=0,
            ):
                from local_deep_research.security.rate_limiter import (
                    _is_api_rate_limit_exempt,
                )

                assert _is_api_rate_limit_exempt() is True

    def test_get_api_user_key_with_user(self, app):
        """Test that key function returns user-prefixed key."""
        with app.test_request_context():
            from flask import g

            g.current_user = "alice"

            from local_deep_research.security.rate_limiter import (
                _get_api_user_key,
            )

            result = _get_api_user_key()
            assert result == "api_user:alice"


class TestUploadRateLimit:
    """Tests for upload_rate_limit_user and upload_rate_limit_ip shared limiters."""

    def test_upload_rate_limit_user_exists_and_callable(self):
        """Test that upload_rate_limit_user is defined and callable."""
        from local_deep_research.security.rate_limiter import (
            upload_rate_limit_user,
        )

        assert upload_rate_limit_user is not None
        assert callable(upload_rate_limit_user)

    def test_upload_rate_limit_ip_exists_and_callable(self):
        """Test that upload_rate_limit_ip is defined and callable."""
        from local_deep_research.security.rate_limiter import (
            upload_rate_limit_ip,
        )

        assert upload_rate_limit_ip is not None
        assert callable(upload_rate_limit_ip)

    def test_get_upload_user_key_with_user(self, app):
        """Test that upload key function returns user-prefixed key."""
        with app.test_request_context():
            from flask import g

            g.current_user = "bob"

            from local_deep_research.security.rate_limiter import (
                _get_upload_user_key,
            )

            result = _get_upload_user_key()
            assert result == "upload_user:bob"

    def test_get_upload_user_key_no_user(self, app):
        """Test that upload key function falls back to IP when no user."""
        with app.test_request_context():
            from local_deep_research.security.rate_limiter import (
                _get_upload_user_key,
            )

            result = _get_upload_user_key()
            assert result.startswith("upload_ip:")


class TestUploadRateLimitFunctional:
    """Functional tests for dual-key upload rate limiting."""

    def test_upload_rate_limit_enforces_per_user(self):
        """Per-user upload limit blocks after threshold."""
        rate_limiter = _reload_rate_limiter_with_limits(
            user_limit="3 per minute", ip_limit="100 per minute"
        )
        try:
            test_app = Flask(__name__)
            test_app.config["SECRET_KEY"] = "test"
            test_app.config["TESTING"] = True
            test_app.config["RATELIMIT_ENABLED"] = True
            test_app.config["RATELIMIT_STRATEGY"] = "moving-window"

            @test_app.route("/upload", methods=["POST"])
            @rate_limiter.upload_rate_limit_user
            @rate_limiter.upload_rate_limit_ip
            def upload():
                return "ok"

            rate_limiter.limiter.init_app(test_app)

            with test_app.test_client() as c:
                with c.session_transaction() as sess:
                    sess["username"] = "uploader"
                # Per-user limit is "3 per minute" — first 3 pass, 4th is blocked
                for i in range(3):
                    resp = c.post("/upload")
                    assert resp.status_code == 200, (
                        f"Request {i + 1} should pass"
                    )
                resp = c.post("/upload")
                assert resp.status_code == 429

            try:
                rate_limiter.limiter.reset()
            except Exception:
                pass
        finally:
            # Restore module to normal state for subsequent tests
            _reload_rate_limiter_with_limits(
                user_limit="60 per minute;1000 per hour",
                ip_limit="60 per minute;1000 per hour",
            )

    def test_upload_per_user_limit_is_independent(self):
        """Per-user upload bucket is keyed by username, not shared."""
        rate_limiter = _reload_rate_limiter_with_limits(
            user_limit="3 per minute", ip_limit="100 per minute"
        )
        try:
            test_app = Flask(__name__)
            test_app.config["SECRET_KEY"] = "test"
            test_app.config["TESTING"] = True
            test_app.config["RATELIMIT_ENABLED"] = True
            test_app.config["RATELIMIT_STRATEGY"] = "moving-window"

            # Only per-user limit (no per-IP) to isolate user-key behavior
            @test_app.route("/upload", methods=["POST"])
            @rate_limiter.upload_rate_limit_user
            def upload():
                return "ok"

            rate_limiter.limiter.init_app(test_app)

            # User A exhausts their per-user limit
            client_a = test_app.test_client()
            with client_a.session_transaction() as sess:
                sess["username"] = "user_a"
            for _ in range(3):
                client_a.post("/upload")
            assert client_a.post("/upload").status_code == 429

            # User B is unaffected (different user bucket)
            client_b = test_app.test_client()
            with client_b.session_transaction() as sess:
                sess["username"] = "user_b"
            assert client_b.post("/upload").status_code == 200

            try:
                rate_limiter.limiter.reset()
            except Exception:
                pass
        finally:
            _reload_rate_limiter_with_limits(
                user_limit="60 per minute;1000 per hour",
                ip_limit="60 per minute;1000 per hour",
            )

    def test_upload_rate_limit_respects_env_var_override(self):
        """End-to-end: patched config flows through to decorator binding.

        Reloading rate_limiter after patching load_server_config simulates
        the env-var-set-at-process-start scenario without relying on a real
        env var (the module captures _config at import time).
        """
        rate_limiter = _reload_rate_limiter_with_limits(
            user_limit="2 per minute", ip_limit="2 per minute"
        )
        try:
            assert rate_limiter._UPLOAD_RATE_LIMIT_USER == "2 per minute"
            assert rate_limiter._UPLOAD_RATE_LIMIT_IP == "2 per minute"

            test_app = Flask(__name__)
            test_app.config["SECRET_KEY"] = "test"
            test_app.config["TESTING"] = True
            test_app.config["RATELIMIT_ENABLED"] = True
            test_app.config["RATELIMIT_STRATEGY"] = "moving-window"

            @test_app.route("/upload", methods=["POST"])
            @rate_limiter.upload_rate_limit_user
            @rate_limiter.upload_rate_limit_ip
            def upload():
                return "ok"

            rate_limiter.limiter.init_app(test_app)

            with test_app.test_client() as c:
                with c.session_transaction() as sess:
                    sess["username"] = "envtest"
                assert c.post("/upload").status_code == 200
                assert c.post("/upload").status_code == 200
                assert c.post("/upload").status_code == 429

            try:
                rate_limiter.limiter.reset()
            except Exception:
                pass
        finally:
            _reload_rate_limiter_with_limits(
                user_limit="60 per minute;1000 per hour",
                ip_limit="60 per minute;1000 per hour",
            )


class TestApiRateLimitCaching:
    """Tests for g-caching and DB fallback in _get_user_api_rate_limit."""

    def test_g_cache_hit_skips_db(self, app):
        """Second call returns cached value without DB access."""
        with app.test_request_context():
            from flask import g
            from local_deep_research.security.rate_limiter import (
                _get_user_api_rate_limit,
            )

            g._api_rate_limit = 42
            result = _get_user_api_rate_limit()
            assert result == 42

    def test_db_exception_falls_back_to_default(self, app):
        """DB failure returns default 60 without raising."""
        app.config["SECRET_KEY"] = "test"
        with app.test_request_context():
            from flask import g
            from local_deep_research.security.rate_limiter import (
                _get_user_api_rate_limit,
            )

            g.current_user = "testuser"
            with patch(
                "local_deep_research.database.session_context.get_user_db_session",
                side_effect=RuntimeError("DB down"),
            ):
                result = _get_user_api_rate_limit()
                assert result == 60

    def test_custom_rate_limit_from_db(self, app):
        """User-configured rate limit is returned from DB."""
        app.config["SECRET_KEY"] = "test"
        with app.test_request_context():
            from flask import g
            from local_deep_research.security.rate_limiter import (
                _get_user_api_rate_limit,
            )

            g.current_user = "testuser"
            mock_sm = MagicMock()
            mock_sm.get_setting.return_value = 30

            mock_session = MagicMock()
            with patch(
                "local_deep_research.database.session_context.get_user_db_session"
            ) as mock_ctx:
                mock_ctx.return_value.__enter__ = MagicMock(
                    return_value=mock_session
                )
                mock_ctx.return_value.__exit__ = MagicMock(return_value=None)
                with patch(
                    "local_deep_research.utilities.db_utils.get_settings_manager",
                    return_value=mock_sm,
                ):
                    result = _get_user_api_rate_limit()
                    assert result == 30
                    assert g._api_rate_limit == 30


class TestSessionFallback:
    """Tests for session fallback when g.current_user is not set."""

    def test_api_user_key_uses_session_fallback(self, app):
        """Key function uses session username when g.current_user absent."""
        app.config["SECRET_KEY"] = "test"
        with app.test_request_context():
            from local_deep_research.security.rate_limiter import (
                _get_api_user_key,
            )

            with app.test_client() as c:
                with c.session_transaction() as sess:
                    sess["username"] = "session_user"
                with c.application.test_request_context():
                    from flask import session as s

                    s["username"] = "session_user"
                    result = _get_api_user_key()
                    assert result == "api_user:session_user"

    def test_upload_user_key_uses_session_fallback(self, app):
        """Upload key function uses session username when g.current_user absent."""
        app.config["SECRET_KEY"] = "test"
        with app.test_request_context():
            from local_deep_research.security.rate_limiter import (
                _get_upload_user_key,
            )

            with app.test_client() as c:
                with c.application.test_request_context():
                    from flask import session as s

                    s["username"] = "session_uploader"
                    result = _get_upload_user_key()
                    assert result == "upload_user:session_uploader"

    def test_g_current_user_takes_priority_over_session(self, app):
        """g.current_user is preferred over session username."""
        app.config["SECRET_KEY"] = "test"
        with app.test_request_context():
            from flask import g, session
            from local_deep_research.security.rate_limiter import (
                _get_api_user_key,
            )

            g.current_user = "g_user"
            session["username"] = "session_user"
            result = _get_api_user_key()
            assert result == "api_user:g_user"

    def test_exempt_uses_session_fallback(self, app):
        """Exempt check uses session username when g.current_user absent."""
        app.config["SECRET_KEY"] = "test"
        with app.test_request_context():
            from flask import session
            from local_deep_research.security.rate_limiter import (
                _is_api_rate_limit_exempt,
            )

            # No g.current_user, no session username → exempt
            assert _is_api_rate_limit_exempt() is True

            # Set session username → not exempt (has user, default limit > 0)
            session["username"] = "session_user"
            with patch(
                "local_deep_research.security.rate_limiter._get_user_api_rate_limit",
                return_value=60,
            ):
                assert _is_api_rate_limit_exempt() is False


class TestGetCurrentUsername:
    """Tests for get_current_username helper."""

    def test_returns_g_current_user(self, app):
        with app.test_request_context():
            from flask import g
            from local_deep_research.security.rate_limiter import (
                get_current_username,
            )

            g.current_user = "alice"
            assert get_current_username() == "alice"

    def test_returns_session_when_no_g_current_user(self, app):
        app.config["SECRET_KEY"] = "test"
        with app.test_request_context():
            from flask import session
            from local_deep_research.security.rate_limiter import (
                get_current_username,
            )

            session["username"] = "bob"
            assert get_current_username() == "bob"

    def test_returns_none_when_neither_set(self, app):
        with app.test_request_context():
            from local_deep_research.security.rate_limiter import (
                get_current_username,
            )

            assert get_current_username() is None

    def test_g_current_user_priority_over_session(self, app):
        app.config["SECRET_KEY"] = "test"
        with app.test_request_context():
            from flask import g, session
            from local_deep_research.security.rate_limiter import (
                get_current_username,
            )

            g.current_user = "g_user"
            session["username"] = "session_user"
            assert get_current_username() == "g_user"

    def test_empty_string_g_current_user_falls_to_session(self, app):
        """Empty string g.current_user is falsy — should fall through to session."""
        app.config["SECRET_KEY"] = "test"
        with app.test_request_context():
            from flask import g, session
            from local_deep_research.security.rate_limiter import (
                get_current_username,
            )

            g.current_user = ""
            session["username"] = "session_user"
            assert get_current_username() == "session_user"
