"""
Extended tests for web/auth/decorators.py

Tests cover:
- _safe_redirect_to_login: safe and unsafe URL redirect behavior
- login_required: unauthenticated and authenticated paths for web/API routes
- current_user: session-based username retrieval
- get_current_db_session: database session retrieval for authenticated users
- inject_current_user: g-object injection, stale session clearing, exception handling
"""

from unittest.mock import MagicMock, patch

from flask import Blueprint, Flask, g, session

from local_deep_research.web.auth.decorators import (
    _safe_redirect_to_login,
    current_user,
    get_current_db_session,
    inject_current_user,
    login_required,
)


def _make_app():
    """Create a minimal Flask app with an auth blueprint for url_for('auth.login')."""
    app = Flask(__name__)
    app.config["SECRET_KEY"] = "test"
    app.config["TESTING"] = True

    auth = Blueprint("auth", __name__, url_prefix="/auth")

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

    app.register_blueprint(auth)
    return app


# ---------------------------------------------------------------------------
# login_required
# ---------------------------------------------------------------------------


class TestLoginRequired:
    """Tests for the login_required decorator."""

    def test_unauthenticated_web_request_redirects_to_login(self):
        """Unauthenticated request to a web route should 302 redirect to login."""
        app = _make_app()

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

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

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

    def test_unauthenticated_api_request_returns_401_json(self):
        """Unauthenticated request to /api/... should return 401 JSON."""
        app = _make_app()

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

            @app.route("/api/data")
            @login_required
            def api_data():
                return {"ok": True}

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

    def test_unauthenticated_settings_api_request_returns_401_json(self):
        """Unauthenticated request to /settings/api/... should return 401 JSON."""
        app = _make_app()

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

            @app.route("/settings/api/config")
            @login_required
            def settings_api_config():
                return {"ok": True}

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

    def test_authenticated_no_db_connection_web_route_clears_session_and_redirects(
        self,
    ):
        """Authenticated user without DB connection on web route: session cleared, redirect."""
        app = _make_app()

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

            @app.route("/page")
            @login_required
            def page():
                return "OK"

            with app.test_client() as client:
                with client.session_transaction() as sess:
                    sess["username"] = "alice"

                resp = client.get("/page")
                assert resp.status_code == 302
                assert "/auth/login" in resp.location

                # Session should have been cleared
                with client.session_transaction() as sess:
                    assert "username" not in sess

    def test_authenticated_no_db_connection_api_route_returns_401_json(self):
        """Authenticated user without DB connection on API route returns 401."""
        app = _make_app()

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

            @app.route("/api/items")
            @login_required
            def api_items():
                return {"ok": True}

            with app.test_client() as client:
                with client.session_transaction() as sess:
                    sess["username"] = "alice"

                resp = client.get("/api/items")
                assert resp.status_code == 401
                assert resp.json["error"] == "Database connection required"

    def test_fully_authenticated_calls_wrapped_function(self):
        """Fully authenticated user with DB connection: wrapped function is called."""
        app = _make_app()

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

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

            with app.test_client() as client:
                with client.session_transaction() as sess:
                    sess["username"] = "alice"

                resp = client.get("/protected")
                assert resp.status_code == 200
                assert resp.data == b"secret content"


# ---------------------------------------------------------------------------
# current_user
# ---------------------------------------------------------------------------


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

    def test_returns_username_when_in_session(self):
        """current_user returns the username stored in session."""
        app = _make_app()
        with app.test_request_context():
            session["username"] = "bob"
            assert current_user() == "bob"

    def test_returns_none_when_no_username_in_session(self):
        """current_user returns None when session has no username."""
        app = _make_app()
        with app.test_request_context():
            assert current_user() is None


# ---------------------------------------------------------------------------
# get_current_db_session
# ---------------------------------------------------------------------------


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

    def test_returns_db_session_when_authenticated(self):
        """Returns the db_manager session object for an authenticated user."""
        app = _make_app()
        mock_session_obj = MagicMock(name="db_session")

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

            with app.test_request_context():
                session["username"] = "carol"
                result = get_current_db_session()
                assert result is mock_session_obj
                mock_db.get_session.assert_called_once_with("carol")

    def test_returns_none_when_no_user(self):
        """Returns None when there is no authenticated user."""
        app = _make_app()

        with patch("local_deep_research.web.auth.decorators.db_manager"):
            with app.test_request_context():
                result = get_current_db_session()
                assert result is None


# ---------------------------------------------------------------------------
# inject_current_user
# ---------------------------------------------------------------------------


class TestInjectCurrentUser:
    """Tests for the inject_current_user before_request handler."""

    def test_sets_g_current_user_and_db_session_when_authenticated(self):
        """When user is authenticated, g.current_user and g.db_session are set."""
        app = _make_app()
        mock_session_obj = MagicMock(name="db_session")

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

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

    def test_sets_both_to_none_when_not_authenticated(self):
        """When no user in session, g.current_user and g.db_session are None."""
        app = _make_app()

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

    def test_clears_session_for_regular_route_when_db_session_none_and_not_connected(
        self,
    ):
        """For regular (non-API/auth) routes, stale session is cleared."""
        app = _make_app()

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

            with app.test_request_context("/dashboard"):
                session["username"] = "eve"
                inject_current_user()
                # Session should have been cleared
                assert g.current_user is None
                assert g.db_session is None
                assert "username" not in session

    def test_does_not_clear_session_for_api_route_when_db_session_none(self):
        """For /api/ routes, session is NOT cleared even without db_session."""
        app = _make_app()

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

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

    def test_sets_db_session_none_on_exception(self):
        """When db_manager.get_session raises, g.db_session is set to None."""
        app = _make_app()

        with patch(
            "local_deep_research.web.auth.decorators.db_manager"
        ) as mock_db:
            mock_db.get_session.side_effect = RuntimeError("connection lost")

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


# ---------------------------------------------------------------------------
# _safe_redirect_to_login
# ---------------------------------------------------------------------------


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

    def test_includes_next_param_when_url_is_safe(self):
        """When URLValidator says the URL is safe, next param is included."""
        app = _make_app()

        with patch(
            "local_deep_research.web.auth.decorators.URLValidator"
        ) as mock_validator:
            mock_validator.is_safe_redirect_url.return_value = True

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

    def test_excludes_next_param_when_url_is_not_safe(self):
        """When URLValidator says the URL is NOT safe, next param is omitted."""
        app = _make_app()

        with patch(
            "local_deep_research.web.auth.decorators.URLValidator"
        ) as mock_validator:
            mock_validator.is_safe_redirect_url.return_value = False

            with app.test_request_context(
                "http://evil.com/steal", base_url="http://localhost/"
            ):
                resp = _safe_redirect_to_login()
                assert resp.status_code == 302
                assert "next=" not in resp.location
                assert "/auth/login" in resp.location
