"""Тесты для SessionRehydrator (секция 8.4 архитектуры).

Контракт: build_rehydration_payload(ctx) -> Mapping с ключами:
  role_id, active_skill_ids, summary, last_messages, goal, phase_state

ISP: конструктор принимает 5 мелких протоколов, а не монолитный MemoryProvider.
"""

from unittest.mock import AsyncMock

import pytest

from cognitia.memory.types import GoalState, MemoryMessage, PhaseState
from cognitia.session.rehydrator import DefaultSessionRehydrator
from cognitia.types import TurnContext


def _make_ctx(**kwargs) -> TurnContext:
    defaults = {
        "user_id": "u1", "topic_id": "t1", "role_id": "coach",
        "model": "sonnet", "active_skill_ids": (),
    }
    defaults.update(kwargs)
    return TurnContext(**defaults)


def _make_stores() -> dict:
    """Создать набор мок-хранилищ для rehydrator."""
    messages = AsyncMock()
    messages.get_messages.return_value = [
        MemoryMessage(role="user", content="какой вклад лучше?"),
        MemoryMessage(role="assistant", content="Зависит от суммы и срока."),
    ]

    summaries = AsyncMock()
    summaries.get_summary.return_value = "Пользователь интересуется вкладами."

    goals = AsyncMock()
    goals.get_active_goal.return_value = GoalState(
        goal_id="g1", title="Накопить 500к", target_amount=500000,
        current_amount=100000, phase="savings",
    )

    sessions = AsyncMock()
    sessions.get_session_state.return_value = {
        "role_id": "deposit_advisor",
        "active_skill_ids": ["finuslugi", "iss-price"],
        "title": "Вклады",
        "prompt_hash": "abc123def456",
    }

    phases = AsyncMock()
    phases.get_phase_state.return_value = PhaseState(
        user_id="u1", phase="savings", notes="50% от цели"
    )

    return {
        "messages": messages,
        "summaries": summaries,
        "goals": goals,
        "sessions": sessions,
        "phases": phases,
    }


@pytest.fixture
def stores() -> dict:
    return _make_stores()


@pytest.fixture
def rehydrator(stores: dict) -> DefaultSessionRehydrator:
    return DefaultSessionRehydrator(**stores)


class TestBuildPayload:
    """build_rehydration_payload() возвращает полный payload."""

    @pytest.mark.asyncio
    async def test_returns_role_id(self, rehydrator: DefaultSessionRehydrator) -> None:
        """Payload содержит role_id из БД."""
        payload = await rehydrator.build_rehydration_payload(_make_ctx())
        assert payload["role_id"] == "deposit_advisor"

    @pytest.mark.asyncio
    async def test_returns_active_skills(self, rehydrator: DefaultSessionRehydrator) -> None:
        """Payload содержит active_skill_ids."""
        payload = await rehydrator.build_rehydration_payload(_make_ctx())
        assert payload["active_skill_ids"] == ["finuslugi", "iss-price"]

    @pytest.mark.asyncio
    async def test_returns_summary(self, rehydrator: DefaultSessionRehydrator) -> None:
        """Payload содержит rolling summary."""
        payload = await rehydrator.build_rehydration_payload(_make_ctx())
        assert "вкладами" in payload["summary"]

    @pytest.mark.asyncio
    async def test_returns_last_messages(self, rehydrator: DefaultSessionRehydrator) -> None:
        """Payload содержит last N messages."""
        payload = await rehydrator.build_rehydration_payload(_make_ctx())
        assert len(payload["last_messages"]) == 2
        assert payload["last_messages"][0].role == "user"

    @pytest.mark.asyncio
    async def test_returns_active_goal(self, rehydrator: DefaultSessionRehydrator) -> None:
        """Payload содержит активную цель."""
        payload = await rehydrator.build_rehydration_payload(_make_ctx())
        assert payload["goal"].title == "Накопить 500к"
        assert payload["goal"].phase == "savings"

    @pytest.mark.asyncio
    async def test_returns_phase_state(self, rehydrator: DefaultSessionRehydrator) -> None:
        """Payload содержит phase_state (R-703)."""
        payload = await rehydrator.build_rehydration_payload(_make_ctx())
        assert payload["phase_state"] is not None
        assert payload["phase_state"].phase == "savings"
        assert payload["phase_state"].notes == "50% от цели"

    @pytest.mark.asyncio
    async def test_returns_prompt_hash(self, rehydrator: DefaultSessionRehydrator) -> None:
        """Payload содержит prompt_hash для diagnostics (GAP-2, §8.4)."""
        payload = await rehydrator.build_rehydration_payload(_make_ctx())
        assert payload["prompt_hash"] == "abc123def456"


class TestMissingData:
    """Payload при отсутствии данных в БД."""

    @pytest.mark.asyncio
    async def test_no_session_state(self) -> None:
        """Если нет session_state — используем данные из ctx."""
        stores = _make_stores()
        stores["sessions"].get_session_state.return_value = None
        rh = DefaultSessionRehydrator(**stores)
        payload = await rh.build_rehydration_payload(_make_ctx(role_id="coach"))
        assert payload["role_id"] == "coach"
        assert payload["active_skill_ids"] == []
        assert payload["prompt_hash"] == ""

    @pytest.mark.asyncio
    async def test_no_summary(self) -> None:
        """Если нет summary — None."""
        stores = _make_stores()
        stores["summaries"].get_summary.return_value = None
        rh = DefaultSessionRehydrator(**stores)
        payload = await rh.build_rehydration_payload(_make_ctx())
        assert payload["summary"] is None

    @pytest.mark.asyncio
    async def test_no_goal(self) -> None:
        """Если нет цели — None."""
        stores = _make_stores()
        stores["goals"].get_active_goal.return_value = None
        rh = DefaultSessionRehydrator(**stores)
        payload = await rh.build_rehydration_payload(_make_ctx())
        assert payload["goal"] is None

    @pytest.mark.asyncio
    async def test_no_messages(self) -> None:
        """Если нет сообщений — пустой список."""
        stores = _make_stores()
        stores["messages"].get_messages.return_value = []
        rh = DefaultSessionRehydrator(**stores)
        payload = await rh.build_rehydration_payload(_make_ctx())
        assert payload["last_messages"] == []

    @pytest.mark.asyncio
    async def test_no_phase_state(self) -> None:
        """Если нет phase_state — None."""
        stores = _make_stores()
        stores["phases"].get_phase_state.return_value = None
        rh = DefaultSessionRehydrator(**stores)
        payload = await rh.build_rehydration_payload(_make_ctx())
        assert payload["phase_state"] is None


class TestPartialData:
    """Edge cases: частичные данные при rehydration."""

    @pytest.mark.asyncio
    async def test_session_state_without_skills_key(self) -> None:
        """Session state без active_skill_ids → пустой список."""
        stores = _make_stores()
        stores["sessions"].get_session_state.return_value = {
            "role_id": "coach",
            # нет active_skill_ids
        }
        rh = DefaultSessionRehydrator(**stores)
        payload = await rh.build_rehydration_payload(_make_ctx())
        assert payload["role_id"] == "coach"
        assert payload["active_skill_ids"] == []

    @pytest.mark.asyncio
    async def test_all_empty_returns_ctx_defaults(self) -> None:
        """Все хранилища пустые → используем данные из ctx."""
        stores = _make_stores()
        stores["sessions"].get_session_state.return_value = None
        stores["summaries"].get_summary.return_value = None
        stores["messages"].get_messages.return_value = []
        stores["goals"].get_active_goal.return_value = None
        stores["phases"].get_phase_state.return_value = None

        rh = DefaultSessionRehydrator(**stores)
        ctx = _make_ctx(role_id="diagnostician", active_skill_ids=("iss",))
        payload = await rh.build_rehydration_payload(ctx)

        assert payload["role_id"] == "diagnostician"
        assert payload["active_skill_ids"] == ["iss"]
        assert payload["summary"] is None
        assert payload["last_messages"] == []
        assert payload["goal"] is None
        assert payload["phase_state"] is None

    @pytest.mark.asyncio
    async def test_last_n_messages_limit(self) -> None:
        """Rehydrator запрашивает ровно last_n messages."""
        stores = _make_stores()
        rh = DefaultSessionRehydrator(**stores, last_n_messages=3)
        await rh.build_rehydration_payload(_make_ctx())
        stores["messages"].get_messages.assert_called_once_with("u1", "t1", limit=3)

    @pytest.mark.asyncio
    async def test_phase_state_full_payload(self) -> None:
        """Phase state с полными данными включается в payload."""
        stores = _make_stores()
        rh = DefaultSessionRehydrator(**stores)
        payload = await rh.build_rehydration_payload(_make_ctx())
        phase = payload["phase_state"]
        assert phase.user_id == "u1"
        assert phase.phase == "savings"
        assert phase.notes == "50% от цели"
