"""Unit tests for ``_chat_step_decision`` in research_service.

This helper encodes the live-emit/reload symmetry invariant:

> For chat sessions, what the live socket emits MUST equal what
> ``loadSession`` reconstructs from chat_progress_steps on reload.

The progress_callback inside ``run_research_process`` consults this
helper for every callback event and uses its (persist, suppress_emit)
return to gate both the DB write and the socket emit.

The tests below pin down the contract per scenario.
"""

import pytest

from src.local_deep_research.web.services.research_service import (
    _chat_step_decision,
    _STEP_PHASES,
)


class TestNonStepPhases:
    """Phases not in _STEP_PHASES (e.g. 'complete') never persist
    but never block the emit either — the completion handler needs them."""

    def test_complete_phase_no_persist_no_suppress(self):
        # "complete" is intentionally excluded from _STEP_PHASES so it
        # doesn't get a row ordered after the response message.
        assert "complete" not in _STEP_PHASES
        persist, suppress = _chat_step_decision(
            phase="complete", last_step_phase=None, is_final=True
        )
        assert persist is False
        assert suppress is False

    def test_unknown_phase_no_persist_no_suppress(self):
        persist, suppress = _chat_step_decision(
            phase="some-future-phase",
            last_step_phase=None,
            is_final=False,
        )
        assert persist is False
        assert suppress is False

    def test_none_phase_no_persist_no_suppress(self):
        persist, suppress = _chat_step_decision(
            phase=None, last_step_phase=None, is_final=False
        )
        assert persist is False
        assert suppress is False


class TestFirstStepEver:
    """The first step of a research always persists; no prior phase."""

    @pytest.mark.parametrize("phase", sorted(_STEP_PHASES))
    def test_first_step_persists_and_emits(self, phase):
        persist, suppress = _chat_step_decision(
            phase=phase, last_step_phase=None, is_final=False
        )
        assert persist is True, f"phase={phase} should persist when no prior"
        assert suppress is False, (
            f"phase={phase} should also emit on first occurrence"
        )


class TestPhaseTransition:
    """Different phase from the last persisted one: persist + emit."""

    def test_search_after_init_persists(self):
        persist, suppress = _chat_step_decision(
            phase="search", last_step_phase="init", is_final=False
        )
        assert (persist, suppress) == (True, False)

    def test_report_generation_after_search_persists(self):
        persist, suppress = _chat_step_decision(
            phase="report_generation",
            last_step_phase="search",
            is_final=False,
        )
        assert (persist, suppress) == (True, False)


class TestRepeatPhaseDedup:
    """Same phase as last persisted: dedup blocks persistence, AND when
    the event is non-final the emit is dropped to maintain symmetry.
    'observation' is whitelisted to repeat.
    """

    def test_repeat_search_non_final_blocks_both(self):
        # The symmetry guarantee: if we wouldn't persist this step, we
        # don't emit it either — reload would not surface it.
        persist, suppress = _chat_step_decision(
            phase="search", last_step_phase="search", is_final=False
        )
        assert persist is False
        assert suppress is True

    def test_repeat_report_generation_non_final_blocks_both(self):
        persist, suppress = _chat_step_decision(
            phase="report_generation",
            last_step_phase="report_generation",
            is_final=False,
        )
        assert (persist, suppress) == (False, True)

    def test_observation_can_repeat(self):
        """'observation' is the documented exception to the dedup —
        it represents distinct in-iteration findings and must always
        persist + emit."""
        persist, suppress = _chat_step_decision(
            phase="observation",
            last_step_phase="observation",
            is_final=False,
        )
        assert persist is True
        assert suppress is False

    def test_repeat_phase_but_is_final_still_emits(self):
        """Final phases (error, report_complete) emit even on repeat so
        the client completion handler fires. Persist is still blocked by
        the dedup so we don't write a duplicate row."""
        persist, suppress = _chat_step_decision(
            phase="error", last_step_phase="error", is_final=True
        )
        assert persist is False
        # The crucial part: do NOT suppress the emit when is_final, even
        # though the DB write was deduped.
        assert suppress is False

    def test_repeat_report_complete_is_final_still_emits(self):
        persist, suppress = _chat_step_decision(
            phase="report_complete",
            last_step_phase="report_complete",
            is_final=True,
        )
        assert (persist, suppress) == (False, False)


class TestSymmetryInvariant:
    """Property-style assertion of the invariant: for every event in
    _STEP_PHASES, if we persist we also emit; if we don't persist and
    it's not final, we must suppress the emit so reload matches live.
    """

    @pytest.mark.parametrize(
        "phase",
        sorted(_STEP_PHASES),
    )
    @pytest.mark.parametrize("last_phase", sorted(_STEP_PHASES) + [None])
    @pytest.mark.parametrize("is_final", [True, False])
    def test_invariant_holds(self, phase, last_phase, is_final):
        persist, suppress = _chat_step_decision(
            phase=phase, last_step_phase=last_phase, is_final=is_final
        )
        if persist:
            # If we persist, we must also emit so the live UI surfaces it.
            assert suppress is False, (
                f"persist=True + suppress=True breaks symmetry: "
                f"phase={phase}, last={last_phase}, is_final={is_final}"
            )
        elif not is_final:
            # Non-final and no persist => emit must be suppressed so
            # live UI doesn't surface an event that reload will miss.
            assert suppress is True, (
                f"non-final + not-persist + not-suppress breaks "
                f"symmetry: phase={phase}, last={last_phase}"
            )
        else:
            # Final + not-persist: emit is allowed through so the
            # client completion handler fires.
            assert suppress is False
