"""Direct integration tests for ``ChatService.add_progress_step``.

This method previously had only indirect coverage via the migration /
cascade tests; this file adds direct coverage.

The method is the persistence half of the step-emit/persist symmetry
that the chat-mode research path in research_service.py maintains. Its
contract:

- ``content`` is required (``ValueError`` on None).
- Sequence number is allocated atomically by ``UPDATE...RETURNING`` on
  ``ResearchHistory.step_count``.
- If the research row doesn't exist, ``ValueError("Research ... not
  found")`` is raised and no ``chat_progress_steps`` row is written.
- Multiple steps for the same research get strictly increasing sequence
  numbers (1, 2, 3, ...).
- The denormalised ``session_id`` column on ``chat_progress_steps``
  mirrors the chat session the research was launched from.
- ``phase`` is optional and stored verbatim.
"""

import uuid

import pytest

from src.local_deep_research.chat.service import ChatService
from src.local_deep_research.database.models import (
    ChatProgressStep,
    ChatSession,
    ChatSessionStatus,
    ResearchHistory,
)


def _seed(db):
    """Insert a session + research row, return (session_id, research_id)."""
    sid = str(uuid.uuid4())
    rid = str(uuid.uuid4())
    db.add(
        ChatSession(
            id=sid,
            title="step-test",
            status=ChatSessionStatus.ACTIVE.value,
            message_count=0,
        )
    )
    db.add(
        ResearchHistory(
            id=rid,
            query="probe",
            mode="quick",
            status="in_progress",
            created_at="2026-05-14T00:00:00",
            chat_session_id=sid,
        )
    )
    db.commit()
    return sid, rid


def _patch_user_db(monkeypatch, SessionLocal):
    """Patch get_user_db_session inside chat.service to yield our test DB."""
    from contextlib import contextmanager

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

    monkeypatch.setattr(
        "src.local_deep_research.chat.service.get_user_db_session", _ctx
    )


def test_rejects_none_content(setup_database_for_all_tests, monkeypatch):
    """Content cannot be None — the row would violate NOT NULL."""
    SessionLocal = setup_database_for_all_tests
    _patch_user_db(monkeypatch, SessionLocal)

    with SessionLocal() as db:
        sid, rid = _seed(db)

    service = ChatService(username="alice")
    with pytest.raises(ValueError, match="content is required"):
        service.add_progress_step(session_id=sid, research_id=rid, content=None)


def test_rejects_unknown_research(setup_database_for_all_tests, monkeypatch):
    """The atomic UPDATE matches zero rows when the research doesn't
    exist, so add_progress_step raises and writes nothing."""
    SessionLocal = setup_database_for_all_tests
    _patch_user_db(monkeypatch, SessionLocal)

    service = ChatService(username="alice")
    with pytest.raises(ValueError, match="Research .* not found"):
        service.add_progress_step(
            session_id=str(uuid.uuid4()),
            research_id=str(uuid.uuid4()),
            content="step content",
        )


def test_persists_row_and_returns_id(setup_database_for_all_tests, monkeypatch):
    """A successful call persists a row in chat_progress_steps and
    returns the new step's UUID."""
    SessionLocal = setup_database_for_all_tests
    _patch_user_db(monkeypatch, SessionLocal)

    with SessionLocal() as db:
        sid, rid = _seed(db)

    service = ChatService(username="alice")
    step_id = service.add_progress_step(
        session_id=sid,
        research_id=rid,
        content="Searching for topic X",
        phase="search",
    )
    assert isinstance(step_id, str) and len(step_id) >= 32

    with SessionLocal() as db:
        row = db.query(ChatProgressStep).filter_by(id=step_id).first()
        assert row is not None
        assert row.session_id == sid
        assert row.research_id == rid
        assert row.content == "Searching for topic X"
        assert row.phase == "search"
        assert row.sequence_number == 1


def test_sequence_increments_monotonically(
    setup_database_for_all_tests, monkeypatch
):
    """Per-research sequence is allocated atomically via
    ``UPDATE step_count RETURNING``. Three consecutive calls must yield
    1, 2, 3."""
    SessionLocal = setup_database_for_all_tests
    _patch_user_db(monkeypatch, SessionLocal)

    with SessionLocal() as db:
        sid, rid = _seed(db)

    service = ChatService(username="alice")
    for _ in range(3):
        service.add_progress_step(
            session_id=sid, research_id=rid, content="step"
        )

    with SessionLocal() as db:
        rows = (
            db.query(ChatProgressStep)
            .filter_by(research_id=rid)
            .order_by(ChatProgressStep.sequence_number)
            .all()
        )
        assert [r.sequence_number for r in rows] == [1, 2, 3]
        # And research_history.step_count must mirror the count.
        rh = db.query(ResearchHistory).filter_by(id=rid).first()
        assert rh.step_count == 3


def test_phase_is_optional(setup_database_for_all_tests, monkeypatch):
    """``phase`` defaults to None; the column stores NULL."""
    SessionLocal = setup_database_for_all_tests
    _patch_user_db(monkeypatch, SessionLocal)

    with SessionLocal() as db:
        sid, rid = _seed(db)

    service = ChatService(username="alice")
    step_id = service.add_progress_step(
        session_id=sid, research_id=rid, content="step without phase"
    )

    with SessionLocal() as db:
        row = db.query(ChatProgressStep).filter_by(id=step_id).first()
        assert row.phase is None
