"""API contract tests for the report-content display routes.

Locks the public shape of report content so a future change can't silently
regress it (#3665):

* ``content`` is the *assembled* legacy view — answer + ``## Sources`` (from
  the ``research_resources`` table) + ``## Research Metrics`` (from
  ``research_meta``) — across every display/export route.
* ``GET /api/report/<id>`` exposes a ``sources`` field populated from
  ``research_resources``. Before the fix it read the dead
  ``all_links_of_system`` metadata key, which the post-refactor save path
  never writes, so it returned ``[]`` for every new research (Fix A).

These exercise the *real* assembly path (``assemble_full_report`` /
``get_research_source_links_batch``) against real seeded rows; only auth and
the per-user-DB plumbing are stubbed.
"""

from contextlib import contextmanager
from datetime import datetime, UTC
from unittest.mock import MagicMock, patch

import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

from local_deep_research.database.models import Base
from local_deep_research.database.models.research import (
    ResearchHistory,
    ResearchResource,
)

RESEARCH_ID = "contract-research-1"
# Deliberately > the batch helper's default top-N (3): the report API must
# return *every* source (no cap), so a reintroduced default limit would fail
# test_api_report_sources_field_matches_research_resources_count.
N_SOURCES = 5


@pytest.fixture(autouse=True)
def mock_db_manager():
    """Satisfy the auth middleware/decorators without a real encrypted DB.

    Mirrors tests/test_followup_api.py: every module-level ``db_manager``
    binding used by the Flask before_request middleware and ``login_required``
    is mocked so an authenticated session passes. ``has_encryption=False``
    skips the encrypted-DB password check.
    """
    with (
        patch("local_deep_research.web.auth.decorators.db_manager") as mock_db,
        patch("local_deep_research.database.encrypted_db.db_manager"),
        patch("local_deep_research.web.auth.database_middleware.db_manager"),
        patch("local_deep_research.web.auth.session_cleanup.db_manager"),
        patch("local_deep_research.web.auth.queue_middleware.db_manager"),
    ):
        mock_db.connections = {"testuser": MagicMock()}
        mock_db.has_encryption = False
        mock_db.is_user_connected.return_value = True
        yield mock_db


@pytest.fixture
def mock_auth_client(app):
    """Test client authenticated via a mock session.

    Uses the shared ``app`` fixture (tests/conftest.py) but authenticates by
    seeding the session directly rather than a real register/login, because
    auth is stubbed via ``mock_db_manager`` and the per-user DB is patched to
    a seeded SQLite file (``seeded_db``). The real ``authenticated_client``
    fixture would create a real encrypted DB we couldn't seed sources into.
    """
    client = app.test_client()
    with client.session_transaction() as sess:
        sess["username"] = "testuser"
        sess["authenticated"] = True
    return client


@pytest.fixture
def seeded_db(tmp_path):
    """Real temp-file SQLite seeded with one completed research + sources.

    Patches ``get_user_db_session`` in both route modules to yield a fresh
    session bound to the seeded DB (a fresh session per request, like the
    real per-request session). A file DB (not ``:memory:``) so every
    ``SessionLocal()`` connection sees the same seeded rows.
    """
    engine = create_engine(f"sqlite:///{tmp_path / 'report_contract.db'}")
    Base.metadata.create_all(engine)
    SessionLocal = sessionmaker(bind=engine)

    seed = SessionLocal()
    seed.add(
        ResearchHistory(
            id=RESEARCH_ID,
            query="What is X?",
            mode="quick",
            status="completed",
            created_at="2026-04-25T12:00:00+00:00",
            completed_at="2026-04-25T12:05:00+00:00",
            report_content=(
                "X is explained here, citing [1](https://src1.example) "
                "and [2](https://src2.example)."
            ),
            research_meta={
                "iterations": 2,
                "generated_at": "2026-04-25T12:05:00+00:00",
            },
        )
    )
    for i in range(1, N_SOURCES + 1):
        url = f"https://src{i}.example"
        seed.add(
            ResearchResource(
                research_id=RESEARCH_ID,
                title=f"Source {i}",
                url=url,
                source_type="web",
                resource_metadata={
                    "original_data": {
                        "index": str(i),
                        "url": url,
                        "title": f"Source {i}",
                    }
                },
                created_at=datetime.now(UTC).isoformat(),
            )
        )
    seed.commit()
    seed.close()

    @contextmanager
    def _fake_user_db(username=None, password=None):
        db = SessionLocal()
        try:
            yield db
        finally:
            db.close()

    with (
        patch(
            "local_deep_research.web.routes.research_routes.get_user_db_session",
            _fake_user_db,
        ),
        patch(
            "local_deep_research.web.routes.history_routes.get_user_db_session",
            _fake_user_db,
        ),
    ):
        yield

    engine.dispose()


# ---------------------------------------------------------------------------
# Assembled-view shape across display routes (locks the legacy report shape)
# ---------------------------------------------------------------------------


def test_history_report_includes_sources_and_metrics(
    mock_auth_client, seeded_db
):
    resp = mock_auth_client.get(f"/history/report/{RESEARCH_ID}")
    assert resp.status_code == 200, resp.data
    content = resp.get_json()["content"]
    assert "## Sources\n" in content
    assert "## Research Metrics\n" in content
    # Sources block references the structured resources.
    assert "src1.example" in content


def test_history_markdown_includes_sources_and_metrics(
    mock_auth_client, seeded_db
):
    resp = mock_auth_client.get(f"/history/markdown/{RESEARCH_ID}")
    assert resp.status_code == 200, resp.data
    content = resp.get_json()["content"]
    assert "## Sources\n" in content
    assert "## Research Metrics\n" in content


def test_api_report_content_includes_sources_and_metrics(
    mock_auth_client, seeded_db
):
    resp = mock_auth_client.get(f"/api/report/{RESEARCH_ID}")
    assert resp.status_code == 200, resp.data
    content = resp.get_json()["content"]
    assert "## Sources\n" in content
    assert "## Research Metrics\n" in content


def test_export_latex_content_includes_sources_and_metrics(
    mock_auth_client, seeded_db
):
    # LaTeX is the registered plain-text export format (markdown is served by
    # /history/markdown; quarto is a .zip; pdf/odt are binary). LaTeX
    # conversion drops the markdown `##` prefix but keeps the header words and
    # body text. "Research Metrics" / "Search Iterations" exist ONLY in the
    # assembled view, never in the answer-only report_content, so finding them
    # proves the export path goes through assemble_full_report.
    resp = mock_auth_client.post(f"/api/v1/research/{RESEARCH_ID}/export/latex")
    assert resp.status_code == 200, resp.data
    body = resp.data.decode("utf-8")
    assert "Research Metrics" in body
    assert "Search Iterations" in body
    assert "Sources" in body
    assert "src1.example" in body


# ---------------------------------------------------------------------------
# Fix A regression fence: /api/report sources field from research_resources
# ---------------------------------------------------------------------------


def test_api_report_sources_field_populated_from_research_resources(
    mock_auth_client, seeded_db
):
    resp = mock_auth_client.get(f"/api/report/{RESEARCH_ID}")
    assert resp.status_code == 200, resp.data
    sources = resp.get_json()["sources"]
    assert isinstance(sources, list)
    assert sources, "sources must not be empty (Fix A regression)"
    for entry in sources:
        assert entry.get("url")
        assert entry.get("title")


def test_api_report_sources_field_matches_research_resources_count(
    mock_auth_client, seeded_db
):
    resp = mock_auth_client.get(f"/api/report/{RESEARCH_ID}")
    assert resp.status_code == 200, resp.data
    sources = resp.get_json()["sources"]
    assert len(sources) == N_SOURCES
