"""Tests for PRD-CORE-084 FR04-FR07: Ceremony mode adaptation.

Covers:
- FR04: ceremony_mode config field and AGENTS.md rendering selection
- FR05: Recall max results capping for light mode
- FR06: Optional learning injection into AGENTS.md
- FR07: Context-aware session_start response
"""

from __future__ import annotations

from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from trw_mcp.models.config import TRWConfig
from trw_mcp.models.config._defaults import LIGHT_MODE_RECALL_CAP

# ---------------------------------------------------------------------------
# FR04: ceremony_mode config field
# ---------------------------------------------------------------------------


class TestCeremonyModeConfig:
    """FR04: ceremony_mode config field defaults to 'full' and accepts 'light'."""

    def test_default_ceremony_mode_is_full(self) -> None:
        config = TRWConfig()
        assert config.ceremony_mode == "full"

    def test_ceremony_mode_light_accepted(self) -> None:
        config = TRWConfig(ceremony_mode="light")
        assert config.ceremony_mode == "light"

    def test_ceremony_mode_full_accepted(self) -> None:
        config = TRWConfig(ceremony_mode="full")
        assert config.ceremony_mode == "full"

    def test_light_mode_recall_cap_is_10(self) -> None:
        assert LIGHT_MODE_RECALL_CAP == 10


# ---------------------------------------------------------------------------
# FR04: AGENTS.md rendering uses render_minimal_protocol() in light mode
# ---------------------------------------------------------------------------


def _run_agents_md_sync(
    tmp_path: Path,
    ceremony_mode: str = "full",
    agents_md_learning_injection: bool = False,
    agents_md_learning_max: int = 5,
    agents_md_learning_min_impact: float = 0.7,
    mock_learnings: list[dict[str, object]] | None = None,
) -> dict[str, object]:
    """Run execute_claude_md_sync with mocked infra targeting AGENTS.md."""
    from trw_mcp.state.claude_md._sync import execute_claude_md_sync
    from trw_mcp.state.persistence import FileStateReader

    trw_dir = tmp_path / ".trw"
    trw_dir.mkdir(parents=True, exist_ok=True)
    (trw_dir / "learnings" / "entries").mkdir(parents=True, exist_ok=True)
    (trw_dir / "reflections").mkdir(exist_ok=True)
    (trw_dir / "context").mkdir(exist_ok=True)
    (trw_dir / "patterns").mkdir(exist_ok=True)

    config = TRWConfig(
        trw_dir=str(trw_dir),
        ceremony_mode=ceremony_mode,
        agents_md_learning_injection=agents_md_learning_injection,
        agents_md_learning_max=agents_md_learning_max,
        agents_md_learning_min_impact=agents_md_learning_min_impact,
    )
    reader = FileStateReader()
    llm = MagicMock()
    llm.available = False

    recall_return = mock_learnings if mock_learnings is not None else []

    with (
        patch("trw_mcp.state.claude_md._sync.collect_promotable_learnings", return_value=[]),
        patch("trw_mcp.state.claude_md._sync.collect_patterns", return_value=[]),
        patch("trw_mcp.state.claude_md._sync.collect_context_data", return_value=({}, {})),
        patch("trw_mcp.state.claude_md.resolve_trw_dir", return_value=trw_dir),
        patch("trw_mcp.state.claude_md.resolve_project_root", return_value=tmp_path),
        patch("trw_mcp.state.analytics.update_analytics_sync"),
        patch("trw_mcp.state.analytics.mark_promoted"),
        patch("trw_mcp.state.claude_md._sync.recall_learnings", return_value=recall_return),
    ):
        return execute_claude_md_sync(
            scope="root",
            target_dir=None,
            config=config,
            reader=reader,
            llm=llm,
            client="opencode",
        )


class TestAgentsMdCeremonyModeRendering:
    """FR04: ceremony_mode controls AGENTS.md rendering path."""

    def test_full_mode_renders_full_agents_section(self, tmp_path: Path) -> None:
        """ceremony_mode=full uses render_agents_trw_section() for AGENTS.md."""
        _run_agents_md_sync(tmp_path, ceremony_mode="full")

        agents_md = tmp_path / "AGENTS.md"
        assert agents_md.exists()
        content = agents_md.read_text(encoding="utf-8")
        # Full mode includes the workflow section
        assert "## Workflow" in content
        assert "## TRW Tools" in content

    def test_light_mode_renders_minimal_protocol(self, tmp_path: Path) -> None:
        """ceremony_mode=light uses render_minimal_protocol() for AGENTS.md."""
        _run_agents_md_sync(tmp_path, ceremony_mode="light")

        agents_md = tmp_path / "AGENTS.md"
        assert agents_md.exists()
        content = agents_md.read_text(encoding="utf-8")
        # Minimal protocol has only start/finish instructions
        assert "trw_session_start()" in content
        assert "trw_deliver()" in content
        # Full mode sections should NOT be present
        assert "## Workflow" not in content
        assert "## TRW Tools" not in content

    def test_light_mode_agents_md_is_compact(self, tmp_path: Path) -> None:
        """ceremony_mode=light produces compact AGENTS.md (fewer lines than full)."""
        # Use separate subdirs to avoid hash-cache interactions
        full_dir = tmp_path / "full_project"
        full_dir.mkdir()
        light_dir = tmp_path / "light_project"
        light_dir.mkdir()

        _run_agents_md_sync(full_dir, ceremony_mode="full")
        full_content = (full_dir / "AGENTS.md").read_text(encoding="utf-8")

        _run_agents_md_sync(light_dir, ceremony_mode="light")
        light_content = (light_dir / "AGENTS.md").read_text(encoding="utf-8")

        assert len(light_content) < len(full_content)


# ---------------------------------------------------------------------------
# FR05: Recall max results capping for light mode
# ---------------------------------------------------------------------------


class TestRecallCappingLightMode:
    """FR05: Light mode caps recall results to LIGHT_MODE_RECALL_CAP."""

    def test_light_mode_caps_recall_results(self, tmp_path: Path) -> None:
        """With ceremony_mode=light and 25 learnings, at most 10 are returned."""
        from trw_mcp.state.persistence import FileStateReader
        from trw_mcp.tools._ceremony_helpers import perform_session_recalls

        trw_dir = tmp_path / ".trw"
        trw_dir.mkdir(parents=True, exist_ok=True)

        config = TRWConfig(
            trw_dir=str(trw_dir),
            ceremony_mode="light",
            recall_max_results=25,
        )

        # Create 25 fake learnings
        all_learnings = [{"id": f"L-{i:04d}", "summary": f"Learning {i}", "impact": 0.8} for i in range(25)]
        reader = FileStateReader()

        def mock_recall(
            trw_dir_arg: Path,
            query: str = "*",
            *,
            tags: list[str] | None = None,
            min_impact: float = 0.0,
            max_results: int = 25,
            compact: bool = False,
            status: str | None = None,
        ) -> list[dict[str, object]]:
            """Return learnings capped to max_results."""
            return all_learnings[:max_results]

        with (
            patch("trw_mcp.state.memory_adapter.recall_learnings", side_effect=mock_recall),
            patch("trw_mcp.state.memory_adapter.update_access_tracking"),
            patch("trw_mcp.tools._session_recall_helpers.log_recall_receipt"),
        ):
            learnings, _auto, _extra = perform_session_recalls(
                trw_dir,
                "",
                config,
                reader,
            )

        assert len(learnings) <= LIGHT_MODE_RECALL_CAP

    def test_full_mode_uses_configured_recall_max(self, tmp_path: Path) -> None:
        """With ceremony_mode=full, recall_max_results is used as-is."""
        from trw_mcp.state.persistence import FileStateReader
        from trw_mcp.tools._ceremony_helpers import perform_session_recalls

        trw_dir = tmp_path / ".trw"
        trw_dir.mkdir(parents=True, exist_ok=True)

        config = TRWConfig(
            trw_dir=str(trw_dir),
            ceremony_mode="full",
            recall_max_results=25,
        )

        all_learnings = [{"id": f"L-{i:04d}", "summary": f"Learning {i}", "impact": 0.8} for i in range(25)]
        reader = FileStateReader()

        def mock_recall(
            trw_dir_arg: Path,
            query: str = "*",
            *,
            tags: list[str] | None = None,
            min_impact: float = 0.0,
            max_results: int = 25,
            compact: bool = False,
            status: str | None = None,
        ) -> list[dict[str, object]]:
            """Return learnings capped to max_results."""
            return all_learnings[:max_results]

        with (
            patch("trw_mcp.state.memory_adapter.recall_learnings", side_effect=mock_recall),
            patch("trw_mcp.state.memory_adapter.update_access_tracking"),
            patch("trw_mcp.tools._session_recall_helpers.log_recall_receipt"),
        ):
            learnings, _auto, _extra = perform_session_recalls(
                trw_dir,
                "",
                config,
                reader,
            )

        # Full mode should return all 25
        assert len(learnings) == 25

    def test_light_mode_effective_max_calculation(self) -> None:
        """Verify effective_max = min(config.recall_max_results, LIGHT_MODE_RECALL_CAP)."""
        config = TRWConfig(ceremony_mode="light", recall_max_results=25)
        effective_max = min(config.recall_max_results, LIGHT_MODE_RECALL_CAP)
        assert effective_max == LIGHT_MODE_RECALL_CAP

    def test_light_mode_respects_lower_configured_max(self) -> None:
        """If recall_max_results < LIGHT_MODE_RECALL_CAP, the lower value is used."""
        config = TRWConfig(ceremony_mode="light", recall_max_results=5)
        effective_max = min(config.recall_max_results, LIGHT_MODE_RECALL_CAP)
        assert effective_max == 5

    def test_light_mode_focused_recall_also_capped(self, tmp_path: Path) -> None:
        """Focused (non-empty query) recall is also capped in light mode."""
        from trw_mcp.state.persistence import FileStateReader
        from trw_mcp.tools._ceremony_helpers import perform_session_recalls

        trw_dir = tmp_path / ".trw"
        trw_dir.mkdir(parents=True, exist_ok=True)

        config = TRWConfig(
            trw_dir=str(trw_dir),
            ceremony_mode="light",
            recall_max_results=25,
        )

        all_learnings = [{"id": f"L-{i:04d}", "summary": f"Learning {i}", "impact": 0.8} for i in range(25)]
        reader = FileStateReader()

        captured_max_results: list[int] = []

        def mock_recall(
            trw_dir_arg: Path,
            query: str = "*",
            *,
            tags: list[str] | None = None,
            min_impact: float = 0.0,
            max_results: int = 25,
            compact: bool = False,
            status: str | None = None,
        ) -> list[dict[str, object]]:
            captured_max_results.append(max_results)
            return all_learnings[:max_results]

        with (
            patch("trw_mcp.state.memory_adapter.recall_learnings", side_effect=mock_recall),
            patch("trw_mcp.state.memory_adapter.update_access_tracking"),
            patch("trw_mcp.tools._session_recall_helpers.log_recall_receipt"),
        ):
            learnings, _auto, _extra = perform_session_recalls(
                trw_dir,
                "testing query",
                config,
                reader,
            )

        # Both focused and baseline recalls should use capped max
        assert all(mr <= LIGHT_MODE_RECALL_CAP for mr in captured_max_results)
        assert len(learnings) <= LIGHT_MODE_RECALL_CAP


# ---------------------------------------------------------------------------
# FR06: Optional learning injection into AGENTS.md
# ---------------------------------------------------------------------------


class TestAgentsMdLearningInjection:
    """FR06: Inject high-impact learnings into AGENTS.md when enabled."""

    def test_injection_enabled_adds_key_learnings_section(self, tmp_path: Path) -> None:
        """When agents_md_learning_injection=True, AGENTS.md has Key Learnings section."""
        mock_learnings: list[dict[str, object]] = [
            {"id": "L-001", "summary": "Always validate input types", "impact": 0.9},
            {"id": "L-002", "summary": "Use structured logging for debugging", "impact": 0.8},
        ]
        _run_agents_md_sync(
            tmp_path,
            agents_md_learning_injection=True,
            mock_learnings=mock_learnings,
        )

        agents_md = tmp_path / "AGENTS.md"
        content = agents_md.read_text(encoding="utf-8")
        assert "## Key Learnings" in content
        assert "Always validate input types" in content
        assert "Use structured logging for debugging" in content

    def test_injection_disabled_no_key_learnings(self, tmp_path: Path) -> None:
        """When agents_md_learning_injection=False, AGENTS.md has no Key Learnings."""
        _run_agents_md_sync(tmp_path, agents_md_learning_injection=False)

        agents_md = tmp_path / "AGENTS.md"
        content = agents_md.read_text(encoding="utf-8")
        assert "## Key Learnings" not in content

    def test_injection_with_empty_learnings(self, tmp_path: Path) -> None:
        """When no learnings qualify, Key Learnings section is not added."""
        _run_agents_md_sync(
            tmp_path,
            agents_md_learning_injection=True,
            mock_learnings=[],
        )

        agents_md = tmp_path / "AGENTS.md"
        content = agents_md.read_text(encoding="utf-8")
        assert "## Key Learnings" not in content

    def test_injection_sanitizes_summaries(self, tmp_path: Path) -> None:
        """Learning summaries are sanitized (no markdown links, HTML, URLs)."""
        learnings_with_links: list[dict[str, object]] = [
            {
                "id": "L-010",
                "summary": "Check [docs](https://example.com) for details",
                "impact": 0.9,
            },
        ]
        _run_agents_md_sync(
            tmp_path,
            agents_md_learning_injection=True,
            mock_learnings=learnings_with_links,
        )

        agents_md = tmp_path / "AGENTS.md"
        content = agents_md.read_text(encoding="utf-8")
        assert "## Key Learnings" in content
        assert "https://example.com" not in content
        assert "Check docs for details" in content

    def test_injection_works_with_light_mode(self, tmp_path: Path) -> None:
        """Learning injection works with light ceremony mode."""
        mock_learnings: list[dict[str, object]] = [
            {"id": "L-001", "summary": "Test learning", "impact": 0.9},
        ]
        _run_agents_md_sync(
            tmp_path,
            ceremony_mode="light",
            agents_md_learning_injection=True,
            mock_learnings=mock_learnings,
        )

        agents_md = tmp_path / "AGENTS.md"
        content = agents_md.read_text(encoding="utf-8")
        assert "## Key Learnings" in content
        # Should use minimal protocol base
        assert "## Workflow" not in content

    def test_injection_fail_open(self, tmp_path: Path) -> None:
        """If learning query fails, AGENTS.md renders without learnings (fail-open)."""
        from trw_mcp.state.claude_md._sync import execute_claude_md_sync
        from trw_mcp.state.persistence import FileStateReader

        trw_dir = tmp_path / ".trw"
        trw_dir.mkdir(parents=True, exist_ok=True)
        (trw_dir / "learnings" / "entries").mkdir(parents=True, exist_ok=True)
        (trw_dir / "reflections").mkdir(exist_ok=True)
        (trw_dir / "context").mkdir(exist_ok=True)
        (trw_dir / "patterns").mkdir(exist_ok=True)

        config = TRWConfig(
            trw_dir=str(trw_dir),
            agents_md_learning_injection=True,
        )
        reader = FileStateReader()
        llm = MagicMock()
        llm.available = False

        with (
            patch("trw_mcp.state.claude_md._sync.collect_promotable_learnings", return_value=[]),
            patch("trw_mcp.state.claude_md._sync.collect_patterns", return_value=[]),
            patch("trw_mcp.state.claude_md._sync.collect_context_data", return_value=({}, {})),
            patch("trw_mcp.state.claude_md.resolve_trw_dir", return_value=trw_dir),
            patch("trw_mcp.state.claude_md.resolve_project_root", return_value=tmp_path),
            patch("trw_mcp.state.analytics.update_analytics_sync"),
            patch("trw_mcp.state.analytics.mark_promoted"),
            patch(
                "trw_mcp.state.claude_md._sync.recall_learnings",
                side_effect=RuntimeError("DB error"),
            ),
        ):
            result = execute_claude_md_sync(
                scope="root",
                target_dir=None,
                config=config,
                reader=reader,
                llm=llm,
                client="opencode",
            )

        # Should still succeed -- fail-open
        assert result["agents_md_synced"] is True
        agents_md = tmp_path / "AGENTS.md"
        content = agents_md.read_text(encoding="utf-8")
        # No Key Learnings section due to error
        assert "## Key Learnings" not in content

    def test_config_defaults_for_learning_injection(self) -> None:
        """Default config values for AGENTS.md learning injection fields."""
        config = TRWConfig()
        assert config.agents_md_learning_injection is True
        assert config.agents_md_learning_max == 5
        assert config.agents_md_learning_min_impact == 0.7


# ---------------------------------------------------------------------------
# FR07: Context-aware session_start response
# ---------------------------------------------------------------------------


class TestSessionStartLightMode:
    """FR07: session_start uses compact response for light mode."""

    def _invoke_session_start(
        self,
        monkeypatch: pytest.MonkeyPatch,
        tmp_path: Path,
        ceremony_mode: str,
    ) -> dict[str, object]:
        """Invoke trw_session_start via the MCP tool path with mocked infra."""
        from tests.conftest import get_tools_sync, make_test_server

        trw_dir = tmp_path / ".trw"
        trw_dir.mkdir(parents=True, exist_ok=True)
        (trw_dir / "learnings" / "entries").mkdir(parents=True, exist_ok=True)
        (trw_dir / "context").mkdir(exist_ok=True)

        cfg = TRWConfig(trw_dir=str(trw_dir))
        object.__setattr__(cfg, "ceremony_mode", ceremony_mode)

        monkeypatch.setenv("TRW_PROJECT_ROOT", str(tmp_path))
        server = make_test_server("ceremony")
        tools = get_tools_sync(server)

        with (
            patch("trw_mcp.tools.ceremony.get_config", return_value=cfg),
            patch("trw_mcp.models.config.get_config", return_value=cfg),
            patch("trw_mcp.tools._ceremony_helpers.get_config", create=True, return_value=cfg),
            patch("trw_mcp.tools.ceremony.resolve_trw_dir", return_value=trw_dir),
            patch("trw_mcp.tools.ceremony.find_active_run", return_value=None),
            patch("trw_mcp.state.memory_adapter.recall_learnings", return_value=[]),
            patch("trw_mcp.state.memory_adapter.update_access_tracking"),
            patch("trw_mcp.tools._session_recall_helpers.log_recall_receipt"),
        ):
            result: dict[str, object] = tools["trw_session_start"].fn(query="")
        return result

    def test_light_mode_framework_reminder_content(
        self,
        monkeypatch: pytest.MonkeyPatch,
        tmp_path: Path,
    ) -> None:
        """Light mode framework_reminder says 'Call trw_deliver() when done'."""
        result = self._invoke_session_start(monkeypatch, tmp_path, "light")
        reminder = str(result.get("framework_reminder", ""))
        assert "trw_deliver()" in reminder
        assert "FRAMEWORK.md" not in reminder

    def test_full_mode_framework_reminder_mentions_framework(
        self,
        monkeypatch: pytest.MonkeyPatch,
        tmp_path: Path,
    ) -> None:
        """Full mode framework_reminder references FRAMEWORK.md."""
        result = self._invoke_session_start(monkeypatch, tmp_path, "full")
        reminder = str(result.get("framework_reminder", ""))
        assert "FRAMEWORK.md" in reminder

    def test_light_mode_skips_ceremony_nudge(
        self,
        monkeypatch: pytest.MonkeyPatch,
        tmp_path: Path,
    ) -> None:
        """In light mode, ceremony_status nudge is not injected into session_start."""
        result = self._invoke_session_start(monkeypatch, tmp_path, "light")
        assert "ceremony_status" not in result

    def test_full_mode_includes_ceremony_nudge(
        self,
        monkeypatch: pytest.MonkeyPatch,
        tmp_path: Path,
    ) -> None:
        """In full mode, ceremony_status nudge IS injected."""
        result = self._invoke_session_start(monkeypatch, tmp_path, "full")
        # Full mode attempts to inject ceremony_status (it may or may not succeed
        # depending on ceremony state file, but the code path is exercised).
        # The key distinction is that light mode definitively skips it.
        # We verify full mode by checking it either has ceremony_status or
        # at least has the FRAMEWORK.md reference in the reminder.
        reminder = str(result.get("framework_reminder", ""))
        assert "FRAMEWORK.md" in reminder
