"""
Tests for web/auth/decorators.py

Tests cover:
- login_required decorator
- current_user function
- get_current_db_session function
- inject_current_user function
- _safe_redirect_to_login function
"""

from unittest.mock import Mock, patch
from flask import Blueprint, Flask, session, g


class TestLoginRequiredDecorator:
    """Tests for login_required decorator."""

    def test_unauthenticated_api_request_returns_401(self):
        """Test that unauthenticated API requests return 401 JSON error."""
        app = Flask(__name__)
        app.secret_key = "test"

        with patch("local_deep_research.web.auth.decorators.db_manager"):
            from local_deep_research.web.auth.decorators import login_required

            @app.route("/api/test")
            @login_required
            def test_route():
                return {"success": True}

            with app.test_client() as client:
                response = client.get("/api/test")
                assert response.status_code == 401
                assert response.json["error"] == "Authentication required"

    def test_unauthenticated_settings_api_request_returns_401(self):
        """Test that unauthenticated settings API requests return 401."""
        app = Flask(__name__)
        app.secret_key = "test"

        with patch("local_deep_research.web.auth.decorators.db_manager"):
            from local_deep_research.web.auth.decorators import login_required

            @app.route("/settings/api/test")
            @login_required
            def test_route():
                return {"success": True}

            with app.test_client() as client:
                response = client.get("/settings/api/test")
                assert response.status_code == 401

    def test_unauthenticated_web_request_redirects(self):
        """Test that unauthenticated web requests redirect to login."""
        app = Flask(__name__)
        app.secret_key = "test"

        # Register auth blueprint with login route
        from flask import Blueprint

        auth = Blueprint("auth", __name__)

        @auth.route("/login")
        def login():
            return "Login Page"

        app.register_blueprint(auth, url_prefix="/auth")

        with patch("local_deep_research.web.auth.decorators.db_manager"):
            from local_deep_research.web.auth.decorators import login_required

            @app.route("/dashboard")
            @login_required
            def dashboard():
                return "Dashboard"

            with app.test_client() as client:
                response = client.get("/dashboard")
                assert response.status_code == 302
                assert "/auth/login" in response.location

    def test_authenticated_without_db_connection_api_returns_401(self):
        """Test authenticated user without DB connection on API route."""
        app = Flask(__name__)
        app.secret_key = "test"

        with patch(
            "local_deep_research.web.auth.decorators.db_manager"
        ) as mock_db_manager:
            mock_db_manager.is_user_connected.return_value = False

            from local_deep_research.web.auth.decorators import login_required

            @app.route("/api/test")
            @login_required
            def test_route():
                return {"success": True}

            with app.test_client() as client:
                with client.session_transaction() as sess:
                    sess["username"] = "testuser"
                response = client.get("/api/test")
                assert response.status_code == 401
                assert response.json["error"] == "Database connection required"

    def test_authenticated_with_db_connection_succeeds(self):
        """Test authenticated user with DB connection succeeds."""
        app = Flask(__name__)
        app.secret_key = "test"

        with patch(
            "local_deep_research.web.auth.decorators.db_manager"
        ) as mock_db_manager:
            mock_db_manager.is_user_connected.return_value = True

            from local_deep_research.web.auth.decorators import login_required

            @app.route("/api/test")
            @login_required
            def test_route():
                return {"success": True}

            with app.test_client() as client:
                with client.session_transaction() as sess:
                    sess["username"] = "testuser"
                response = client.get("/api/test")
                assert response.status_code == 200

    def test_unauthenticated_nested_news_api_returns_401(self):
        """Nested API blueprints (e.g. /news/api/...) must return JSON 401,
        not an HTML redirect. Regression guard for the case where API paths
        only matched as a top-level prefix."""
        app = Flask(__name__)
        app.secret_key = "test"

        with patch("local_deep_research.web.auth.decorators.db_manager"):
            from local_deep_research.web.auth.decorators import login_required

            @app.route("/news/api/categories")
            @login_required
            def categories():
                return {"success": True}

            with app.test_client() as client:
                response = client.get("/news/api/categories")
                assert response.status_code == 401
                assert response.json["error"] == "Authentication required"

    def test_unauthenticated_nested_library_api_returns_401(self):
        """Nested /library/api/... paths must also return JSON 401."""
        app = Flask(__name__)
        app.secret_key = "test"

        with patch("local_deep_research.web.auth.decorators.db_manager"):
            from local_deep_research.web.auth.decorators import login_required

            @app.route("/library/api/documents")
            @login_required
            def documents():
                return {"success": True}

            with app.test_client() as client:
                response = client.get("/library/api/documents")
                assert response.status_code == 401

    def test_authenticated_no_db_on_nested_api_returns_401(self):
        """Stale-session case on a nested API path must return JSON 401,
        not redirect to the login page."""
        app = Flask(__name__)
        app.secret_key = "test"

        with patch(
            "local_deep_research.web.auth.decorators.db_manager"
        ) as mock_db_manager:
            mock_db_manager.is_user_connected.return_value = False

            from local_deep_research.web.auth.decorators import login_required

            @app.route("/news/api/feed")
            @login_required
            def feed():
                return {"success": True}

            with app.test_client() as client:
                with client.session_transaction() as sess:
                    sess["username"] = "testuser"
                response = client.get("/news/api/feed")
                assert response.status_code == 401
                assert response.json["error"] == "Database connection required"


class TestIsApiPath:
    """Tests for the _is_api_path helper."""

    def test_top_level_api_path(self):
        from local_deep_research.web.auth.decorators import _is_api_path

        assert _is_api_path("/api/v1/foo") is True

    def test_nested_news_api_path(self):
        from local_deep_research.web.auth.decorators import _is_api_path

        assert _is_api_path("/news/api/categories") is True

    def test_nested_library_api_path(self):
        from local_deep_research.web.auth.decorators import _is_api_path

        assert _is_api_path("/library/api/documents") is True

    def test_settings_api_path(self):
        from local_deep_research.web.auth.decorators import _is_api_path

        assert _is_api_path("/settings/api/foo") is True

    def test_non_api_page_path(self):
        from local_deep_research.web.auth.decorators import _is_api_path

        assert _is_api_path("/news/") is False
        assert _is_api_path("/dashboard") is False
        assert _is_api_path("/news/subscriptions") is False

    def test_partial_api_word_does_not_match(self):
        """The `api` segment must be slash-bounded — non-API paths whose
        names happen to start with 'api' must NOT be classified as API."""
        from local_deep_research.web.auth.decorators import _is_api_path

        assert _is_api_path("/apidocs") is False
        assert _is_api_path("/openapi.json") is False
        assert _is_api_path("/notapi/x") is False

    def test_path_ending_in_slash_api(self):
        """Paths ending in `/api` (no further segments) are JSON
        endpoints — e.g. /settings/api, /history/api."""
        from local_deep_research.web.auth.decorators import _is_api_path

        assert _is_api_path("/settings/api") is True
        assert _is_api_path("/history/api") is True
        assert _is_api_path("/foo/api") is True
        assert _is_api_path("/api") is True


class TestCurrentUser:
    """Tests for current_user function."""

    def test_current_user_returns_username(self):
        """Test current_user returns username from session."""
        app = Flask(__name__)
        app.secret_key = "test"

        from local_deep_research.web.auth.decorators import current_user

        with app.test_request_context():
            session["username"] = "testuser"
            assert current_user() == "testuser"

    def test_current_user_returns_none_when_not_authenticated(self):
        """Test current_user returns None when not authenticated."""
        app = Flask(__name__)
        app.secret_key = "test"

        from local_deep_research.web.auth.decorators import current_user

        with app.test_request_context():
            assert current_user() is None


class TestGetCurrentDbSession:
    """Tests for get_current_db_session function."""

    def test_get_session_returns_session_for_authenticated_user(self):
        """Test get_current_db_session returns session for authenticated user."""
        app = Flask(__name__)
        app.secret_key = "test"

        mock_session = Mock()

        with patch(
            "local_deep_research.web.auth.decorators.db_manager"
        ) as mock_db_manager:
            mock_db_manager.get_session.return_value = mock_session

            from local_deep_research.web.auth.decorators import (
                get_current_db_session,
            )

            with app.test_request_context():
                session["username"] = "testuser"
                result = get_current_db_session()
                assert result == mock_session
                mock_db_manager.get_session.assert_called_once_with("testuser")

    def test_get_session_returns_none_when_not_authenticated(self):
        """Test get_current_db_session returns None when not authenticated."""
        app = Flask(__name__)
        app.secret_key = "test"

        from local_deep_research.web.auth.decorators import (
            get_current_db_session,
        )

        with app.test_request_context():
            result = get_current_db_session()
            assert result is None


class TestInjectCurrentUser:
    """Tests for inject_current_user function."""

    def test_inject_user_sets_g_current_user(self):
        """Test inject_current_user sets g.current_user."""
        app = Flask(__name__)
        app.secret_key = "test"

        mock_db_session = Mock()

        with patch(
            "local_deep_research.web.auth.decorators.db_manager"
        ) as mock_db_manager:
            mock_db_manager.get_session.return_value = mock_db_session
            mock_db_manager.is_user_connected.return_value = True

            from local_deep_research.web.auth.decorators import (
                inject_current_user,
            )

            with app.test_request_context():
                session["username"] = "testuser"
                inject_current_user()
                assert g.current_user == "testuser"
                # Session is now created lazily, not eagerly in inject_current_user
                assert g.db_session is None

    def test_inject_user_no_session(self):
        """Test inject_current_user when no session."""
        app = Flask(__name__)
        app.secret_key = "test"

        from local_deep_research.web.auth.decorators import inject_current_user

        with app.test_request_context():
            inject_current_user()
            assert g.current_user is None
            assert g.db_session is None

    def test_inject_user_with_db_error(self):
        """Test inject_current_user handles DB errors gracefully."""
        app = Flask(__name__)
        app.secret_key = "test"

        with patch(
            "local_deep_research.web.auth.decorators.db_manager"
        ) as mock_db_manager:
            mock_db_manager.get_session.side_effect = Exception("DB Error")

            from local_deep_research.web.auth.decorators import (
                inject_current_user,
            )

            with app.test_request_context():
                session["username"] = "testuser"
                inject_current_user()
                assert g.current_user == "testuser"
                assert g.db_session is None

    def test_inject_user_clears_stale_session_for_regular_routes(self):
        """Test inject_current_user clears stale session for regular routes."""
        app = Flask(__name__)
        app.secret_key = "test"

        with patch(
            "local_deep_research.web.auth.decorators.db_manager"
        ) as mock_db_manager:
            mock_db_manager.get_session.return_value = None
            mock_db_manager.is_user_connected.return_value = False

            from local_deep_research.web.auth.decorators import (
                inject_current_user,
            )

            with app.test_request_context("/dashboard"):
                session["username"] = "testuser"
                inject_current_user()
                # Session should be cleared
                assert g.current_user is None
                assert g.db_session is None

    def test_inject_user_allows_api_routes_without_db(self):
        """Test inject_current_user allows API routes without DB connection."""
        app = Flask(__name__)
        app.secret_key = "test"

        with patch(
            "local_deep_research.web.auth.decorators.db_manager"
        ) as mock_db_manager:
            mock_db_manager.get_session.return_value = None
            mock_db_manager.is_user_connected.return_value = False

            from local_deep_research.web.auth.decorators import (
                inject_current_user,
            )

            with app.test_request_context("/api/test"):
                session["username"] = "testuser"
                inject_current_user()
                # g.current_user should still be set for API routes
                assert g.current_user == "testuser"

    def test_inject_user_allows_auth_routes_without_db(self):
        """Test inject_current_user allows auth routes without DB connection."""
        app = Flask(__name__)
        app.secret_key = "test"

        with patch(
            "local_deep_research.web.auth.decorators.db_manager"
        ) as mock_db_manager:
            mock_db_manager.get_session.return_value = None
            mock_db_manager.is_user_connected.return_value = False

            from local_deep_research.web.auth.decorators import (
                inject_current_user,
            )

            with app.test_request_context("/auth/login"):
                session["username"] = "testuser"
                inject_current_user()
                # g.current_user should still be set for auth routes
                assert g.current_user == "testuser"


class TestSafeRedirectToLogin:
    """Tests for _safe_redirect_to_login open redirect prevention."""

    def _setup_app_with_login_required_route(self):
        """Create a Flask app with auth blueprint and a protected route."""
        app = Flask(__name__)
        app.secret_key = "test"

        auth = Blueprint("auth", __name__)

        @auth.route("/login")
        def login():
            return "Login Page"

        app.register_blueprint(auth, url_prefix="/auth")
        return app

    def test_valid_same_host_next_url_is_preserved(self):
        """Valid same-host URL should be included as next parameter."""
        app = self._setup_app_with_login_required_route()

        with patch("local_deep_research.web.auth.decorators.db_manager"):
            from local_deep_research.web.auth.decorators import login_required

            @app.route("/dashboard")
            @login_required
            def dashboard():
                return "Dashboard"

            with app.test_client() as client:
                response = client.get("/dashboard")
                assert response.status_code == 302
                location = response.location
                assert "/auth/login" in location
                assert "next=" in location

    def test_protocol_relative_url_rejected(self):
        """Protocol-relative URLs (//evil.com) should NOT appear in next param."""
        app = self._setup_app_with_login_required_route()

        with patch("local_deep_research.web.auth.decorators.db_manager"):
            with patch(
                "local_deep_research.web.auth.decorators.URLValidator"
            ) as mock_validator:
                # Simulate URLValidator rejecting the URL
                mock_validator.is_safe_redirect_url.return_value = False

                from local_deep_research.web.auth.decorators import (
                    _safe_redirect_to_login,
                )

                with app.test_request_context(
                    "//evil.com/steal-cookies",
                    base_url="http://localhost:5000",
                ):
                    response = _safe_redirect_to_login()
                    assert response.status_code == 302
                    # Should redirect to login WITHOUT next parameter
                    assert "next=" not in response.location

    def test_path_traversal_url_rejected(self):
        """Path traversal in request URL should NOT appear in next param."""
        app = self._setup_app_with_login_required_route()

        with patch("local_deep_research.web.auth.decorators.db_manager"):
            from local_deep_research.web.auth.decorators import (
                _safe_redirect_to_login,
            )

            with app.test_request_context(
                "http://localhost:5000/../../../etc/passwd",
                base_url="http://localhost:5000",
            ):
                response = _safe_redirect_to_login()
                assert response.status_code == 302
                assert "next=" not in response.location

    def test_safe_url_includes_next_param(self):
        """Safe URLs should include the next parameter."""
        app = self._setup_app_with_login_required_route()

        with patch("local_deep_research.web.auth.decorators.db_manager"):
            from local_deep_research.web.auth.decorators import (
                _safe_redirect_to_login,
            )

            with app.test_request_context(
                "/dashboard",
                base_url="http://localhost:5000",
            ):
                response = _safe_redirect_to_login()
                assert response.status_code == 302
                assert "next=" in response.location

    def test_stale_session_redirect_uses_safe_redirect(self):
        """When DB connection is lost, redirect should use _safe_redirect_to_login."""
        app = self._setup_app_with_login_required_route()

        with patch(
            "local_deep_research.web.auth.decorators.db_manager"
        ) as mock_db_manager:
            mock_db_manager.is_user_connected.return_value = False

            from local_deep_research.web.auth.decorators import login_required

            @app.route("/protected")
            @login_required
            def protected():
                return "Protected"

            with app.test_client() as client:
                with client.session_transaction() as sess:
                    sess["username"] = "testuser"
                response = client.get("/protected")
                assert response.status_code == 302
                assert "/auth/login" in response.location
