"""
HTTP behavioral tests for encrypted DB password check at research start.

Verifies that /api/start_research returns 401 when the encrypted DB
password is unavailable, and that the check happens BEFORE creating
database records (no orphaned IN_PROGRESS rows).
"""

from contextlib import contextmanager
from unittest.mock import Mock, patch

from flask import Flask, jsonify

from local_deep_research.followup_research.routes import followup_bp
from local_deep_research.web.auth.routes import auth_bp
from local_deep_research.web.routes.research_routes import research_bp


# ---------------------------------------------------------------------------
# Test Infrastructure
# ---------------------------------------------------------------------------

_ROUTES_MOD = "local_deep_research.web.routes.research_routes"


def _create_test_app():
    """Create Flask app with auth + research blueprints."""
    app = Flask(__name__)
    app.config["SECRET_KEY"] = "test-secret"
    app.config["WTF_CSRF_ENABLED"] = False
    app.register_blueprint(auth_bp)
    app.register_blueprint(research_bp)

    @app.errorhandler(500)
    def _handle_500(error):
        return jsonify({"error": "Internal server error"}), 500

    return app


@contextmanager
def _authenticated_client(app, has_encryption=False, password_available=True):
    """Provide test client with mocked auth, DB session, and settings."""
    mock_db = Mock()
    mock_db.connections = {"testuser": True}
    mock_db.has_encryption = has_encryption

    # Chainable mock query
    _mock_query = Mock()
    _mock_query.all.return_value = []
    _mock_query.first.return_value = None
    _mock_query.count.return_value = 0
    _mock_query.scalar.return_value = 0
    _mock_query.filter_by.return_value = _mock_query
    _mock_query.filter.return_value = _mock_query
    _mock_query.order_by.return_value = _mock_query
    _mock_query.limit.return_value = _mock_query

    _mock_db_session = Mock()
    _mock_db_session.query.return_value = _mock_query

    @contextmanager
    def _fake_session(*args, **kwargs):
        yield _mock_db_session

    # Mock session password store
    mock_password_store = Mock()
    if password_available:
        mock_password_store.get_session_password.return_value = "test-password"
    else:
        mock_password_store.get_session_password.return_value = None

    # Mock SettingsManager
    mock_settings_manager = Mock()
    mock_settings_manager.get_setting.side_effect = lambda key, default=None: {
        "llm.provider": "OLLAMA",
        "llm.model": "test-model",
        "search.tool": "searxng",
        "search.iterations": 5,
        "search.questions_per_iteration": 5,
        "search.search_strategy": "source-based",
        "app.max_concurrent_researches": 3,
    }.get(key, default)
    mock_settings_manager.get_all_settings.return_value = {}

    # Mock start_research_process to avoid actually starting threads
    mock_start = Mock()
    mock_start.return_value = Mock()  # mock thread

    # Mock SettingsManager constructor to return our mock
    mock_settings_cls = Mock(return_value=mock_settings_manager)

    patches = [
        patch("local_deep_research.web.auth.decorators.db_manager", mock_db),
        patch(f"{_ROUTES_MOD}.db_manager", mock_db),
        patch(
            f"{_ROUTES_MOD}.get_user_db_session",
            side_effect=_fake_session,
        ),
        patch(
            f"{_ROUTES_MOD}.start_research_process",
            mock_start,
        ),
        patch(
            "local_deep_research.database.session_passwords.session_password_store",
            mock_password_store,
        ),
        patch(
            "local_deep_research.settings.manager.SettingsManager",
            mock_settings_cls,
        ),
        patch(
            "local_deep_research.settings.SettingsManager",
            mock_settings_cls,
        ),
    ]

    started = []
    try:
        for p in patches:
            started.append(p.start())
        with app.test_client() as client:
            with client.session_transaction() as sess:
                sess["username"] = "testuser"
                sess["session_id"] = "test-session-id"
            yield client, mock_start, _mock_db_session
    finally:
        for p in patches:
            p.stop()


def _post_start_research(client):
    """POST a minimal research request."""
    return client.post(
        "/api/start_research",
        json={
            "query": "test query",
            "mode": "quick",
            "model_provider": "OLLAMA",
            "model": "test-model",
        },
        content_type="application/json",
    )


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------


class TestEncryptedDbPasswordCheck:
    """Verify encrypted DB password check at /api/start_research."""

    def test_encrypted_db_no_password_returns_401(self):
        """Encrypted DB + no password → 401 with session expired message."""
        app = _create_test_app()
        with _authenticated_client(
            app, has_encryption=True, password_available=False
        ) as (client, mock_start, mock_db_session):
            resp = _post_start_research(client)
            assert resp.status_code == 401
            data = resp.get_json()
            assert "session has expired" in data["message"].lower()
            # Research should NOT have been started
            mock_start.assert_not_called()
            # No DB record should have been created (core guarantee)
            mock_db_session.add.assert_not_called()

    def test_encrypted_db_with_password_does_not_return_401(self):
        """Encrypted DB + password available → should NOT return 401."""
        app = _create_test_app()
        with _authenticated_client(
            app, has_encryption=True, password_available=True
        ) as (client, _mock_start, _mock_db_session):
            resp = _post_start_research(client)
            # 500 is expected from incomplete mocking (settings snapshot);
            # the key assertion is that the password check passed (not 401).
            assert resp.status_code in (200, 500)

    def test_unencrypted_db_no_password_does_not_return_401(
        self,
    ):  # DevSkim: ignore DS101155 - testing DB encryption flag, not TLS certificates
        """Unencrypted DB + no password → should NOT return 401 (warning only)."""
        app = _create_test_app()
        with _authenticated_client(
            app, has_encryption=False, password_available=False
        ) as (client, _mock_start, _mock_db_session):
            resp = _post_start_research(client)
            assert resp.status_code in (200, 500)


# ---------------------------------------------------------------------------
# Follow-up route tests
# ---------------------------------------------------------------------------

_FOLLOWUP_MOD = "local_deep_research.followup_research.routes"


def _create_followup_test_app():
    """Create Flask app with auth + followup blueprints."""
    app = Flask(__name__)
    app.config["SECRET_KEY"] = "test-secret"
    app.config["WTF_CSRF_ENABLED"] = False
    app.register_blueprint(auth_bp)
    app.register_blueprint(followup_bp)

    @app.errorhandler(500)
    def _handle_500(error):
        return jsonify({"error": "Internal server error"}), 500

    return app


@contextmanager
def _authenticated_followup_client(
    app, has_encryption=False, password_available=True
):
    """Provide test client with mocked auth and followup dependencies."""
    mock_db = Mock()
    mock_db.connections = {"testuser": True}
    mock_db.has_encryption = has_encryption

    _mock_db_session = Mock()

    @contextmanager
    def _fake_session(*args, **kwargs):
        yield _mock_db_session

    mock_password_store = Mock()
    if password_available:
        mock_password_store.get_session_password.return_value = "test-password"
    else:
        mock_password_store.get_session_password.return_value = None

    mock_settings_manager = Mock()
    mock_settings_manager.get_all_settings.return_value = {}
    mock_settings_cls = Mock(return_value=mock_settings_manager)

    patches = [
        patch("local_deep_research.web.auth.decorators.db_manager", mock_db),
        patch("local_deep_research.database.encrypted_db.db_manager", mock_db),
        patch(
            "local_deep_research.database.session_passwords.session_password_store",
            mock_password_store,
        ),
        patch(
            "local_deep_research.database.session_context.get_user_db_session",
            side_effect=_fake_session,
        ),
        patch(
            "local_deep_research.settings.manager.SettingsManager",
            mock_settings_cls,
        ),
    ]

    started = []
    try:
        for p in patches:
            started.append(p.start())
        with app.test_client() as client:
            with client.session_transaction() as sess:
                sess["username"] = "testuser"
                sess["session_id"] = "test-session-id"
            yield client, _mock_db_session
    finally:
        for p in patches:
            p.stop()


def _post_followup_start(client):
    """POST a minimal followup start request."""
    return client.post(
        "/api/followup/start",
        json={
            "parent_research_id": "fake-parent-id",
            "question": "follow-up question",
        },
        content_type="application/json",
    )


class TestFollowupEncryptedDbPasswordCheck:
    """Verify encrypted DB password check at /api/followup/start."""

    def test_encrypted_db_no_password_returns_401(self):
        """Encrypted DB + no password → 401 on followup start."""
        app = _create_followup_test_app()
        with _authenticated_followup_client(
            app, has_encryption=True, password_available=False
        ) as (client, mock_db_session):
            resp = _post_followup_start(client)
            assert resp.status_code == 401
            data = resp.get_json()
            assert data["success"] is False
            assert "session has expired" in data["error"].lower()
            # No DB record should have been created
            mock_db_session.add.assert_not_called()
