"""Extended tests for history route functions.

Covers get_history, get_research_status, and get_log_count endpoints
with emphasis on parameter clamping, duration recalculation, and
active-research progress reporting.
"""

from unittest.mock import MagicMock, patch

import pytest
from flask import Flask

from local_deep_research.web.auth import auth_bp
from local_deep_research.web.routes.history_routes import history_bp

# Target path for the db_manager used inside the login_required decorator
_AUTH_DB_MANAGER = "local_deep_research.web.auth.decorators.db_manager"
# Target path prefix for objects imported into history_routes
_HR = "local_deep_research.web.routes.history_routes"


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture()
def app():
    """Minimal Flask app with auth and history blueprints registered."""
    application = Flask(__name__)
    application.config["SECRET_KEY"] = "test-secret-key"
    application.config["TESTING"] = True

    # auth_bp is needed because login_required calls url_for("auth.login")
    application.register_blueprint(auth_bp)
    application.register_blueprint(history_bp)

    return application


@pytest.fixture()
def client(app):
    """Unauthenticated test client."""
    return app.test_client()


def _mock_auth_db_manager():
    """Return a MagicMock that satisfies the login_required db_manager check."""
    return MagicMock(is_user_connected=MagicMock(return_value=True))


def _make_db_session_ctx(mock_session):
    """Build a mock context-manager for get_user_db_session."""
    ctx = MagicMock()
    ctx.__enter__ = MagicMock(return_value=mock_session)
    ctx.__exit__ = MagicMock(return_value=None)
    return ctx


def _build_query_chain(results):
    """Build a chained SQLAlchemy-style mock query returning *results*."""
    mock_query = MagicMock()
    (
        mock_query.outerjoin.return_value.group_by.return_value.order_by.return_value.limit.return_value.offset.return_value.all
    ).return_value = results
    return mock_query


# ---------------------------------------------------------------------------
# get_history  /history/api
# ---------------------------------------------------------------------------


class TestGetHistoryAuth:
    """Authentication tests for the /history/api endpoint."""

    def test_returns_401_when_not_authenticated(self, client):
        """An unauthenticated request to an API path returns JSON 401."""
        response = client.get("/history/api")
        # /history/api ends in /api, so login_required treats it as an API
        # route and returns JSON 401 instead of redirecting.
        assert response.status_code == 401
        assert response.get_json() == {"error": "Authentication required"}

    def test_returns_success_with_items_when_authenticated(self, app):
        """Authenticated request returns JSON with status=success and items."""
        mock_session = MagicMock()
        mock_session.query.return_value = _build_query_chain([])

        with (
            patch(
                f"{_HR}.get_user_db_session",
                return_value=_make_db_session_ctx(mock_session),
            ),
            patch(_AUTH_DB_MANAGER, _mock_auth_db_manager()),
        ):
            with app.test_client() as c:
                with c.session_transaction() as sess:
                    sess["username"] = "testuser"
                response = c.get("/history/api")

        assert response.status_code == 200
        data = response.get_json()
        assert data["status"] == "success"
        assert "items" in data


class TestGetHistoryParameterClamping:
    """Verify that limit and offset are clamped to safe ranges."""

    def _get_history(self, app, query_string):
        """Helper that issues an authenticated GET to /history/api."""
        mock_session = MagicMock()
        mock_query = _build_query_chain([])
        mock_session.query.return_value = mock_query

        with (
            patch(
                f"{_HR}.get_user_db_session",
                return_value=_make_db_session_ctx(mock_session),
            ),
            patch(_AUTH_DB_MANAGER, _mock_auth_db_manager()),
        ):
            with app.test_client() as c:
                with c.session_transaction() as sess:
                    sess["username"] = "testuser"
                response = c.get("/history/api", query_string=query_string)

        return response, mock_query

    def test_limit_zero_clamped_to_one(self, app):
        """limit=0 should be clamped to 1."""
        response, mock_query = self._get_history(app, {"limit": 0})
        assert response.status_code == 200

        # Walk the chain to the .limit() call and inspect its argument
        limit_call = mock_query.outerjoin.return_value.group_by.return_value.order_by.return_value.limit
        limit_call.assert_called_once_with(1)

    def test_limit_above_500_clamped(self, app):
        """limit=1000 should be clamped to 500."""
        response, mock_query = self._get_history(app, {"limit": 1000})
        assert response.status_code == 200

        limit_call = mock_query.outerjoin.return_value.group_by.return_value.order_by.return_value.limit
        limit_call.assert_called_once_with(500)

    def test_negative_offset_clamped_to_zero(self, app):
        """offset=-5 should be clamped to 0."""
        response, mock_query = self._get_history(app, {"offset": -5})
        assert response.status_code == 200

        offset_call = mock_query.outerjoin.return_value.group_by.return_value.order_by.return_value.limit.return_value.offset
        offset_call.assert_called_once_with(0)


class TestGetHistoryDurationRecalculation:
    """Verify that duration is recalculated when duration_seconds is None."""

    def test_recalculates_duration_from_timestamps(self, app):
        """When duration_seconds is None but timestamps exist, duration is derived."""
        mock_research = MagicMock()
        mock_research.id = "dur-test"
        mock_research.title = "Duration Test"
        mock_research.query = "test"
        mock_research.mode = "quick"
        mock_research.status = "completed"
        mock_research.created_at = "2024-06-01T10:00:00"
        mock_research.completed_at = "2024-06-01T10:05:00"
        mock_research.duration_seconds = None
        mock_research.research_meta = None
        mock_research.chat_session_id = None
        mock_research.document_count = 0

        mock_session = MagicMock()
        mock_session.query.return_value = _build_query_chain([mock_research])

        with (
            patch(
                f"{_HR}.get_user_db_session",
                return_value=_make_db_session_ctx(mock_session),
            ),
            patch(_AUTH_DB_MANAGER, _mock_auth_db_manager()),
        ):
            with app.test_client() as c:
                with c.session_transaction() as sess:
                    sess["username"] = "testuser"
                response = c.get("/history/api")

        assert response.status_code == 200
        data = response.get_json()
        item = data["items"][0]
        # 5 minutes = 300 seconds
        assert item["duration_seconds"] == 300


# ---------------------------------------------------------------------------
# get_research_status  /history/status/<id>
# ---------------------------------------------------------------------------


class TestGetResearchStatus:
    """Tests for /history/status/<id> endpoint."""

    def test_returns_404_when_research_not_found(self, app):
        """Should return 404 JSON when research does not exist."""
        mock_session = MagicMock()
        mock_query = MagicMock()
        mock_query.filter_by.return_value.first.return_value = None
        mock_session.query.return_value = mock_query

        with (
            patch(
                f"{_HR}.get_user_db_session",
                return_value=_make_db_session_ctx(mock_session),
            ),
            patch(_AUTH_DB_MANAGER, _mock_auth_db_manager()),
        ):
            with app.test_client() as c:
                with c.session_transaction() as sess:
                    sess["username"] = "testuser"
                response = c.get("/history/status/nonexistent-id")

        assert response.status_code == 404
        data = response.get_json()
        # The shared _research_not_found helper emits a superset of both
        # historical 404 shapes: Shape B readers rely on "status"/"message",
        # Shape A readers rely on "error". All three must be present.
        assert data["status"] == "error"
        assert data["message"] == "Research not found"
        assert data["error"] == "Research not found"

    def test_returns_progress_from_active_research(self, app):
        """When research_id is in active_research, progress comes from there."""
        mock_research = MagicMock()
        mock_research.id = "active-1"
        mock_research.query = "running query"
        mock_research.mode = "deep"
        mock_research.status = "in_progress"
        mock_research.created_at = "2024-06-01T10:00:00"
        mock_research.completed_at = None
        mock_research.progress_log = "[]"
        mock_research.report_path = None

        mock_session = MagicMock()
        mock_query = MagicMock()
        mock_query.filter_by.return_value.first.return_value = mock_research
        mock_session.query.return_value = mock_query

        fake_snapshot = {
            "progress": 42,
            "status": "in_progress",
            "log": [{"time": "10:01", "message": "step 1"}],
            "settings": None,
        }

        with (
            patch(
                f"{_HR}.get_user_db_session",
                return_value=_make_db_session_ctx(mock_session),
            ),
            patch(
                f"{_HR}.get_active_research_snapshot",
                return_value=fake_snapshot,
            ),
            patch(_AUTH_DB_MANAGER, _mock_auth_db_manager()),
        ):
            with app.test_client() as c:
                with c.session_transaction() as sess:
                    sess["username"] = "testuser"
                response = c.get("/history/status/active-1")

        assert response.status_code == 200
        data = response.get_json()
        assert data["progress"] == 42
        assert len(data["log"]) == 1

    def test_returns_progress_100_for_completed_research(self, app):
        """Completed research that is not active should have progress=100."""
        mock_research = MagicMock()
        mock_research.id = "done-1"
        mock_research.query = "completed query"
        mock_research.mode = "quick"
        mock_research.status = "completed"
        mock_research.created_at = "2024-06-01T10:00:00"
        mock_research.completed_at = "2024-06-01T10:05:00"
        mock_research.progress_log = "[]"
        mock_research.report_path = "/reports/done.md"

        mock_session = MagicMock()
        mock_query = MagicMock()
        mock_query.filter_by.return_value.first.return_value = mock_research
        mock_session.query.return_value = mock_query

        with (
            patch(
                f"{_HR}.get_user_db_session",
                return_value=_make_db_session_ctx(mock_session),
            ),
            patch(f"{_HR}.get_active_research_snapshot", return_value=None),
            patch(_AUTH_DB_MANAGER, _mock_auth_db_manager()),
        ):
            with app.test_client() as c:
                with c.session_transaction() as sess:
                    sess["username"] = "testuser"
                response = c.get("/history/status/done-1")

        assert response.status_code == 200
        data = response.get_json()
        assert data["progress"] == 100


# ---------------------------------------------------------------------------
# get_log_count  /history/log_count/<id>
# ---------------------------------------------------------------------------


class TestGetLogCount:
    """Tests for /history/log_count/<id> endpoint."""

    def test_returns_total_logs_count(self, app):
        """Should return the total number of logs for the research."""
        with (
            patch(f"{_HR}.get_total_logs_for_research", return_value=37),
            patch(_AUTH_DB_MANAGER, _mock_auth_db_manager()),
        ):
            with app.test_client() as c:
                with c.session_transaction() as sess:
                    sess["username"] = "testuser"
                response = c.get("/history/log_count/some-id")

        assert response.status_code == 200
        data = response.get_json()
        assert data["status"] == "success"
        assert data["total_logs"] == 37
