"""Final miscellaneous coverage tests targeting specific uncovered lines.

Covers:
- bootstrap.py: lines 272, 305, 315, 330, 340, 378-379
- clients/llm.py: lines 205-209
- models/run.py: lines 52-55, 262-265
- state/index_sync.py: lines 79-80, 107-109, 196
- state/recall_tracking.py: lines 66-68
- telemetry/publisher.py: lines 49, 76
- state/auto_upgrade.py: lines 29-30
- prompts/aaref.py: line 28
- prompts/messaging.py: lines 60, 98
- state/_paths.py: lines 58, 172
"""

from __future__ import annotations

import shutil
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock, patch

import pytest

# ---------------------------------------------------------------------------
# Module-level path checks for @pytest.mark.skipif decorators
# ---------------------------------------------------------------------------

try:
    from trw_mcp.bootstrap._utils import _DATA_DIR as _BS_DATA_DIR
except ImportError:
    _BS_DATA_DIR = Path("/nonexistent")

_HOOKS_DIR = _BS_DATA_DIR / "hooks"
_SKILLS_DIR = _BS_DATA_DIR / "skills"
_AGENTS_DIR = _BS_DATA_DIR / "agents"

_HAS_HOOKS_DIR = _HOOKS_DIR.is_dir()
_HOOK_FILES = list(_HOOKS_DIR.glob("*.sh")) if _HAS_HOOKS_DIR else []
_HAS_HOOK_FILES = len(_HOOK_FILES) > 0

_HAS_SKILLS_DIR = _SKILLS_DIR.is_dir()
_SKILL_DIRS = [d for d in _SKILLS_DIR.iterdir() if d.is_dir()] if _HAS_SKILLS_DIR else []
_HAS_SKILL_DIRS = len(_SKILL_DIRS) > 0
_FIRST_SKILL_DIR = _SKILL_DIRS[0] if _HAS_SKILL_DIRS else None
_SKILL_DIR_FILES = [f for f in _FIRST_SKILL_DIR.iterdir() if f.is_file()] if _FIRST_SKILL_DIR is not None else []
_HAS_SKILL_FILES = len(_SKILL_DIR_FILES) > 0

_HAS_AGENTS_DIR = _AGENTS_DIR.is_dir()
_AGENT_FILES = list(_AGENTS_DIR.glob("*.md")) if _HAS_AGENTS_DIR else []
_HAS_AGENT_FILES = len(_AGENT_FILES) > 0

try:
    from trw_mcp.prompts.aaref import _DATA_DIR as _AAREF_DATA_DIR
except ImportError:
    _AAREF_DATA_DIR = Path("/nonexistent")

_AAREF_TEMPLATES = list(_AAREF_DATA_DIR.glob("*.md")) if _AAREF_DATA_DIR.is_dir() else []
_HAS_AAREF_TEMPLATES = len(_AAREF_TEMPLATES) > 0


# ---------------------------------------------------------------------------
# bootstrap.py coverage
# ---------------------------------------------------------------------------


class TestBootstrapDryRunBranches:
    """Cover dry_run branches in update_project that require specific file states."""

    def _make_trw_target(self, tmp_path: Path) -> Path:
        """Create a minimal target dir with .trw/ so update_project doesn't error."""
        target = tmp_path / "target"
        target.mkdir()
        (target / ".trw").mkdir()
        (target / ".claude" / "hooks").mkdir(parents=True)
        (target / ".claude" / "skills").mkdir(parents=True)
        (target / ".claude" / "agents").mkdir(parents=True)
        return target

    @pytest.mark.skipif(not _HAS_HOOKS_DIR, reason="No hooks in bundled data")
    @pytest.mark.skipif(not _HAS_HOOK_FILES, reason="No .sh files in hooks")
    def test_dry_run_hook_identical_file_skips_update(self, tmp_path: Path) -> None:
        """Line 272: dry_run with identical hook — no 'would update' added."""
        from trw_mcp import bootstrap as bs

        target = self._make_trw_target(tmp_path)
        hooks_source = bs._DATA_DIR / "hooks"
        hook_files = [f for f in hooks_source.iterdir() if f.suffix == ".sh"]

        # Copy a real hook to destination so files are identical
        hook_src = hook_files[0]
        dest_hook = target / ".claude" / "hooks" / hook_src.name
        shutil.copy2(hook_src, dest_hook)

        result = bs.update_project(target, dry_run=True)
        # Identical file: should NOT appear in 'would update' for this specific hook
        would_update_names = [s for s in result.get("updated", []) if hook_src.name in s and "would update" in s]
        assert len(would_update_names) == 0, (
            f"Identical file should not appear in dry_run updated list: {would_update_names}"
        )

    @pytest.mark.skipif(not _HAS_HOOKS_DIR, reason="No hooks in bundled data")
    @pytest.mark.skipif(not _HAS_HOOK_FILES, reason="No .sh files in hooks")
    def test_dry_run_hook_different_content_flags_would_update(self, tmp_path: Path) -> None:
        """Line 272: dry_run with modified hook — appends 'would update'."""
        from trw_mcp import bootstrap as bs

        target = self._make_trw_target(tmp_path)
        hooks_source = bs._DATA_DIR / "hooks"
        hook_files = [f for f in hooks_source.iterdir() if f.suffix == ".sh"]

        hook_src = hook_files[0]
        dest_hook = target / ".claude" / "hooks" / hook_src.name
        # Write DIFFERENT content so _files_identical returns False
        dest_hook.write_text("#!/bin/bash\necho 'old version'\n", encoding="utf-8")

        result = bs.update_project(target, dry_run=True)
        would_update = [s for s in result.get("updated", []) if "would update" in s]
        assert any(hook_src.name in s for s in would_update)

    @pytest.mark.skipif(not _HAS_SKILLS_DIR, reason="No skills in bundled data")
    @pytest.mark.skipif(not _HAS_SKILL_DIRS, reason="No skill directories")
    @pytest.mark.skipif(not _HAS_SKILL_FILES, reason="No files in skill dir")
    def test_dry_run_skill_file_identical_no_update(self, tmp_path: Path) -> None:
        """Line 305: dry_run skill file identical — not added to updated list."""
        from trw_mcp import bootstrap as bs

        target = self._make_trw_target(tmp_path)
        skills_source = bs._DATA_DIR / "skills"
        skill_dirs = [d for d in skills_source.iterdir() if d.is_dir()]
        skill_dir = skill_dirs[0]
        skill_files = [f for f in skill_dir.iterdir() if f.is_file()]
        skill_file = skill_files[0]
        dest_skill_dir = target / ".claude" / "skills" / skill_dir.name
        dest_skill_dir.mkdir(parents=True, exist_ok=True)
        dest_file = dest_skill_dir / skill_file.name
        # Copy identical content
        shutil.copy2(skill_file, dest_file)

        result = bs.update_project(target, dry_run=True)
        # No 'would update' for this specific file since it's identical
        would_update = [s for s in result.get("updated", []) if "would update" in s]
        assert not any(skill_file.name in s for s in would_update), (
            f"Identical skill file should not be flagged: {would_update}"
        )

    @pytest.mark.skipif(not _HAS_SKILLS_DIR, reason="No skills in bundled data")
    @pytest.mark.skipif(not _HAS_SKILL_DIRS, reason="No skill directories")
    @pytest.mark.skipif(not _HAS_SKILL_FILES, reason="No files in skill dir")
    def test_dry_run_skill_file_different_flags_would_update(self, tmp_path: Path) -> None:
        """Line 305 alt path: dry_run skill with different content."""
        from trw_mcp import bootstrap as bs

        target = self._make_trw_target(tmp_path)
        skills_source = bs._DATA_DIR / "skills"
        skill_dirs = [d for d in skills_source.iterdir() if d.is_dir()]
        skill_dir = skill_dirs[0]
        skill_files = [f for f in skill_dir.iterdir() if f.is_file()]
        skill_file = skill_files[0]
        dest_skill_dir = target / ".claude" / "skills" / skill_dir.name
        dest_skill_dir.mkdir(parents=True, exist_ok=True)
        dest_file = dest_skill_dir / skill_file.name
        # Write DIFFERENT content
        dest_file.write_text("# old content that differs", encoding="utf-8")

        result = bs.update_project(target, dry_run=True)
        would_update = [s for s in result.get("updated", []) if "would update" in s]
        assert any(skill_file.name in s for s in would_update)

    @pytest.mark.skipif(not _HAS_SKILLS_DIR, reason="No skills in bundled data")
    @pytest.mark.skipif(not _HAS_SKILL_DIRS, reason="No skill directories")
    @pytest.mark.skipif(not _HAS_SKILL_FILES, reason="No files in skill dir")
    def test_dry_run_new_skill_file_would_create(self, tmp_path: Path) -> None:
        """Line 315 (else branch): skill file doesn't exist → would create."""
        from trw_mcp import bootstrap as bs

        target = self._make_trw_target(tmp_path)

        # Do NOT create the destination skill dir/files - they don't exist
        result = bs.update_project(target, dry_run=True)
        would_create = result.get("created", [])
        assert any("would create" in s for s in would_create)

    @pytest.mark.skipif(not _HAS_AGENTS_DIR, reason="No agents in bundled data")
    @pytest.mark.skipif(not _HAS_AGENT_FILES, reason="No .md agents")
    def test_dry_run_agent_file_identical_not_flagged(self, tmp_path: Path) -> None:
        """Line 330: dry_run agent identical — no 'would update' added."""
        from trw_mcp import bootstrap as bs

        target = self._make_trw_target(tmp_path)
        agents_source = bs._DATA_DIR / "agents"
        agent_files = [f for f in agents_source.iterdir() if f.suffix == ".md"]
        agent_file = agent_files[0]
        dest_agent = target / ".claude" / "agents" / agent_file.name
        # Copy identical content
        shutil.copy2(agent_file, dest_agent)

        result = bs.update_project(target, dry_run=True)
        would_update = [s for s in result.get("updated", []) if "would update" in s]
        assert not any(agent_file.name in s for s in would_update)

    @pytest.mark.skipif(not _HAS_AGENTS_DIR, reason="No agents in bundled data")
    @pytest.mark.skipif(not _HAS_AGENT_FILES, reason="No .md agents")
    def test_dry_run_agent_different_content_flags_would_update(self, tmp_path: Path) -> None:
        """Line 330 alt path: agent file with different content."""
        from trw_mcp import bootstrap as bs

        target = self._make_trw_target(tmp_path)
        agents_source = bs._DATA_DIR / "agents"
        agent_files = [f for f in agents_source.iterdir() if f.suffix == ".md"]
        agent_file = agent_files[0]
        dest_agent = target / ".claude" / "agents" / agent_file.name
        dest_agent.write_text("# old agent content", encoding="utf-8")

        result = bs.update_project(target, dry_run=True)
        would_update = [s for s in result.get("updated", []) if "would update" in s]
        assert any(agent_file.name in s for s in would_update)

    @pytest.mark.skipif(not _HAS_AGENTS_DIR, reason="No agents in bundled data")
    @pytest.mark.skipif(not _HAS_AGENT_FILES, reason="No .md agents")
    def test_dry_run_new_agent_file_would_create(self, tmp_path: Path) -> None:
        """Line 340 (else branch): agent file doesn't exist → would create."""
        from trw_mcp import bootstrap as bs

        target = self._make_trw_target(tmp_path)

        # Don't create any agent files in target — all would be "created"
        result = bs.update_project(target, dry_run=True)
        would_create = result.get("created", [])
        assert any("would create" in s for s in would_create)

    def test_update_project_claude_md_write_failure(self, tmp_path: Path) -> None:
        """Lines 378-379: CLAUDE.md write fails → error appended."""
        from trw_mcp import bootstrap as bs

        target = self._make_trw_target(tmp_path)
        # CLAUDE.md does not exist — code tries to create it (line 376-379)
        # Patch write_text to raise OSError
        original_write_text = Path.write_text

        call_count = 0

        def patched_write_text(self: Path, content: str, encoding: str = "utf-8", **kw: Any) -> None:
            nonlocal call_count
            if self.name == "CLAUDE.md":
                call_count += 1
                raise OSError("permission denied")
            return original_write_text(self, content, encoding=encoding, **kw)

        with patch.object(Path, "write_text", patched_write_text):
            result = bs.update_project(target, dry_run=False)

        assert any("CLAUDE.md" in e for e in result["errors"])
        assert call_count >= 1


# ---------------------------------------------------------------------------
# clients/llm.py coverage (lines 205-209)
# ---------------------------------------------------------------------------


class TestLLMClientAskSync:
    """Cover the ThreadPoolExecutor branch when event loop is already running."""

    async def test_ask_sync_with_running_loop_uses_thread_pool(self) -> None:
        """Lines 205-209: ask_sync when a loop is running uses ThreadPoolExecutor."""
        from trw_mcp.clients.llm import LLMClient

        client = LLMClient()
        # When SDK is not installed, ask_sync returns None without hitting the branch
        if not client.available:
            # Manually test the thread pool path by mocking _available
            client._available = True

            async def mock_ask(*args: Any, **kwargs: Any) -> str | None:
                return "mocked response"

            with patch.object(client, "ask", mock_ask):
                # Since we're in an async test, a running loop exists
                # ask_sync will detect it and use ThreadPoolExecutor
                result = client.ask_sync("test prompt")
                assert result == "mocked response"
        else:
            # SDK is available — test directly
            with patch.object(client, "ask", return_value="mocked") as mock_ask:
                # We ARE in an async context (asyncio_mode=auto), so ask_sync
                # will hit the ThreadPoolExecutor branch
                result = client.ask_sync("test prompt")
                # ask_sync returns str | None
                assert result is None or isinstance(result, str)

    def test_ask_sync_without_running_loop(self) -> None:
        """Lines 200-203: ask_sync without a running loop uses asyncio.run."""
        from trw_mcp.clients.llm import LLMClient

        client = LLMClient()
        if not client.available:
            # Not available → returns None immediately without hitting branch
            result = client.ask_sync("test")
            assert result is None
            return

        # SDK available — patch ask to return quickly
        async def fast_ask(*args: Any, **kwargs: Any) -> str:
            return "sync result"

        with patch.object(client, "ask", fast_ask):
            # This test runs synchronously, so no event loop is running
            result = client.ask_sync("test prompt")
            assert result == "sync result"

    async def test_ask_sync_thread_pool_executes_coroutine(self) -> None:
        """Lines 205-209: directly test the concurrent.futures path."""
        from trw_mcp.clients.llm import LLMClient

        client = LLMClient()
        # Force _available=True regardless of SDK
        object.__setattr__(client, "_available", True)

        async def mock_ask(prompt: str, *, system: Any = None, model: Any = None, max_turns: Any = None) -> str:
            return "thread pool result"

        with patch.object(client, "ask", mock_ask):
            # We're inside an async test (running loop exists) → ThreadPoolExecutor path
            result = client.ask_sync("hello")
            assert result == "thread pool result"


# ---------------------------------------------------------------------------
# models/run.py coverage (lines 52-55, 262-265)
# ---------------------------------------------------------------------------


class TestReversionTriggerClassify:
    """Lines 52-55: ReversionTrigger.classify."""

    def test_classify_valid_value_returns_member(self) -> None:
        """Line 53: valid trigger string returns the enum member."""
        from trw_mcp.models.run import ReversionTrigger

        result = ReversionTrigger.classify("refactor_needed")
        assert result == ReversionTrigger.REFACTOR_NEEDED

    def test_classify_architecture_mismatch(self) -> None:
        from trw_mcp.models.run import ReversionTrigger

        result = ReversionTrigger.classify("architecture_mismatch")
        assert result == ReversionTrigger.ARCHITECTURE_MISMATCH

    def test_classify_unknown_returns_other(self) -> None:
        """Line 55: unrecognized string returns OTHER."""
        from trw_mcp.models.run import ReversionTrigger

        result = ReversionTrigger.classify("totally_unknown_trigger")
        assert result == ReversionTrigger.OTHER

    def test_classify_empty_string_returns_other(self) -> None:
        from trw_mcp.models.run import ReversionTrigger

        result = ReversionTrigger.classify("")
        assert result == ReversionTrigger.OTHER


class TestEventTypeResolve:
    """Lines 262-265: EventType.resolve."""

    def test_resolve_valid_event_returns_member(self) -> None:
        """Line 263: valid event string returns enum member."""
        from trw_mcp.models.run import EventType

        result = EventType.resolve("run_init")
        assert result == EventType.RUN_INIT

    def test_resolve_checkpoint_event(self) -> None:
        from trw_mcp.models.run import EventType

        result = EventType.resolve("checkpoint")
        assert result == EventType.CHECKPOINT

    def test_resolve_unknown_returns_none(self) -> None:
        """Line 265: unrecognized string returns None."""
        from trw_mcp.models.run import EventType

        result = EventType.resolve("totally_unknown_event_xyz")
        assert result is None

    def test_resolve_empty_string_returns_none(self) -> None:
        from trw_mcp.models.run import EventType

        result = EventType.resolve("")
        assert result is None


# ---------------------------------------------------------------------------
# state/index_sync.py coverage (lines 79-80, 107-109, 196)
# ---------------------------------------------------------------------------


class TestIndexSyncCoverage:
    """Cover index_sync.py uncovered lines."""

    def test_scan_prd_dir_exception_logged_and_skipped(self, tmp_path: Path) -> None:
        """Lines 79-80: exception during PRD scan is caught and logged."""
        from trw_mcp.state.index_sync import _scan_prd_dir

        prds_dir = tmp_path / "prds"
        prds_dir.mkdir()

        # Write a PRD file with invalid frontmatter that causes parse_frontmatter to fail
        bad_prd = prds_dir / "PRD-TEST-001.md"
        # Write content that has frontmatter markers but with data that causes
        # a TypeError in downstream processing — write bytes that fail read
        bad_prd.write_text("---\nid: null\ntitle:\n---\n# content", encoding="utf-8")

        # Mock parse_frontmatter to raise ValueError for this file
        with patch("trw_mcp.state.index_sync.parse_frontmatter") as mock_parse:
            mock_parse.side_effect = ValueError("bad frontmatter")
            entries = _scan_prd_dir(prds_dir)

        # Exception is caught — no entries returned, no exception raised
        assert entries == []

    def test_scan_prd_dir_oserror_skipped(self, tmp_path: Path) -> None:
        """Lines 79-80: OSError during file read is caught."""
        from trw_mcp.state.index_sync import _scan_prd_dir

        prds_dir = tmp_path / "prds"
        prds_dir.mkdir()

        prd_file = prds_dir / "PRD-TEST-001.md"
        prd_file.write_text("---\nid: PRD-TEST-001\n---\n", encoding="utf-8")

        with patch.object(Path, "read_text") as mock_read:
            mock_read.side_effect = OSError("disk error")
            entries = _scan_prd_dir(prds_dir)

        assert entries == []

    def test_scan_prd_frontmatters_adds_archived_entry(self, tmp_path: Path) -> None:
        """Lines 107-109: archived PRD with new ID gets added to entries."""
        from trw_mcp.state.index_sync import scan_prd_frontmatters

        # Create active prds dir with one PRD
        req_root = tmp_path / "docs" / "requirements"
        prds_dir = req_root / "prds"
        prds_dir.mkdir(parents=True)

        # Create archived prds dir with a DIFFERENT PRD
        archive_prds = req_root / "archive" / "prds"
        archive_prds.mkdir(parents=True)

        active_prd = prds_dir / "PRD-CORE-001.md"
        active_prd.write_text(
            "---\nid: PRD-CORE-001\ntitle: Active PRD\npriority: P1\nstatus: done\ncategory: CORE\n---\n",
            encoding="utf-8",
        )

        archived_prd = archive_prds / "PRD-CORE-002.md"
        archived_prd.write_text(
            "---\nid: PRD-CORE-002\ntitle: Archived PRD\npriority: P2\nstatus: deprecated\ncategory: CORE\n---\n",
            encoding="utf-8",
        )

        entries = scan_prd_frontmatters(prds_dir)

        ids = [e.id for e in entries]
        assert "PRD-CORE-001" in ids
        assert "PRD-CORE-002" in ids  # archived entry appended (lines 107-109)

    def test_scan_prd_frontmatters_archived_duplicate_skipped(self, tmp_path: Path) -> None:
        """Lines 105-109: archived PRD with SAME ID as active is deduplicated."""
        from trw_mcp.state.index_sync import scan_prd_frontmatters

        req_root = tmp_path / "docs" / "requirements"
        prds_dir = req_root / "prds"
        prds_dir.mkdir(parents=True)
        archive_prds = req_root / "archive" / "prds"
        archive_prds.mkdir(parents=True)

        # Both dirs have same PRD ID
        active_prd = prds_dir / "PRD-CORE-001.md"
        active_prd.write_text(
            "---\nid: PRD-CORE-001\ntitle: Active Version\npriority: P1\nstatus: done\ncategory: CORE\n---\n",
            encoding="utf-8",
        )
        archived_prd = archive_prds / "PRD-CORE-001.md"
        archived_prd.write_text(
            "---\nid: PRD-CORE-001\ntitle: Old Version\npriority: P1\nstatus: deprecated\ncategory: CORE\n---\n",
            encoding="utf-8",
        )

        entries = scan_prd_frontmatters(prds_dir)
        core001_entries = [e for e in entries if e.id == "PRD-CORE-001"]
        # Active takes precedence — only one entry
        assert len(core001_entries) == 1
        assert core001_entries[0].title == "Active Version"

    def test_render_index_catalogue_with_deprecated(self, tmp_path: Path) -> None:
        """Line 196: deprecated count > 0 appends deprecated summary."""
        from trw_mcp.state.index_sync import PRDEntry, render_index_catalogue

        entries = [
            PRDEntry(id="PRD-CORE-001", title="Done PRD", priority="P1", status="done", category="CORE"),
            PRDEntry(id="PRD-CORE-002", title="Deprecated PRD", priority="P2", status="deprecated", category="CORE"),
        ]
        result = render_index_catalogue(entries)
        assert "deprecated" in result
        assert "1 deprecated" in result  # line 196 branch hit


# ---------------------------------------------------------------------------
# state/recall_tracking.py coverage (lines 66-68)
# ---------------------------------------------------------------------------


class TestRecallTrackingExceptionPath:
    """Lines 66-68: record_outcome exception path."""

    def test_record_outcome_exception_returns_false(self, tmp_path: Path) -> None:
        """Lines 66-68: exception during record_outcome returns False."""
        from trw_mcp.state import recall_tracking

        # Make resolve_trw_dir raise an exception
        with patch("trw_mcp.state.recall_tracking.resolve_trw_dir") as mock_resolve:
            mock_resolve.side_effect = RuntimeError("trw dir not found")
            result = recall_tracking.record_outcome("L-abc123", "positive")

        assert result is False

    def test_record_outcome_file_not_exists_returns_false(self, tmp_path: Path) -> None:
        """record_outcome returns False if tracking file doesn't exist (line 56)."""
        from trw_mcp.state import recall_tracking

        with patch("trw_mcp.state.recall_tracking.resolve_trw_dir") as mock_resolve:
            mock_resolve.return_value = tmp_path / ".trw"
            # tracking file doesn't exist
            result = recall_tracking.record_outcome("L-abc123", "positive")

        assert result is False

    def test_record_outcome_writer_exception_returns_false(self, tmp_path: Path) -> None:
        """Lines 66-68: FileStateWriter.append_jsonl raises exception."""
        from trw_mcp.state import recall_tracking

        trw_dir = tmp_path / ".trw"
        logs_dir = trw_dir / "logs"
        logs_dir.mkdir(parents=True)
        # Create the tracking file so the existence check passes
        tracking_path = logs_dir / "recall_tracking.jsonl"
        tracking_path.write_text("", encoding="utf-8")

        with patch("trw_mcp.state.recall_tracking.resolve_trw_dir") as mock_resolve:
            mock_resolve.return_value = trw_dir
            with patch("trw_mcp.state.recall_tracking.FileStateWriter") as mock_writer_cls:
                mock_writer = MagicMock()
                mock_writer.append_jsonl.side_effect = OSError("write failed")
                mock_writer_cls.return_value = mock_writer
                result = recall_tracking.record_outcome("L-abc123", "neutral")

        assert result is False


# ---------------------------------------------------------------------------
# telemetry/publisher.py coverage (lines 49, 76)
# ---------------------------------------------------------------------------


class TestPublisherCoverage:
    """Cover publisher.py uncovered lines."""

    def _make_cfg_with_platform(self, tmp_path: Path) -> Any:
        """Create a config with platform_url set."""
        from trw_mcp.models.config import TRWConfig

        cfg = TRWConfig.__new__(TRWConfig)
        object.__setattr__(cfg, "platform_url", "http://test.example.com")
        object.__setattr__(cfg, "platform_telemetry_enabled", True)
        object.__setattr__(cfg, "installation_id", "test-install")
        return cfg

    def test_publish_learnings_empty_data_continue(self, tmp_path: Path) -> None:
        """Line 49: reader.read_yaml returns empty/falsy data → continue."""
        from trw_mcp.telemetry import publisher as pub

        # publisher looks at: trw_dir / "learnings" / "entries"
        entries_dir = tmp_path / "learnings" / "entries"
        entries_dir.mkdir(parents=True)

        # Write a YAML file that will read as empty
        empty_file = entries_dir / "empty.yaml"
        empty_file.write_text("", encoding="utf-8")

        with patch("trw_mcp.telemetry.publisher.get_config") as mock_cfg:
            cfg = MagicMock()
            cfg.platform_url = "http://test.example.com"
            cfg.effective_platform_urls = ["http://test.example.com"]
            cfg.platform_telemetry_enabled = True
            cfg.installation_id = "test"
            mock_cfg.return_value = cfg

            with patch("trw_mcp.telemetry.publisher.resolve_trw_dir") as mock_trw:
                mock_trw.return_value = tmp_path

                with patch("trw_mcp.telemetry.publisher.FileStateReader") as mock_reader_cls:
                    mock_reader = MagicMock()
                    mock_reader.read_yaml.return_value = {}  # falsy for `if not data`
                    mock_reader_cls.return_value = mock_reader

                    result = pub.publish_learnings()

        # Empty data → continue (no entries published/errored)
        assert result["published"] == 0

    def test_publish_learnings_tags_not_list_coerced_to_empty(self, tmp_path: Path) -> None:
        """Line 76: tags field is not a list → coerced to []."""
        from trw_mcp.telemetry import publisher as pub

        # publisher looks at: trw_dir / "learnings" / "entries"
        entries_dir = tmp_path / "learnings" / "entries"
        entries_dir.mkdir(parents=True)

        yaml_file = entries_dir / "learning.yaml"
        yaml_file.write_text(
            "status: active\nimpact: 0.9\nsummary: Test\ndetail: Detail\ntags: not-a-list\n",
            encoding="utf-8",
        )

        with patch("trw_mcp.telemetry.publisher.get_config") as mock_cfg:
            cfg = MagicMock()
            cfg.platform_url = "http://test.example.com"
            cfg.effective_platform_urls = ["http://test.example.com"]
            cfg.platform_telemetry_enabled = True
            cfg.installation_id = "test"
            cfg.platform_api_key.get_secret_value.return_value = ""
            mock_cfg.return_value = cfg

            with patch("trw_mcp.telemetry.publisher.resolve_trw_dir") as mock_trw:
                mock_trw.return_value = tmp_path

                with patch("trw_mcp.telemetry.publisher.FileStateReader") as mock_reader_cls:
                    mock_reader = MagicMock()
                    # Return data where tags is a string (not a list)
                    mock_reader.read_yaml.return_value = {
                        "status": "active",
                        "impact": 0.9,
                        "summary": "Test summary",
                        "detail": "Test detail",
                        "tags": "not-a-list",  # string, not list
                        "published_to_platform": False,
                    }
                    mock_reader_cls.return_value = mock_reader

                    with patch("trw_mcp.telemetry.publisher.strip_pii", side_effect=lambda x: x):
                        with patch("trw_mcp.telemetry.publisher.embed", return_value=[0.1]):
                            with patch("trw_mcp.telemetry.publisher._post_learning", return_value=True):
                                result = pub.publish_learnings()

        # tags coerced to [] — no error
        assert result["errors"] == 0
        assert result["published"] == 1


# ---------------------------------------------------------------------------
# state/auto_upgrade.py coverage (lines 29-30)
# ---------------------------------------------------------------------------


class TestAutoUpgradeCoverage:
    """Lines 29-30: get_installed_version ImportError/AttributeError."""

    def test_get_installed_version_missing_attr_returns_fallback(
        self,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Lines 29-30: When __version__ is missing, returns '0.0.0'.

        The function does ``from trw_mcp import __version__``.
        When __version__ is absent, ImportError is caught and '0.0.0' returned.
        (Both ImportError and AttributeError trigger the same except clause.)
        """
        import trw_mcp as _trw_mcp_mod
        from trw_mcp.state.auto_upgrade import get_installed_version

        monkeypatch.delattr(_trw_mcp_mod, "__version__", raising=False)
        result = get_installed_version()
        assert result == "0.0.0"

    def test_get_installed_version_returns_actual_version(self) -> None:
        """Positive case: get_installed_version returns a semver string."""
        import re

        from trw_mcp.state.auto_upgrade import get_installed_version

        version = get_installed_version()
        assert isinstance(version, str)
        assert len(version) > 0
        assert re.match(r"\d+\.\d+\.\d+", version), f"Expected semver pattern, got: {version!r}"


# ---------------------------------------------------------------------------
# prompts/aaref.py coverage (line 28)
# ---------------------------------------------------------------------------


class TestAarefPromptFallback:
    """Line 28: _load_prompt_template fallback when file not found."""

    def test_load_prompt_template_missing_file_returns_fallback(self) -> None:
        """Line 28: non-existent template returns fallback message."""
        from trw_mcp.prompts.aaref import _load_prompt_template

        result = _load_prompt_template("nonexistent_template_xyz.md")
        assert "nonexistent_template_xyz.md" in result
        assert "not found" in result

    @pytest.mark.skipif(not _HAS_AAREF_TEMPLATES, reason="No .md templates in aaref data dir")
    def test_load_prompt_template_existing_file_returns_content(self) -> None:
        """Line 26-27: existing template file returns its content."""
        from trw_mcp.prompts.aaref import _DATA_DIR, _load_prompt_template

        # Find any actual template file in the data dir
        templates = list(_DATA_DIR.glob("*.md"))

        content = _load_prompt_template(templates[0].name)
        assert len(content) > 0
        assert "not found" not in content


# ---------------------------------------------------------------------------
# prompts/messaging.py coverage (lines 60, 98)
# ---------------------------------------------------------------------------


class TestMessagingCoverage:
    """Lines 60, 98: kwargs formatting and list fallback."""

    def test_get_message_with_kwargs_formats_string(self) -> None:
        """Line 60: get_message with kwargs substitutes values."""
        from trw_mcp.prompts.messaging import _load_messages, get_message

        messages = _load_messages()
        # Find a key that has a format placeholder, or mock one
        with patch("trw_mcp.prompts.messaging._load_messages") as mock_load:
            mock_load.return_value = {"test_key": "Hello {name}, you have {count} items"}
            result = get_message("test_key", name="Alice", count=5)

        assert result == "Hello Alice, you have 5 items"

    def test_get_message_without_kwargs_returns_raw(self) -> None:
        """Line 61 (no kwargs branch): returns raw string."""
        from trw_mcp.prompts import messaging

        with patch.object(messaging, "_load_messages") as mock_load:
            mock_load.return_value = {"simple_key": "Simple message"}
            result = messaging.get_message("simple_key")

        assert result == "Simple message"

    def test_get_message_or_default_with_kwargs_fallback(self) -> None:
        """Lines 78-79: get_message_or_default kwargs applied to default."""
        from trw_mcp.prompts.messaging import get_message_or_default

        result = get_message_or_default(
            "nonexistent_key_xyz",
            "Default {thing} message",
            thing="formatted",
        )
        assert result == "Default formatted message"

    def test_get_message_lines_returns_list_type(self) -> None:
        """Lines 97-98: get_message_lines with list value."""
        from trw_mcp.prompts import messaging

        with patch.object(messaging, "_load_messages") as mock_load:
            mock_load.return_value = {"list_key": ["item one", "item two", "item three"]}
            result = messaging.get_message_lines("list_key")

        assert result == ["item one", "item two", "item three"]

    def test_get_message_lines_non_list_wrapped(self) -> None:
        """Line 98 (else branch): non-list value wrapped in list."""
        from trw_mcp.prompts import messaging

        with patch.object(messaging, "_load_messages") as mock_load:
            mock_load.return_value = {"scalar_key": "single value"}
            result = messaging.get_message_lines("scalar_key")

        assert result == ["single value"]

    def test_get_message_with_kwargs_coerces_values_to_str(self) -> None:
        """Line 60: kwargs values are coerced to str before format."""
        from trw_mcp.prompts import messaging

        with patch.object(messaging, "_load_messages") as mock_load:
            mock_load.return_value = {"count_msg": "Count is {n}"}
            result = messaging.get_message("count_msg", n=42)

        assert result == "Count is 42"


# ---------------------------------------------------------------------------
# state/_paths.py coverage (lines 58, 172)
# ---------------------------------------------------------------------------


class TestPathsCoverage:
    """Lines 58, 172: _find_latest_run_dir and detect_current_phase."""

    def test_find_latest_run_dir_skips_non_dir_runs(self, tmp_path: Path) -> None:
        """iter_run_dirs skips task_dirs without valid run subdirectories."""
        from trw_mcp.state._paths import _find_latest_run_dir

        runs_root = tmp_path / ".trw" / "runs"
        runs_root.mkdir(parents=True)

        # Create a task_dir that has NO run subdirectories
        task_no_runs = runs_root / "task-no-runs"
        task_no_runs.mkdir()
        # No run dirs inside — should be skipped

        # Create another task_dir that HAS a valid run
        task_with_run = runs_root / "task-with-run"
        run_dir = task_with_run / "20260206T120000Z-abc1"
        (run_dir / "meta").mkdir(parents=True)
        (run_dir / "meta" / "run.yaml").write_text("run_id: abc1\n", encoding="utf-8")

        result = _find_latest_run_dir(runs_root)
        assert result is not None
        assert result.name == "20260206T120000Z-abc1"

    def test_detect_current_phase_skips_non_dir_runs(
        self,
        tmp_path: Path,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """detect_current_phase skips task_dirs without valid run subdirectories."""
        from trw_mcp.state import _paths

        runs_root = tmp_path / ".trw" / "runs"
        runs_root.mkdir(parents=True)

        # task_dir with no run subdirectories — should be skipped
        no_runs_dir = runs_root / "no-runs-task"
        no_runs_dir.mkdir()

        # Valid task with a completed (not active) run
        valid_task = runs_root / "valid-task"
        run_dir = valid_task / "20260206T120000Z-xyz1"
        (run_dir / "meta").mkdir(parents=True)
        run_yaml = run_dir / "meta" / "run.yaml"
        # Status is "complete", not "active"
        run_yaml.write_text("run_id: xyz1\nphase: deliver\nstatus: complete\n", encoding="utf-8")

        from trw_mcp.models.config import TRWConfig

        cfg = TRWConfig()
        object.__setattr__(cfg, "runs_root", ".trw/runs")

        with patch("trw_mcp.state._paths.resolve_project_root", return_value=tmp_path):
            with patch("trw_mcp.state._paths.get_config", return_value=cfg):
                result = _paths.detect_current_phase()

        # Status is "complete", not "active" → returns None
        assert result is None

    def test_detect_current_phase_inactive_run_returns_none(
        self,
        tmp_path: Path,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """detect_current_phase returns None when status != active."""
        from trw_mcp.state import _paths

        runs_root = tmp_path / ".trw" / "runs"
        task_dir = runs_root / "my-task"
        run_dir = task_dir / "20260206T120000Z-xyz1"
        (run_dir / "meta").mkdir(parents=True)
        run_yaml = run_dir / "meta" / "run.yaml"
        run_yaml.write_text("run_id: xyz1\nphase: deliver\nstatus: complete\n", encoding="utf-8")

        from trw_mcp.models.config import TRWConfig

        cfg = TRWConfig()
        object.__setattr__(cfg, "runs_root", ".trw/runs")

        with patch("trw_mcp.state._paths.resolve_project_root", return_value=tmp_path):
            with patch("trw_mcp.state._paths.get_config", return_value=cfg):
                result = _paths.detect_current_phase()

        assert result is None

    def test_detect_current_phase_active_run_returns_phase(
        self,
        tmp_path: Path,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Positive: detect_current_phase returns phase when status == active."""
        from trw_mcp.state import _paths

        runs_root = tmp_path / ".trw" / "runs"
        task_dir = runs_root / "my-task"
        run_dir = task_dir / "20260206T120000Z-xyz2"
        (run_dir / "meta").mkdir(parents=True)
        run_yaml = run_dir / "meta" / "run.yaml"
        run_yaml.write_text("run_id: xyz2\nphase: implement\nstatus: active\n", encoding="utf-8")

        from trw_mcp.models.config import TRWConfig

        cfg = TRWConfig()
        object.__setattr__(cfg, "runs_root", ".trw/runs")

        with patch("trw_mcp.state._paths.resolve_project_root", return_value=tmp_path):
            with patch("trw_mcp.state._paths.get_config", return_value=cfg):
                result = _paths.detect_current_phase()

        assert result == "implement"
