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

Tests cover:
- Login, register, and logout routes
- CSRF token endpoint
- Check auth endpoint
- Change password endpoint
- Integrity check endpoint
- Open redirect prevention
"""

from pathlib import Path
from unittest.mock import MagicMock, patch

from flask import Flask

from local_deep_research.security.password_validator import PasswordValidator


class TestGetCsrfToken:
    """Tests for /csrf-token endpoint."""

    def test_returns_csrf_token(self):
        """Should return CSRF token."""
        app = Flask(__name__)
        app.secret_key = "test"
        app.config["WTF_CSRF_ENABLED"] = True

        with patch("flask_wtf.csrf.generate_csrf") as mock_csrf:
            mock_csrf.return_value = "test_csrf_token_123"

            from local_deep_research.web.auth.routes import auth_bp

            app.register_blueprint(auth_bp)

            with app.test_client() as client:
                response = client.get("/auth/csrf-token")
                assert response.status_code == 200
                assert response.json["csrf_token"] == "test_csrf_token_123"


class TestLoginPage:
    """Tests for GET /login endpoint."""

    def test_renders_login_page(self):
        """Should render login page for unauthenticated users."""
        app = Flask(__name__)
        app.secret_key = "test"
        app.config["WTF_CSRF_ENABLED"] = False
        app.template_folder = "templates"  # May need adjustment

        with (
            patch(
                "local_deep_research.web.auth.routes.load_server_config"
            ) as mock_config,
            patch(
                "local_deep_research.web.auth.routes.render_template"
            ) as mock_render,
        ):
            mock_config.return_value = {"allow_registrations": True}
            mock_render.return_value = "Login Page"

            from local_deep_research.web.auth.routes import auth_bp

            app.register_blueprint(auth_bp)

            with app.test_client() as client:
                client.get("/auth/login")
                # Should call render_template
                mock_render.assert_called()

    def test_redirects_if_already_logged_in(self):
        """Should redirect to index if user already logged in."""
        app = Flask(__name__)
        app.secret_key = "test"
        app.config["WTF_CSRF_ENABLED"] = False

        @app.route("/")
        def index():
            return "Index"

        with patch(
            "local_deep_research.web.auth.routes.load_server_config"
        ) as mock_config:
            mock_config.return_value = {"allow_registrations": True}

            from local_deep_research.web.auth.routes import auth_bp

            app.register_blueprint(auth_bp)

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

                response = client.get("/auth/login")
                assert response.status_code == 302


class TestLogin:
    """Tests for POST /login endpoint."""

    def test_returns_400_without_username(self):
        """Should return 400 when username is missing."""
        app = Flask(__name__)
        app.secret_key = "test"
        app.config["WTF_CSRF_ENABLED"] = False

        with (
            patch(
                "local_deep_research.web.auth.routes.load_server_config"
            ) as mock_config,
            patch(
                "local_deep_research.web.auth.routes.render_template"
            ) as mock_render,
        ):
            mock_config.return_value = {"allow_registrations": True}
            mock_render.return_value = "Login Page"

            from local_deep_research.web.auth.routes import auth_bp

            app.register_blueprint(auth_bp)

            with app.test_client() as client:
                response = client.post(
                    "/auth/login",
                    data={"username": "", "password": "password123"},
                )
                assert response.status_code == 400

    def test_returns_400_without_password(self):
        """Should return 400 when password is missing."""
        app = Flask(__name__)
        app.secret_key = "test"
        app.config["WTF_CSRF_ENABLED"] = False

        with (
            patch(
                "local_deep_research.web.auth.routes.load_server_config"
            ) as mock_config,
            patch(
                "local_deep_research.web.auth.routes.render_template"
            ) as mock_render,
        ):
            mock_config.return_value = {"allow_registrations": True}
            mock_render.return_value = "Login Page"

            from local_deep_research.web.auth.routes import auth_bp

            app.register_blueprint(auth_bp)

            with app.test_client() as client:
                response = client.post(
                    "/auth/login",
                    data={"username": "testuser", "password": ""},
                )
                assert response.status_code == 400

    def test_returns_401_for_invalid_credentials(self):
        """Should return 401 for invalid credentials."""
        app = Flask(__name__)
        app.secret_key = "test"
        app.config["WTF_CSRF_ENABLED"] = False

        with (
            patch(
                "local_deep_research.web.auth.routes.load_server_config"
            ) as mock_config,
            patch(
                "local_deep_research.web.auth.routes.db_manager"
            ) as mock_db_manager,
            patch(
                "local_deep_research.web.auth.routes.render_template"
            ) as mock_render,
        ):
            mock_config.return_value = {"allow_registrations": True}
            mock_db_manager.open_user_database.return_value = None
            mock_render.return_value = "Login Page"

            from local_deep_research.web.auth.routes import auth_bp

            app.register_blueprint(auth_bp)

            with app.test_client() as client:
                response = client.post(
                    "/auth/login",
                    data={"username": "testuser", "password": "wrongpassword"},
                )
                assert response.status_code == 401

    def test_returns_503_for_database_init_failure_without_lockout(self):
        """A DatabaseInitializationError should yield 503 and bypass lockout.

        Regression for the migration-failure path: the user's password is
        valid (we got far enough to attempt schema init), so penalising
        them with a lockout for a server-side configuration problem
        (e.g. world-writable migrations dir) would be wrong.
        """
        from local_deep_research.database.encrypted_db import (
            DatabaseInitializationError,
        )

        app = Flask(__name__)
        app.secret_key = "test"
        app.config["WTF_CSRF_ENABLED"] = False

        with (
            patch(
                "local_deep_research.web.auth.routes.load_server_config"
            ) as mock_config,
            patch(
                "local_deep_research.web.auth.routes.db_manager"
            ) as mock_db_manager,
            patch(
                "local_deep_research.web.auth.routes.render_template"
            ) as mock_render,
            patch(
                "local_deep_research.web.auth.routes.get_account_lockout_manager"
            ) as mock_lockout_factory,
        ):
            mock_config.return_value = {"allow_registrations": True}
            mock_db_manager.open_user_database.side_effect = (
                DatabaseInitializationError("boom")
            )
            mock_render.return_value = "Login Page"

            mock_lockout = mock_lockout_factory.return_value
            mock_lockout.is_locked.return_value = False

            from local_deep_research.web.auth.routes import auth_bp

            app.register_blueprint(auth_bp)

            with app.test_client() as client:
                response = client.post(
                    "/auth/login",
                    data={
                        "username": "validuser",
                        "password": "correctpassword",
                    },
                )
                assert response.status_code == 503
                # Critical: do not punish the user for a server-side bug.
                mock_lockout.record_failure.assert_not_called()


class TestRegisterPage:
    """Tests for GET /register endpoint."""

    def test_redirects_when_registrations_disabled(self):
        """Should redirect to login when registrations are disabled."""
        app = Flask(__name__)
        app.secret_key = "test"
        app.config["WTF_CSRF_ENABLED"] = False

        with patch(
            "local_deep_research.web.auth.routes.load_server_config"
        ) as mock_config:
            mock_config.return_value = {"allow_registrations": False}

            from local_deep_research.web.auth.routes import auth_bp

            app.register_blueprint(auth_bp)

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


class TestRegister:
    """Tests for POST /register endpoint."""

    def test_returns_400_for_short_username(self):
        """Should return 400 when username is too short."""
        app = Flask(__name__)
        app.secret_key = "test"
        app.config["WTF_CSRF_ENABLED"] = False

        with (
            patch(
                "local_deep_research.web.auth.routes.load_server_config"
            ) as mock_config,
            patch(
                "local_deep_research.web.auth.routes.render_template"
            ) as mock_render,
        ):
            mock_config.return_value = {"allow_registrations": True}
            mock_render.return_value = "Register Page"

            from local_deep_research.web.auth.routes import auth_bp

            app.register_blueprint(auth_bp)

            with app.test_client() as client:
                response = client.post(
                    "/auth/register",
                    data={
                        "username": "ab",  # Too short
                        "password": "password123",
                        "confirm_password": "password123",
                        "acknowledge": "true",
                    },
                )
                assert response.status_code == 400

    def test_returns_400_for_invalid_username_chars(self):
        """Should return 400 when username contains invalid characters."""
        app = Flask(__name__)
        app.secret_key = "test"
        app.config["WTF_CSRF_ENABLED"] = False

        with (
            patch(
                "local_deep_research.web.auth.routes.load_server_config"
            ) as mock_config,
            patch(
                "local_deep_research.web.auth.routes.render_template"
            ) as mock_render,
        ):
            mock_config.return_value = {"allow_registrations": True}
            mock_render.return_value = "Register Page"

            from local_deep_research.web.auth.routes import auth_bp

            app.register_blueprint(auth_bp)

            with app.test_client() as client:
                response = client.post(
                    "/auth/register",
                    data={
                        "username": "test@user!",  # Invalid chars
                        "password": "password123",
                        "confirm_password": "password123",
                        "acknowledge": "true",
                    },
                )
                assert response.status_code == 400

    def test_returns_400_for_short_password(self):
        """Should return 400 when password is too short."""
        app = Flask(__name__)
        app.secret_key = "test"
        app.config["WTF_CSRF_ENABLED"] = False

        with (
            patch(
                "local_deep_research.web.auth.routes.load_server_config"
            ) as mock_config,
            patch(
                "local_deep_research.web.auth.routes.render_template"
            ) as mock_render,
        ):
            mock_config.return_value = {"allow_registrations": True}
            mock_render.return_value = "Register Page"

            from local_deep_research.web.auth.routes import auth_bp

            app.register_blueprint(auth_bp)

            with app.test_client() as client:
                response = client.post(
                    "/auth/register",
                    data={
                        "username": "testuser",
                        "password": "short",  # Too short
                        "confirm_password": "short",
                        "acknowledge": "true",
                    },
                )
                assert response.status_code == 400

    def test_returns_400_for_password_mismatch(self):
        """Should return 400 when passwords don't match."""
        app = Flask(__name__)
        app.secret_key = "test"
        app.config["WTF_CSRF_ENABLED"] = False

        with (
            patch(
                "local_deep_research.web.auth.routes.load_server_config"
            ) as mock_config,
            patch(
                "local_deep_research.web.auth.routes.render_template"
            ) as mock_render,
        ):
            mock_config.return_value = {"allow_registrations": True}
            mock_render.return_value = "Register Page"

            from local_deep_research.web.auth.routes import auth_bp

            app.register_blueprint(auth_bp)

            with app.test_client() as client:
                response = client.post(
                    "/auth/register",
                    data={
                        "username": "testuser",
                        "password": "password123",
                        "confirm_password": "different123",
                        "acknowledge": "true",
                    },
                )
                assert response.status_code == 400

    def test_returns_400_without_acknowledgment(self):
        """Should return 400 when acknowledgment not provided."""
        app = Flask(__name__)
        app.secret_key = "test"
        app.config["WTF_CSRF_ENABLED"] = False

        with (
            patch(
                "local_deep_research.web.auth.routes.load_server_config"
            ) as mock_config,
            patch(
                "local_deep_research.web.auth.routes.render_template"
            ) as mock_render,
        ):
            mock_config.return_value = {"allow_registrations": True}
            mock_render.return_value = "Register Page"

            from local_deep_research.web.auth.routes import auth_bp

            app.register_blueprint(auth_bp)

            with app.test_client() as client:
                response = client.post(
                    "/auth/register",
                    data={
                        "username": "testuser",
                        "password": "password123",
                        "confirm_password": "password123",
                        # No acknowledge
                    },
                )
                assert response.status_code == 400


class TestLogout:
    """Tests for /logout endpoint."""

    def test_clears_session_on_logout(self):
        """Should clear session on logout."""
        app = Flask(__name__)
        app.secret_key = "test"
        app.config["WTF_CSRF_ENABLED"] = False

        with (
            patch("local_deep_research.web.auth.routes.db_manager"),
            patch("local_deep_research.web.auth.routes.session_manager"),
            patch(
                "local_deep_research.database.session_passwords.session_password_store"
            ),
        ):
            from local_deep_research.web.auth.routes import auth_bp

            app.register_blueprint(auth_bp)

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

                response = client.post("/auth/logout")
                assert response.status_code == 302

                with client.session_transaction() as sess:
                    assert "username" not in sess

    def test_redirects_to_login(self):
        """Should redirect to login after logout."""
        app = Flask(__name__)
        app.secret_key = "test"
        app.config["WTF_CSRF_ENABLED"] = False

        with (
            patch("local_deep_research.web.auth.routes.db_manager"),
            patch("local_deep_research.web.auth.routes.session_manager"),
            patch(
                "local_deep_research.database.session_passwords.session_password_store"
            ),
        ):
            from local_deep_research.web.auth.routes import auth_bp

            app.register_blueprint(auth_bp)

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

    def test_unregisters_user_from_scheduler_on_logout(self):
        """Should call scheduler.unregister_user() during logout."""
        app = Flask(__name__)
        app.secret_key = "test"
        app.config["WTF_CSRF_ENABLED"] = False

        mock_scheduler = MagicMock()
        mock_scheduler.is_running = True

        with (
            patch("local_deep_research.web.auth.routes.db_manager"),
            patch("local_deep_research.web.auth.routes.session_manager"),
            patch(
                "local_deep_research.database.session_passwords.session_password_store"
            ),
            patch(
                "local_deep_research.scheduler.background.get_background_job_scheduler",
                return_value=mock_scheduler,
            ),
        ):
            from local_deep_research.web.auth.routes import auth_bp

            app.register_blueprint(auth_bp)

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

                client.post("/auth/logout")
                mock_scheduler.unregister_user.assert_called_once_with(
                    "testuser"
                )


class TestCheckAuth:
    """Tests for /check endpoint."""

    def test_returns_authenticated_true_when_logged_in(self):
        """Should return authenticated=True when logged in."""
        app = Flask(__name__)
        app.secret_key = "test"
        app.config["WTF_CSRF_ENABLED"] = False

        from local_deep_research.web.auth.routes import auth_bp

        app.register_blueprint(auth_bp)

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

            response = client.get("/auth/check")
            assert response.status_code == 200
            assert response.json["authenticated"] is True
            assert response.json["username"] == "testuser"

    def test_returns_authenticated_false_when_not_logged_in(self):
        """Should return authenticated=False when not logged in."""
        app = Flask(__name__)
        app.secret_key = "test"
        app.config["WTF_CSRF_ENABLED"] = False

        from local_deep_research.web.auth.routes import auth_bp

        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.get("/auth/check")
            assert response.status_code == 401
            assert response.json["authenticated"] is False


class TestChangePassword:
    """Tests for /change-password endpoint."""

    def test_redirects_when_not_logged_in(self):
        """Should redirect to login when not authenticated."""
        app = Flask(__name__)
        app.secret_key = "test"
        app.config["WTF_CSRF_ENABLED"] = False

        from local_deep_research.web.auth.routes import auth_bp

        app.register_blueprint(auth_bp)

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

    def test_returns_400_without_current_password(self):
        """Should return 400 when current password is missing."""
        app = Flask(__name__)
        app.secret_key = "test"
        app.config["WTF_CSRF_ENABLED"] = False

        with patch(
            "local_deep_research.web.auth.routes.render_template"
        ) as mock_render:
            mock_render.return_value = "Change Password Page"

            from local_deep_research.web.auth.routes import auth_bp

            app.register_blueprint(auth_bp)

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

                response = client.post(
                    "/auth/change-password",
                    data={
                        "current_password": "",
                        "new_password": "newpassword123",
                        "confirm_password": "newpassword123",
                    },
                )
                assert response.status_code == 400

    def test_returns_400_when_passwords_match(self):
        """Should return 400 when new password is same as current."""
        app = Flask(__name__)
        app.secret_key = "test"
        app.config["WTF_CSRF_ENABLED"] = False

        with patch(
            "local_deep_research.web.auth.routes.render_template"
        ) as mock_render:
            mock_render.return_value = "Change Password Page"

            from local_deep_research.web.auth.routes import auth_bp

            app.register_blueprint(auth_bp)

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

                response = client.post(
                    "/auth/change-password",
                    data={
                        "current_password": "samepassword123",
                        "new_password": "samepassword123",
                        "confirm_password": "samepassword123",
                    },
                )
                assert response.status_code == 400

    def test_successful_password_change(self):
        """Should destroy ALL sessions and redirect to login on success."""
        app = Flask(__name__)
        app.secret_key = "test"
        app.config["WTF_CSRF_ENABLED"] = False

        with (
            patch("local_deep_research.web.auth.routes.db_manager") as mock_db,
            patch(
                "local_deep_research.web.auth.routes.render_template"
            ) as mock_render,
            patch(
                "local_deep_research.web.auth.routes.session_manager"
            ) as mock_sm,
            patch(
                "local_deep_research.database.session_passwords.session_password_store"
            ) as mock_pw_store,
        ):
            mock_db.change_password.return_value = True
            mock_render.return_value = "Change Password Page"
            mock_sm.destroy_all_user_sessions.return_value = 2

            from local_deep_research.web.auth.routes import auth_bp

            app.register_blueprint(auth_bp)

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

                response = client.post(
                    "/auth/change-password",
                    data={
                        "current_password": "OldPass123",
                        "new_password": "NewStrongP4ss!",
                        "confirm_password": "NewStrongP4ss!",
                    },
                    follow_redirects=False,
                )
                assert response.status_code == 302
                assert "login" in response.location

                with client.session_transaction() as sess:
                    assert "username" not in sess

                mock_db.change_password.assert_called_once_with(
                    "testuser", "OldPass123", "NewStrongP4ss!"
                )
                mock_db.close_user_database.assert_called_once_with("testuser")

                # All sessions for this user should be destroyed
                mock_sm.destroy_all_user_sessions.assert_called_once_with(
                    "testuser"
                )

                # All stored passwords should be cleared
                mock_pw_store.clear_all_for_user.assert_called_once_with(
                    "testuser"
                )

    def test_returns_401_for_wrong_current_password(self):
        """Should return 401 when current password is incorrect."""
        app = Flask(__name__)
        app.secret_key = "test"
        app.config["WTF_CSRF_ENABLED"] = False

        with (
            patch("local_deep_research.web.auth.routes.db_manager") as mock_db,
            patch(
                "local_deep_research.web.auth.routes.render_template"
            ) as mock_render,
        ):
            mock_db.change_password.return_value = False
            mock_render.return_value = "Change Password Page"

            from local_deep_research.web.auth.routes import auth_bp

            app.register_blueprint(auth_bp)

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

                response = client.post(
                    "/auth/change-password",
                    data={
                        "current_password": "WrongPass123",
                        "new_password": "NewStrongP4ss!",
                        "confirm_password": "NewStrongP4ss!",
                    },
                )
                assert response.status_code == 401
                mock_render.assert_called_with(
                    "auth/change_password.html",
                    password_requirements=PasswordValidator.get_requirements(),
                )

    def test_renders_page_when_authenticated(self):
        """Should render change password page for authenticated user."""
        app = Flask(__name__)
        app.secret_key = "test"
        app.config["WTF_CSRF_ENABLED"] = False

        with patch(
            "local_deep_research.web.auth.routes.render_template"
        ) as mock_render:
            mock_render.return_value = "Change Password Page"

            from local_deep_research.web.auth.routes import auth_bp

            app.register_blueprint(auth_bp)

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

                response = client.get("/auth/change-password")
                assert response.status_code == 200
                mock_render.assert_called_with(
                    "auth/change_password.html",
                    password_requirements=PasswordValidator.get_requirements(),
                )

    def test_renders_template_without_missing_dashboard_route(self):
        """Should render the real template using the root dashboard route."""
        template_dir = (
            Path(__file__).resolve().parents[3]
            / "src"
            / "local_deep_research"
            / "web"
            / "templates"
        )
        app = Flask(__name__, template_folder=str(template_dir))
        app.secret_key = "test"
        app.config["WTF_CSRF_ENABLED"] = False
        app.jinja_env.globals["vite_hmr"] = lambda: ""
        app.jinja_env.globals["vite_asset"] = lambda *_args, **_kwargs: ""
        app.jinja_env.globals["csrf_token"] = lambda: "test-csrf-token"

        @app.route("/")
        def index():
            return "Index"

        from local_deep_research.web.auth.routes import auth_bp

        app.register_blueprint(auth_bp)

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

            response = client.get("/auth/change-password")

        assert response.status_code == 200
        assert b'href="/"' in response.data

    def test_returns_400_for_weak_new_password(self):
        """Should return 400 when new password is too weak."""
        app = Flask(__name__)
        app.secret_key = "test"
        app.config["WTF_CSRF_ENABLED"] = False

        with patch(
            "local_deep_research.web.auth.routes.render_template"
        ) as mock_render:
            mock_render.return_value = "Change Password Page"

            from local_deep_research.web.auth.routes import auth_bp

            app.register_blueprint(auth_bp)

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

                response = client.post(
                    "/auth/change-password",
                    data={
                        "current_password": "OldPass123",
                        "new_password": "Pass1",
                        "confirm_password": "Pass1",
                    },
                )
                assert response.status_code == 400

    def test_returns_400_for_mismatched_new_passwords(self):
        """Should return 400 when new passwords don't match."""
        app = Flask(__name__)
        app.secret_key = "test"
        app.config["WTF_CSRF_ENABLED"] = False

        with patch(
            "local_deep_research.web.auth.routes.render_template"
        ) as mock_render:
            mock_render.return_value = "Change Password Page"

            from local_deep_research.web.auth.routes import auth_bp

            app.register_blueprint(auth_bp)

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

                response = client.post(
                    "/auth/change-password",
                    data={
                        "current_password": "OldPass123",
                        "new_password": "NewStrongP4ss!",
                        "confirm_password": "DifferentP4ss!",
                    },
                )
                assert response.status_code == 400


class TestIntegrityCheck:
    """Tests for /integrity-check endpoint."""

    def test_returns_401_when_not_authenticated(self):
        """Should return 401 when not authenticated."""
        app = Flask(__name__)
        app.secret_key = "test"
        app.config["WTF_CSRF_ENABLED"] = False

        from local_deep_research.web.auth.routes import auth_bp

        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.get("/auth/integrity-check")
            assert response.status_code == 401

    def test_returns_integrity_status(self):
        """Should return integrity status for authenticated user."""
        app = Flask(__name__)
        app.secret_key = "test"
        app.config["WTF_CSRF_ENABLED"] = False

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

            from local_deep_research.web.auth.routes import auth_bp

            app.register_blueprint(auth_bp)

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

                response = client.get("/auth/integrity-check")
                assert response.status_code == 200
                assert response.json["username"] == "testuser"
                assert response.json["integrity"] == "valid"


class TestOpenRedirectPrevention:
    """Tests for open redirect prevention in login."""

    def test_blocks_external_redirect(self):
        """Should block redirect to external domain."""
        app = Flask(__name__)
        app.secret_key = "test"
        app.config["WTF_CSRF_ENABLED"] = False

        @app.route("/")
        def index():
            return "Index"

        mock_engine = MagicMock()
        mock_session = MagicMock()
        mock_settings_manager = MagicMock()
        mock_settings_manager.db_version_matches_package.return_value = True

        with (
            patch(
                "local_deep_research.web.auth.routes.load_server_config"
            ) as mock_config,
            patch(
                "local_deep_research.web.auth.routes.db_manager"
            ) as mock_db_manager,
            patch(
                "local_deep_research.web.auth.routes.session_manager"
            ) as mock_session_manager,
            patch(
                "local_deep_research.web.auth.routes.auth_db_session"
            ) as mock_auth_db,
            patch(
                "local_deep_research.database.temp_auth.temp_auth_store"
            ) as mock_temp_auth,
            patch(
                "local_deep_research.database.session_passwords.session_password_store"
            ),
            patch(
                "local_deep_research.settings.manager.SettingsManager"
            ) as mock_settings_cls,
            patch(
                "local_deep_research.database.library_init.initialize_library_for_user"
            ),
            patch(
                "local_deep_research.scheduler.background.get_background_job_scheduler"
            ),
            patch("local_deep_research.database.models.ProviderModel"),
        ):
            mock_config.return_value = {"allow_registrations": True}
            mock_db_manager.open_user_database.return_value = mock_engine
            mock_db_manager.get_session.return_value = mock_session
            mock_session_manager.create_session.return_value = "session_123"
            # auth_db_session is only called in a background daemon thread
            # (_perform_post_login_tasks), not in the synchronous login handler.
            # MagicMock auto-supports context manager protocol, which is sufficient
            # here since we only test HTTP redirect behavior, not DB interactions.
            mock_auth_db.return_value = MagicMock()
            mock_settings_cls.return_value = mock_settings_manager
            mock_temp_auth.store_auth.return_value = "test_auth_token"

            from local_deep_research.web.auth.routes import auth_bp

            app.register_blueprint(auth_bp)

            with app.test_client() as client:
                response = client.post(
                    "/auth/login?next=https://evil.com/steal",
                    data={"username": "testuser", "password": "password123"},
                )

                # Should redirect to safe URL, not evil.com
                assert response.status_code == 302
                assert "evil.com" not in response.location

    def test_allows_safe_relative_redirect(self):
        """Should allow safe relative redirects."""
        app = Flask(__name__)
        app.secret_key = "test"
        app.config["WTF_CSRF_ENABLED"] = False

        @app.route("/")
        def index():
            return "Index"

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

        mock_engine = MagicMock()
        mock_session = MagicMock()
        mock_settings_manager = MagicMock()
        mock_settings_manager.db_version_matches_package.return_value = True

        with (
            patch(
                "local_deep_research.web.auth.routes.load_server_config"
            ) as mock_config,
            patch(
                "local_deep_research.web.auth.routes.db_manager"
            ) as mock_db_manager,
            patch(
                "local_deep_research.web.auth.routes.session_manager"
            ) as mock_session_manager,
            patch(
                "local_deep_research.web.auth.routes.auth_db_session"
            ) as mock_auth_db,
            patch(
                "local_deep_research.database.temp_auth.temp_auth_store"
            ) as mock_temp_auth,
            patch(
                "local_deep_research.database.session_passwords.session_password_store"
            ),
            patch(
                "local_deep_research.settings.manager.SettingsManager"
            ) as mock_settings_cls,
            patch(
                "local_deep_research.database.library_init.initialize_library_for_user"
            ),
            patch(
                "local_deep_research.scheduler.background.get_background_job_scheduler"
            ),
            patch("local_deep_research.database.models.ProviderModel"),
        ):
            mock_config.return_value = {"allow_registrations": True}
            mock_db_manager.open_user_database.return_value = mock_engine
            mock_db_manager.get_session.return_value = mock_session
            mock_session_manager.create_session.return_value = "session_123"
            # auth_db_session: background-thread-only, auto-CM sufficient (see first test)
            mock_auth_db.return_value = MagicMock()
            mock_settings_cls.return_value = mock_settings_manager
            mock_temp_auth.store_auth.return_value = "test_auth_token"

            from local_deep_research.web.auth.routes import auth_bp

            app.register_blueprint(auth_bp)

            with app.test_client() as client:
                response = client.post(
                    "/auth/login?next=/dashboard",
                    data={"username": "testuser", "password": "password123"},
                )

                # Should redirect to dashboard
                assert response.status_code == 302
                assert "/dashboard" in response.location

    def test_path_traversal_blocked(self):
        """Path traversal in next param is blocked by is_safe_redirect_url.

        The validator detects '..' in the URL path component and rejects
        it, so login falls back to the index page instead of redirecting
        to the traversal target.
        """
        app = Flask(__name__)
        app.secret_key = "test"
        app.config["WTF_CSRF_ENABLED"] = False

        @app.route("/")
        def index():
            return "Index"

        mock_engine = MagicMock()
        mock_session = MagicMock()
        mock_settings_manager = MagicMock()
        mock_settings_manager.db_version_matches_package.return_value = True

        with (
            patch(
                "local_deep_research.web.auth.routes.load_server_config"
            ) as mock_config,
            patch(
                "local_deep_research.web.auth.routes.db_manager"
            ) as mock_db_manager,
            patch(
                "local_deep_research.web.auth.routes.session_manager"
            ) as mock_session_manager,
            patch(
                "local_deep_research.web.auth.routes.auth_db_session"
            ) as mock_auth_db,
            patch(
                "local_deep_research.database.temp_auth.temp_auth_store"
            ) as mock_temp_auth,
            patch(
                "local_deep_research.database.session_passwords.session_password_store"
            ),
            patch(
                "local_deep_research.settings.manager.SettingsManager"
            ) as mock_settings_cls,
            patch(
                "local_deep_research.database.library_init.initialize_library_for_user"
            ),
            patch(
                "local_deep_research.scheduler.background.get_background_job_scheduler"
            ),
            patch("local_deep_research.database.models.ProviderModel"),
        ):
            mock_config.return_value = {"allow_registrations": True}
            mock_db_manager.open_user_database.return_value = mock_engine
            mock_db_manager.get_session.return_value = mock_session
            mock_session_manager.create_session.return_value = "session_123"
            # auth_db_session: background-thread-only, auto-CM sufficient (see first test)
            mock_auth_db.return_value = MagicMock()
            mock_settings_cls.return_value = mock_settings_manager
            mock_temp_auth.store_auth.return_value = "test_auth_token"

            from local_deep_research.web.auth.routes import auth_bp

            app.register_blueprint(auth_bp)

            with app.test_client() as client:
                response = client.post(
                    "/auth/login?next=/../../../etc/passwd",
                    data={"username": "testuser", "password": "password123"},
                )

                # Path traversal is blocked by is_safe_redirect_url,
                # so login succeeds but redirects to index (fallback)
                assert response.status_code == 302
                assert "etc/passwd" not in response.location

    def test_encoded_path_traversal_blocked(self):
        """URL-encoded path traversal (%2e%2e) in next param is blocked."""
        app = Flask(__name__)
        app.secret_key = "test"
        app.config["WTF_CSRF_ENABLED"] = False

        @app.route("/")
        def index():
            return "Index"

        mock_engine = MagicMock()
        mock_session = MagicMock()
        mock_settings_manager = MagicMock()
        mock_settings_manager.db_version_matches_package.return_value = True

        with (
            patch(
                "local_deep_research.web.auth.routes.load_server_config"
            ) as mock_config,
            patch(
                "local_deep_research.web.auth.routes.db_manager"
            ) as mock_db_manager,
            patch(
                "local_deep_research.web.auth.routes.session_manager"
            ) as mock_session_manager,
            patch(
                "local_deep_research.web.auth.routes.auth_db_session"
            ) as mock_auth_db,
            patch(
                "local_deep_research.database.temp_auth.temp_auth_store"
            ) as mock_temp_auth,
            patch(
                "local_deep_research.database.session_passwords.session_password_store"
            ),
            patch(
                "local_deep_research.settings.manager.SettingsManager"
            ) as mock_settings_cls,
            patch(
                "local_deep_research.database.library_init.initialize_library_for_user"
            ),
            patch(
                "local_deep_research.scheduler.background.get_background_job_scheduler"
            ),
            patch("local_deep_research.database.models.ProviderModel"),
        ):
            mock_config.return_value = {"allow_registrations": True}
            mock_db_manager.open_user_database.return_value = mock_engine
            mock_db_manager.get_session.return_value = mock_session
            mock_session_manager.create_session.return_value = "session_123"
            # auth_db_session: background-thread-only, auto-CM sufficient (see first test)
            mock_auth_db.return_value = MagicMock()
            mock_settings_cls.return_value = mock_settings_manager
            mock_temp_auth.store_auth.return_value = "test_auth_token"

            from local_deep_research.web.auth.routes import auth_bp

            app.register_blueprint(auth_bp)

            with app.test_client() as client:
                response = client.post(
                    "/auth/login?next=%2e%2e/admin",
                    data={"username": "testuser", "password": "password123"},
                )

                # Encoded path traversal is blocked
                assert response.status_code == 302
                assert "admin" not in response.location


class TestRedirectPathExtraction:
    """Tests for defense-in-depth path extraction from redirect URLs.

    After is_safe_redirect_url validates same-host, the code extracts only
    the path+query+fragment via urljoin+urlparse. These tests verify that
    absolute URLs are correctly converted to relative paths and edge cases
    are handled safely.
    """

    def _make_app_and_login(self, next_param):
        """Helper: create app, register auth blueprint, POST login with next param."""
        app = Flask(__name__)
        app.secret_key = "test"
        app.config["WTF_CSRF_ENABLED"] = False

        @app.route("/")
        def index():
            return "Index"

        mock_engine = MagicMock()
        mock_session = MagicMock()
        mock_settings_manager = MagicMock()
        mock_settings_manager.db_version_matches_package.return_value = True

        with (
            patch(
                "local_deep_research.web.auth.routes.load_server_config"
            ) as mock_config,
            patch(
                "local_deep_research.web.auth.routes.db_manager"
            ) as mock_db_manager,
            patch(
                "local_deep_research.web.auth.routes.session_manager"
            ) as mock_session_manager,
            patch(
                "local_deep_research.web.auth.routes.auth_db_session"
            ) as mock_auth_db,
            patch(
                "local_deep_research.database.temp_auth.temp_auth_store"
            ) as mock_temp_auth,
            patch(
                "local_deep_research.database.session_passwords.session_password_store"
            ),
            patch(
                "local_deep_research.settings.manager.SettingsManager"
            ) as mock_settings_cls,
            patch(
                "local_deep_research.database.library_init.initialize_library_for_user"
            ),
            patch(
                "local_deep_research.scheduler.background.get_background_job_scheduler"
            ),
            patch("local_deep_research.database.models.ProviderModel"),
        ):
            mock_config.return_value = {"allow_registrations": True}
            mock_db_manager.open_user_database.return_value = mock_engine
            mock_db_manager.get_session.return_value = mock_session
            mock_session_manager.create_session.return_value = "session_123"
            # auth_db_session: background-thread-only, auto-CM sufficient (see first test)
            mock_auth_db.return_value = MagicMock()
            mock_settings_cls.return_value = mock_settings_manager
            mock_temp_auth.store_auth.return_value = "test_auth_token"

            from local_deep_research.web.auth.routes import auth_bp

            app.register_blueprint(auth_bp)

            with app.test_client() as client:
                response = client.post(
                    f"/auth/login?next={next_param}",
                    data={"username": "testuser", "password": "password123"},
                )
                return response

    def test_absolute_same_origin_url_extracts_path(self):
        """Absolute same-origin URL should redirect to path only."""
        response = self._make_app_and_login("http://localhost/dashboard")
        assert response.status_code == 302
        assert response.location.endswith("/dashboard")
        assert "localhost" not in response.location.split("/dashboard")[0]

    def test_preserves_query_string(self):
        """Query string in next param should be preserved in redirect."""
        # URL-encode the next value so &page=2 isn't split as a separate param
        from urllib.parse import quote

        response = self._make_app_and_login(
            quote("/search?q=test&page=2", safe="")
        )
        assert response.status_code == 302
        assert "/search?q=test&page=2" in response.location

    def test_preserves_fragment(self):
        """Fragment in next param should be preserved in redirect."""
        response = self._make_app_and_login("/dashboard%23section")
        assert response.status_code == 302
        assert "/dashboard" in response.location

    def test_absolute_url_with_query_extracts_path_and_query(self):
        """Absolute URL with query should extract path+query only."""
        from urllib.parse import quote

        response = self._make_app_and_login(
            quote("http://localhost/research?id=123&tab=sources", safe="")
        )
        assert response.status_code == 302
        assert "/research?id=123&tab=sources" in response.location
        # Must not contain the scheme/host
        assert "http://localhost" not in response.location

    def test_no_next_param_redirects_to_index(self):
        """Missing next param should redirect to index."""
        app = Flask(__name__)
        app.secret_key = "test"
        app.config["WTF_CSRF_ENABLED"] = False

        @app.route("/")
        def index():
            return "Index"

        mock_engine = MagicMock()
        mock_session = MagicMock()
        mock_settings_manager = MagicMock()
        mock_settings_manager.db_version_matches_package.return_value = True

        with (
            patch(
                "local_deep_research.web.auth.routes.load_server_config"
            ) as mock_config,
            patch(
                "local_deep_research.web.auth.routes.db_manager"
            ) as mock_db_manager,
            patch(
                "local_deep_research.web.auth.routes.session_manager"
            ) as mock_session_manager,
            patch(
                "local_deep_research.web.auth.routes.auth_db_session"
            ) as mock_auth_db,
            patch(
                "local_deep_research.database.temp_auth.temp_auth_store"
            ) as mock_temp_auth,
            patch(
                "local_deep_research.database.session_passwords.session_password_store"
            ),
            patch(
                "local_deep_research.settings.manager.SettingsManager"
            ) as mock_settings_cls,
            patch(
                "local_deep_research.database.library_init.initialize_library_for_user"
            ),
            patch(
                "local_deep_research.scheduler.background.get_background_job_scheduler"
            ),
            patch("local_deep_research.database.models.ProviderModel"),
        ):
            mock_config.return_value = {"allow_registrations": True}
            mock_db_manager.open_user_database.return_value = mock_engine
            mock_db_manager.get_session.return_value = mock_session
            mock_session_manager.create_session.return_value = "session_123"
            # auth_db_session: background-thread-only, auto-CM sufficient (see first test)
            mock_auth_db.return_value = MagicMock()
            mock_settings_cls.return_value = mock_settings_manager
            mock_temp_auth.store_auth.return_value = "test_auth_token"

            from local_deep_research.web.auth.routes import auth_bp

            app.register_blueprint(auth_bp)

            with app.test_client() as client:
                response = client.post(
                    "/auth/login",
                    data={"username": "testuser", "password": "password123"},
                )
                assert response.status_code == 302
                assert response.location.endswith("/")


class TestPostLoginTasksPasswordForwarding:
    """Tests for _perform_post_login_tasks password forwarding."""

    def test_post_login_tasks_forwards_password(self):
        """get_user_db_session must be called with both username AND password."""
        with (
            patch(
                "local_deep_research.settings.manager.SettingsManager"
            ) as mock_settings_cls,
            patch(
                "local_deep_research.database.library_init.initialize_library_for_user"
            ) as mock_init_lib,
            patch(
                "local_deep_research.web.auth.routes.auth_db_session"
            ) as mock_auth_db,
            patch(
                "local_deep_research.scheduler.background.get_background_job_scheduler"
            ),
            patch("local_deep_research.database.models.ProviderModel"),
            patch(
                "local_deep_research.database.session_context.get_user_db_session"
            ) as mock_get_session,
        ):
            mock_settings_mgr = MagicMock()
            mock_settings_mgr.db_version_matches_package.return_value = True
            mock_settings_cls.return_value = mock_settings_mgr
            mock_init_lib.return_value = {"success": True}
            mock_auth_db.return_value.__enter__ = MagicMock(
                return_value=MagicMock()
            )
            mock_auth_db.return_value.__exit__ = MagicMock(return_value=False)

            # get_user_db_session is used as context manager
            mock_session = MagicMock()
            mock_get_session.return_value.__enter__ = MagicMock(
                return_value=mock_session
            )
            mock_get_session.return_value.__exit__ = MagicMock(
                return_value=False
            )

            from local_deep_research.web.auth.routes import (
                _perform_post_login_tasks,
            )

            # Call directly (synchronous — @thread_cleanup is transparent)
            _perform_post_login_tasks.__wrapped__("testuser", "testpass")

            # get_user_db_session should be called with (username, password)
            # at least twice: once for settings migration, once for model cache
            calls = mock_get_session.call_args_list
            password_calls = [
                c
                for c in calls
                if c.args == ("testuser", "testpass")
                or c.kwargs.get("password") == "testpass"
            ]
            assert len(password_calls) >= 2, (
                f"Expected get_user_db_session called with password at least "
                f"twice, got {len(password_calls)}: {calls}"
            )


class TestRegistrationTimingAttackPrevention:
    """Tests for registration timing attack prevention.

    These tests verify that the registration flow follows OWASP best practices:
    - Generic error messages (prevent content-based enumeration)
    - IntegrityError handling (catches race conditions with generic error + rollback)
    """

    def test_generic_error_message_for_duplicate_username(self):
        """Verify duplicate username returns generic error message (OWASP requirement)."""
        from unittest.mock import patch

        app = Flask(__name__)
        app.secret_key = "test"
        app.config["WTF_CSRF_ENABLED"] = False

        with (
            patch(
                "local_deep_research.web.auth.routes.load_server_config"
            ) as mock_config,
            patch("local_deep_research.web.auth.routes.db_manager") as mock_db,
            patch(
                "local_deep_research.web.auth.routes.render_template"
            ) as mock_render,
            patch("local_deep_research.web.auth.routes.flash") as mock_flash,
        ):
            mock_config.return_value = {"allow_registrations": True}
            mock_db.user_exists.return_value = True  # User already exists
            mock_db.has_encryption = True
            mock_render.return_value = "Register Page"

            from local_deep_research.web.auth.routes import auth_bp

            app.register_blueprint(auth_bp)

            with app.test_client() as client:
                response = client.post(
                    "/auth/register",
                    data={
                        "username": "existinguser",
                        "password": "TestPassword123!",
                        "confirm_password": "TestPassword123!",
                        "acknowledge": "true",
                    },
                )

                assert response.status_code == 400
                mock_flash.assert_any_call(
                    "Registration failed. Please try a different username.",
                    "error",
                )

    def test_integrity_error_returns_generic_message(self):
        """Verify IntegrityError (race condition) returns generic error (400 not 500)."""
        from unittest.mock import MagicMock, patch
        from sqlalchemy.exc import IntegrityError

        app = Flask(__name__)
        app.secret_key = "test"
        app.config["WTF_CSRF_ENABLED"] = False

        with (
            patch(
                "local_deep_research.web.auth.routes.load_server_config"
            ) as mock_config,
            patch("local_deep_research.web.auth.routes.db_manager") as mock_db,
            patch(
                "local_deep_research.web.auth.routes.auth_db_session"
            ) as mock_auth_db,
            patch(
                "local_deep_research.web.auth.routes.render_template"
            ) as mock_render,
            patch("local_deep_research.web.auth.routes.flash") as mock_flash,
        ):
            mock_config.return_value = {"allow_registrations": True}
            mock_db.user_exists.return_value = False  # Passes pre-check
            mock_db.has_encryption = True
            mock_render.return_value = "Register Page"

            # Simulate IntegrityError on commit
            mock_session = MagicMock()
            mock_session.add = MagicMock()
            mock_session.commit = MagicMock(
                side_effect=IntegrityError("statement", "params", "orig")
            )
            mock_session.rollback = MagicMock()
            mock_session.close = MagicMock()
            # auth_db_session() is used as a context manager
            mock_auth_db.return_value.__enter__ = MagicMock(
                return_value=mock_session
            )
            mock_auth_db.return_value.__exit__ = MagicMock(return_value=False)

            from local_deep_research.web.auth.routes import auth_bp

            app.register_blueprint(auth_bp)

            with app.test_client() as client:
                response = client.post(
                    "/auth/register",
                    data={
                        "username": "racecondition_user",
                        "password": "TestPassword123!",
                        "confirm_password": "TestPassword123!",
                        "acknowledge": "true",
                    },
                )

                assert response.status_code == 400
                mock_flash.assert_any_call(
                    "Registration failed. Please try a different username.",
                    "error",
                )
                mock_session.rollback.assert_called_once()
                mock_session.add.assert_called_once()
