"""Integration tests for ievo.tui.app — Textual Pilot-based real widget interaction.

These tests use Textual's `app.run_test()` + Pilot API to test the full
TUI lifecycle with real widgets, events, and keyboard input. They complement
the unit-level mock tests in test_tui.py.

All tests are async and run with pytest-asyncio (asyncio_mode = "auto" in pyproject.toml).
"""

from __future__ import annotations

from pathlib import Path
from unittest.mock import patch

import yaml as _yaml
from textual.widgets import SelectionList, Static, TabbedContent  # noqa: E402

from ievo.tui.app import IevoApp

# ── Helpers ──────────────────────────────────────────────────


def _make_project(root: Path, agents: dict[str, str] | None = None) -> None:
    """Create a minimal project structure for integration tests.

    Creates:
    - .ievo/manifest.yaml
    - agents/<name>/EVOLUTION_LOG.md for each agent
    """
    if agents is None:
        agents = {"spec-writer": "0.1.0"}

    manifest_data = {
        "project": "test-project",
        "version": "0.1.0",
        "agents": agents,
    }
    (root / ".ievo").mkdir(exist_ok=True)
    (root / ".ievo" / "manifest.yaml").write_text(_yaml.dump(manifest_data, sort_keys=False))

    for name in agents:
        agent_dir = root / "agents" / name
        agent_dir.mkdir(parents=True, exist_ok=True)
        (agent_dir / "EVOLUTION_LOG.md").write_text("# Evolution Log\n\n")


def _make_app() -> IevoApp:
    """Return a fresh IevoApp instance."""
    return IevoApp()


# ── Step 1: Focus and tab navigation diagnostics ─────────────


async def test_focus_lands_on_selection_list(tmp_path: Path) -> None:
    """After fix (AUTO_FOCUS = '#agents-list'), focus lands on SelectionList.

    REQ-007 AC: Tab key moves focus; SelectionList is focusable.
    REQ-007 AC: On mount, Agents tab visible and SelectionList rendered.
    """
    _make_project(tmp_path)
    app = _make_app()
    with patch("ievo.tui.app.find_project_root", return_value=tmp_path):
        async with app.run_test(size=(120, 40)) as pilot:
            await pilot.pause()
            # AUTO_FOCUS = "#agents-list" must land focus here
            assert isinstance(app.focused, SelectionList)
            assert app.focused.id == "agents-list"


async def test_tab_switch_agents_to_pipeline(tmp_path: Path) -> None:
    """Pressing 'p' switches to the Pipeline tab.

    REQ-007 AC: Pressing p switches to Pipeline tab.
    """
    _make_project(tmp_path)
    app = _make_app()
    with patch("ievo.tui.app.find_project_root", return_value=tmp_path):
        async with app.run_test(size=(120, 40)) as pilot:
            await pilot.pause()
            tabbed = app.query_one(TabbedContent)
            assert tabbed.active == "agents"

            await pilot.press("p")
            await pilot.pause()
            assert tabbed.active == "pipeline"


async def test_tab_switch_pipeline_to_evolution(tmp_path: Path) -> None:
    """Pressing 'e' switches to the Evolution tab.

    REQ-007 AC: Pressing e switches to Evolution tab.
    """
    _make_project(tmp_path)
    app = _make_app()
    with patch("ievo.tui.app.find_project_root", return_value=tmp_path):
        async with app.run_test(size=(120, 40)) as pilot:
            await pilot.pause()
            await pilot.press("e")
            await pilot.pause()
            tabbed = app.query_one(TabbedContent)
            assert tabbed.active == "evolution"


async def test_tab_switch_to_files(tmp_path: Path) -> None:
    """Pressing 'f' switches to the Files tab.

    REQ-007 AC: Pressing f switches to Files tab.
    """
    _make_project(tmp_path)
    app = _make_app()
    with patch("ievo.tui.app.find_project_root", return_value=tmp_path):
        async with app.run_test(size=(120, 40)) as pilot:
            await pilot.pause()
            await pilot.press("f")
            await pilot.pause()
            tabbed = app.query_one(TabbedContent)
            assert tabbed.active == "files"


async def test_tab_switch_back_to_agents(tmp_path: Path) -> None:
    """Pressing 'a' returns to the Agents tab from any other tab.

    REQ-007 AC: Pressing a returns to Agents tab.
    """
    _make_project(tmp_path)
    app = _make_app()
    with patch("ievo.tui.app.find_project_root", return_value=tmp_path):
        async with app.run_test(size=(120, 40)) as pilot:
            await pilot.pause()
            await pilot.press("p")
            await pilot.pause()
            await pilot.press("a")
            await pilot.pause()
            tabbed = app.query_one(TabbedContent)
            assert tabbed.active == "agents"


async def test_tab_key_cycles_focus(tmp_path: Path) -> None:
    """Tab key moves focus; SelectionList is reachable via Tab.

    REQ-007 AC: Tab key moves focus; SelectionList is focusable.
    """
    _make_project(tmp_path)
    app = _make_app()
    with patch("ievo.tui.app.find_project_root", return_value=tmp_path):
        async with app.run_test(size=(120, 40)) as pilot:
            await pilot.pause()
            # With AUTO_FOCUS = "#agents-list", focus is already on SelectionList.
            # Verify it's focusable (it is focused).
            assert isinstance(app.focused, SelectionList)


# ── Step 3: SelectionList population ─────────────────────────


async def test_selection_list_has_all_bundled_agents(tmp_path: Path) -> None:
    """All bundled agents appear in the SelectionList.

    REQ-007 AC: On mount, Agents tab visible and SelectionList rendered with all bundled entries.
    """
    from ievo.marketplace import list_bundled_agents

    _make_project(tmp_path, agents={})
    app = _make_app()
    with patch("ievo.tui.app.find_project_root", return_value=tmp_path):
        async with app.run_test(size=(120, 40)) as pilot:
            await pilot.pause()
            selection_list = app.query_one("#agents-list", SelectionList)
            assert selection_list.option_count == len(list_bundled_agents())


async def test_selection_list_shows_installed_checked(tmp_path: Path) -> None:
    """Installed agents appear checked in the SelectionList.

    REQ-007 AC: SelectionList shows installed agents with checkboxes checked.
    """
    _make_project(tmp_path, agents={"spec-writer": "0.1.0"})
    app = _make_app()
    with patch("ievo.tui.app.find_project_root", return_value=tmp_path):
        async with app.run_test(size=(120, 40)) as pilot:
            await pilot.pause()
            selection_list = app.query_one("#agents-list", SelectionList)
            # spec-writer is installed → should be in selected list
            assert "spec-writer" in selection_list.selected


async def test_selection_list_shows_uninstalled_unchecked(tmp_path: Path) -> None:
    """With no agents installed, SelectionList has no checked items.

    REQ-007 AC: Uninstalled agents have unchecked checkboxes.
    """
    _make_project(tmp_path, agents={})
    app = _make_app()
    with patch("ievo.tui.app.find_project_root", return_value=tmp_path):
        async with app.run_test(size=(120, 40)) as pilot:
            await pilot.pause()
            selection_list = app.query_one("#agents-list", SelectionList)
            assert list(selection_list.selected) == []


# ── Step 4: Agent toggle ON/OFF integration tests ────────────


async def test_toggle_agent_on_creates_md_file(tmp_path: Path) -> None:
    """Toggling ON an agent creates the .md file and updates the manifest.

    REQ-007 AC: Space toggles ON — agent .md file created, manifest updated.
    """
    _make_project(tmp_path, agents={})
    app = _make_app()
    with patch("ievo.tui.app.find_project_root", return_value=tmp_path):
        async with app.run_test(size=(120, 40)) as pilot:
            await pilot.pause()
            # SelectionList already has focus (AUTO_FOCUS fix).
            # Navigate to first agent with Down, then toggle with Space.
            await pilot.press("down")
            await pilot.pause()
            await pilot.press("space")
            await pilot.pause()

            # Determine which agent is first in the registry
            selection_list = app.query_one("#agents-list", SelectionList)
            assert len(selection_list.selected) == 1
            toggled_name = list(selection_list.selected)[0]

            # File must exist
            agent_md = tmp_path / ".claude" / "agents" / f"{toggled_name}.md"
            assert agent_md.exists(), f"Expected {agent_md} to exist after toggle ON"

            # Manifest must be updated
            from ievo.core.project import ProjectManifest

            manifest = ProjectManifest.load(tmp_path)
            assert manifest.has_agent(toggled_name)


async def test_toggle_agent_off_removes_md_file(tmp_path: Path) -> None:
    """Toggling OFF an installed agent removes the .md file and updates the manifest.

    REQ-007 AC: Space toggles OFF — agent .md file removed, manifest updated.
    """
    _make_project(tmp_path, agents={"spec-writer": "0.1.0"})
    # Pre-create the .md file so it can be removed
    claude_dir = tmp_path / ".claude" / "agents"
    claude_dir.mkdir(parents=True, exist_ok=True)
    (claude_dir / "spec-writer.md").write_text("---\nname: spec-writer\n---\n")

    app = _make_app()
    with patch("ievo.tui.app.find_project_root", return_value=tmp_path):
        async with app.run_test(size=(120, 40), notifications=True) as pilot:
            await pilot.pause()
            # Find spec-writer's position in the list and navigate to it
            selection_list = app.query_one("#agents-list", SelectionList)
            # Find index of spec-writer
            spec_writer_idx = None
            for i in range(selection_list.option_count):
                if selection_list.get_option_at_index(i).value == "spec-writer":
                    spec_writer_idx = i
                    break
            assert spec_writer_idx is not None

            # Navigate to spec-writer
            await pilot.press("down")  # Go to index 0 first
            await pilot.pause()
            for _ in range(spec_writer_idx):
                await pilot.press("down")
                await pilot.pause()

            # Toggle OFF (spec-writer is currently checked)
            await pilot.press("space")
            await pilot.pause()

            # File must be removed
            assert not (claude_dir / "spec-writer.md").exists()

            # Manifest must be updated
            from ievo.core.project import ProjectManifest

            manifest = ProjectManifest.load(tmp_path)
            assert not manifest.has_agent("spec-writer")


async def test_toggle_mandatory_agent_off_shows_warning(tmp_path: Path) -> None:
    """Toggling OFF a mandatory agent shows a warning notification.

    REQ-007 AC: Toggling a mandatory agent OFF shows a warning notification (toast).
    """
    _make_project(tmp_path, agents={"spec-writer": "0.1.0"})
    claude_dir = tmp_path / ".claude" / "agents"
    claude_dir.mkdir(parents=True, exist_ok=True)
    (claude_dir / "spec-writer.md").write_text("---\nname: spec-writer\n---\n")

    app = _make_app()
    notifications: list = []

    original_notify = IevoApp.notify

    def capture_notify(self, message, **kwargs):
        notifications.append({"message": message, **kwargs})
        return original_notify(self, message, **kwargs)

    with (
        patch("ievo.tui.app.find_project_root", return_value=tmp_path),
        patch.object(IevoApp, "notify", capture_notify),
    ):
        async with app.run_test(size=(120, 40), notifications=True) as pilot:
            await pilot.pause()
            selection_list = app.query_one("#agents-list", SelectionList)

            # Find spec-writer index
            spec_writer_idx = None
            for i in range(selection_list.option_count):
                if selection_list.get_option_at_index(i).value == "spec-writer":
                    spec_writer_idx = i
                    break
            assert spec_writer_idx is not None

            # Navigate to spec-writer
            await pilot.press("down")
            await pilot.pause()
            for _ in range(spec_writer_idx):
                await pilot.press("down")
                await pilot.pause()

            # Toggle OFF
            await pilot.press("space")
            await pilot.pause()

    # Check that a warning notification about mandatory was sent
    # Note: a startup "outdated" warning may also fire since spec-writer 0.1.0 < 0.2.0
    warning_notifications = [n for n in notifications if n.get("severity") == "warning"]
    assert len(warning_notifications) >= 1
    mandatory_warnings = [n for n in warning_notifications if "mandatory" in n["message"].lower()]
    assert len(mandatory_warnings) >= 1, (
        f"Expected a 'mandatory' warning but got: {[n['message'] for n in warning_notifications]}"
    )


# ── Step 5: Keyboard navigation and pipeline update ──────────


async def test_arrow_keys_navigate_selection_list(tmp_path: Path) -> None:
    """Up/Down arrows navigate between agent entries in SelectionList.

    REQ-007 AC: Up/Down arrows navigate between entries.
    """
    _make_project(tmp_path, agents={})
    app = _make_app()
    with patch("ievo.tui.app.find_project_root", return_value=tmp_path):
        async with app.run_test(size=(120, 40)) as pilot:
            await pilot.pause()
            selection_list = app.query_one("#agents-list", SelectionList)

            # Press Down 3 times → should be at index 2
            await pilot.press("down")
            await pilot.pause()
            await pilot.press("down")
            await pilot.pause()
            await pilot.press("down")
            await pilot.pause()
            assert selection_list.highlighted == 2

            # Press Up → should be at index 1
            await pilot.press("up")
            await pilot.pause()
            assert selection_list.highlighted == 1


async def test_pipeline_updates_after_toggle(tmp_path: Path) -> None:
    """Pipeline tab content reflects the agent set after toggle.

    REQ-007 AC: Pipeline tab content updates after agent toggle.
    """
    _make_project(tmp_path, agents={})
    app = _make_app()
    with patch("ievo.tui.app.find_project_root", return_value=tmp_path):
        async with app.run_test(size=(120, 40)) as pilot:
            await pilot.pause()

            # Verify pipeline shows "No agents" initially
            pipeline_view = app.query_one("#pipeline-view", Static)
            initial_content = str(pipeline_view.render())
            assert "No agents" in initial_content

            # Toggle first agent ON
            await pilot.press("down")
            await pilot.pause()
            await pilot.press("space")
            await pilot.pause()

            # Determine which agent was toggled
            selection_list = app.query_one("#agents-list", SelectionList)
            toggled_name = list(selection_list.selected)[0]

            # Switch to Pipeline tab
            await pilot.press("p")
            await pilot.pause()

            # Pipeline content should now include the toggled agent name
            updated_content = str(pipeline_view.render())
            assert toggled_name in updated_content


# ── Step 6: Edge cases and lifecycle ─────────────────────────


async def test_no_project_shows_message() -> None:
    """App with no project shows 'No project found' message.

    REQ-007 AC: App with no project shows 'No project found' message, no crash.
    """
    app = _make_app()
    with patch("ievo.tui.app.find_project_root", return_value=None):
        async with app.run_test(size=(120, 40)) as pilot:
            await pilot.pause()
            info = app.query_one("#project-info", Static)
            content = str(info.render())
            assert "No project" in content


async def test_no_project_no_crash_on_interaction() -> None:
    """With no project, pressing various keys causes no crash.

    REQ-007 AC: No crash on interaction when no project is found.
    """
    app = _make_app()
    with patch("ievo.tui.app.find_project_root", return_value=None):
        async with app.run_test(size=(120, 40)) as pilot:
            await pilot.pause()
            # Press all navigation keys — none should raise
            for key in ("p", "e", "f", "a", "tab", "space", "up", "down"):
                await pilot.press(key)
                await pilot.pause()
            # If we get here without exception, the test passes


async def test_quit_action(tmp_path: Path) -> None:
    """Pressing 'q' exits the app cleanly.

    REQ-007 AC: Pressing q exits the app cleanly.
    """
    _make_project(tmp_path)
    app = _make_app()
    with patch("ievo.tui.app.find_project_root", return_value=tmp_path):
        async with app.run_test(size=(120, 40)) as pilot:
            await pilot.pause()
            await pilot.press("q")
            await pilot.pause()
        # If the context manager exits without exception, app quit cleanly.


async def test_refresh_action_updates_project_root(tmp_path: Path) -> None:
    """Pressing 'r' re-runs find_project_root and updates project_root.

    REQ-007 AC: Pressing r refreshes data (calls find_project_root again).
    """
    _make_project(tmp_path)
    app = _make_app()

    call_count = 0
    original_results = [tmp_path, None]

    def counting_find_root():
        nonlocal call_count
        result = original_results[min(call_count, len(original_results) - 1)]
        call_count += 1
        return result

    with patch("ievo.tui.app.find_project_root", side_effect=counting_find_root):
        async with app.run_test(size=(120, 40)) as pilot:
            await pilot.pause()
            assert call_count == 1  # called once on mount
            assert app.project_root == tmp_path

            await pilot.press("r")
            await pilot.pause()
            assert call_count == 2  # called again on refresh
            assert app.project_root is None

            # info widget should now show "No project"
            info = app.query_one("#project-info", Static)
            content = str(info.render())
            assert "No project" in content


async def test_compose_yields_all_expected_widgets(tmp_path: Path) -> None:
    """All expected widget IDs exist after compose.

    REQ-007 AC: On mount, all expected widgets are present.
    """
    _make_project(tmp_path)
    app = _make_app()
    with patch("ievo.tui.app.find_project_root", return_value=tmp_path):
        async with app.run_test(size=(120, 40)) as pilot:
            await pilot.pause()
            # Verify all expected widget IDs are present
            expected_ids = [
                "#logo",
                "#project-info",
                "#agents-list",
                "#pipeline-view",
                "#evo-log",
                "#project-tree",
            ]
            for widget_id in expected_ids:
                widget = app.query_one(widget_id)
                assert widget is not None, f"Widget {widget_id} not found"


# ── REQ-008: Agent Version Check integration tests ───────────


async def test_outdated_badge_visible_on_startup(tmp_path: Path) -> None:
    """Outdated agent row shows [outdated] badge in SelectionList.

    REQ-008 AC: outdated badge shown in SelectionList for outdated agents.
    """
    # spec-writer 0.1.0 is outdated vs registry 0.2.0
    _make_project(tmp_path, agents={"spec-writer": "0.1.0"})
    app = _make_app()
    with patch("ievo.tui.app.find_project_root", return_value=tmp_path):
        async with app.run_test(size=(120, 40)) as pilot:
            await pilot.pause()
            selection_list = app.query_one("#agents-list", SelectionList)

            # Find spec-writer option and check its prompt text
            spec_writer_opt = None
            for i in range(selection_list.option_count):
                opt = selection_list.get_option_at_index(i)
                if opt.value == "spec-writer":
                    spec_writer_opt = opt
                    break
            assert spec_writer_opt is not None
            prompt_text = str(spec_writer_opt.prompt)
            assert "outdated" in prompt_text.lower()


async def test_no_outdated_badge_when_up_to_date(tmp_path: Path) -> None:
    """No outdated badge when versions match.

    REQ-008 AC: no badge shown when all agents up to date.
    """
    _make_project(tmp_path, agents={"spec-writer": "0.2.0"})
    app = _make_app()
    with patch("ievo.tui.app.find_project_root", return_value=tmp_path):
        async with app.run_test(size=(120, 40)) as pilot:
            await pilot.pause()
            selection_list = app.query_one("#agents-list", SelectionList)

            for i in range(selection_list.option_count):
                opt = selection_list.get_option_at_index(i)
                prompt_text = str(opt.prompt)
                assert "outdated" not in prompt_text.lower(), (
                    f"Unexpected outdated badge on {opt.value}: {prompt_text}"
                )


async def test_outdated_toast_on_startup(tmp_path: Path) -> None:
    """Warning toast shown on startup when agents are outdated.

    REQ-008 AC: startup toast with severity=warning when outdated agents detected.
    """
    _make_project(tmp_path, agents={"spec-writer": "0.1.0"})
    app = _make_app()
    notifications: list = []

    original_notify = IevoApp.notify

    def capture_notify(self, message, **kwargs):
        notifications.append({"message": message, **kwargs})
        return original_notify(self, message, **kwargs)

    with (
        patch("ievo.tui.app.find_project_root", return_value=tmp_path),
        patch.object(IevoApp, "notify", capture_notify),
    ):
        async with app.run_test(size=(120, 40), notifications=True) as pilot:
            await pilot.pause()

    warning_notifications = [n for n in notifications if n.get("severity") == "warning"]
    assert len(warning_notifications) >= 1
    assert "outdated" in warning_notifications[0]["message"].lower()


async def test_no_outdated_toast_when_up_to_date(tmp_path: Path) -> None:
    """No warning toast on startup when all agents are up to date.

    REQ-008 AC: no startup toast when all up-to-date.
    """
    _make_project(tmp_path, agents={"spec-writer": "0.2.0"})
    app = _make_app()
    notifications: list = []

    original_notify = IevoApp.notify

    def capture_notify(self, message, **kwargs):
        notifications.append({"message": message, **kwargs})
        return original_notify(self, message, **kwargs)

    with (
        patch("ievo.tui.app.find_project_root", return_value=tmp_path),
        patch.object(IevoApp, "notify", capture_notify),
    ):
        async with app.run_test(size=(120, 40), notifications=True) as pilot:
            await pilot.pause()

    warning_notifications = [n for n in notifications if n.get("severity") == "warning"]
    assert len(warning_notifications) == 0


async def test_u_key_updates_all_outdated(tmp_path: Path) -> None:
    """Pressing 'u' updates all outdated agents.

    REQ-008 AC: u key batch-updates all outdated agents.
    """
    _make_project(tmp_path, agents={"spec-writer": "0.1.0"})
    app = _make_app()
    notifications: list = []

    original_notify = IevoApp.notify

    def capture_notify(self, message, **kwargs):
        notifications.append({"message": message, **kwargs})
        return original_notify(self, message, **kwargs)

    with (
        patch("ievo.tui.app.find_project_root", return_value=tmp_path),
        patch.object(IevoApp, "notify", capture_notify),
    ):
        async with app.run_test(size=(120, 40), notifications=True) as pilot:
            await pilot.pause()
            # Verify badge present before update
            selection_list = app.query_one("#agents-list", SelectionList)
            for i in range(selection_list.option_count):
                opt = selection_list.get_option_at_index(i)
                if opt.value == "spec-writer":
                    assert "outdated" in str(opt.prompt).lower()
                    break

            # Press u to update all
            await pilot.press("u")
            await pilot.pause()

    # Verify agent .md was created (installed)
    agent_md = tmp_path / ".claude" / "agents" / "spec-writer.md"
    assert agent_md.exists(), "spec-writer.md should be installed after u press"

    # Verify manifest was updated to new version
    from ievo.core.project import ProjectManifest

    manifest = ProjectManifest.load(tmp_path)
    assert manifest.agents.get("spec-writer") == "0.2.0"

    # Verify "Updated" notification was shown
    info_notifications = [n for n in notifications if n.get("severity") == "information"]
    updated_notifications = [n for n in info_notifications if "Updated" in n.get("message", "")]
    assert len(updated_notifications) >= 1


async def test_u_key_when_all_up_to_date(tmp_path: Path) -> None:
    """Pressing 'u' with no outdated agents shows 'All agents are up to date'.

    REQ-008 AC: no outdated + u press shows 'All up to date'.
    """
    _make_project(tmp_path, agents={"spec-writer": "0.2.0"})
    app = _make_app()
    notifications: list = []

    original_notify = IevoApp.notify

    def capture_notify(self, message, **kwargs):
        notifications.append({"message": message, **kwargs})
        return original_notify(self, message, **kwargs)

    with (
        patch("ievo.tui.app.find_project_root", return_value=tmp_path),
        patch.object(IevoApp, "notify", capture_notify),
    ):
        async with app.run_test(size=(120, 40), notifications=True) as pilot:
            await pilot.pause()
            await pilot.press("u")
            await pilot.pause()

    info_notifications = [n for n in notifications if n.get("severity") == "information"]
    up_to_date_notifications = [
        n for n in info_notifications if "All agents are up to date" in n.get("message", "")
    ]
    assert len(up_to_date_notifications) >= 1


async def test_outdated_badge_disappears_after_single_agent_reinstall(tmp_path: Path) -> None:
    """Toggle OFF then ON reinstalls agent and clears the outdated badge.

    REQ-008 AC: badge disappears after update via toggle reinstall.
    """
    _make_project(tmp_path, agents={"spec-writer": "0.1.0"})
    # Pre-create the .md file for the toggle-OFF path
    claude_dir = tmp_path / ".claude" / "agents"
    claude_dir.mkdir(parents=True, exist_ok=True)
    (claude_dir / "spec-writer.md").write_text("---\nname: spec-writer\n---\n")

    app = _make_app()
    with patch("ievo.tui.app.find_project_root", return_value=tmp_path):
        async with app.run_test(size=(120, 40), notifications=True) as pilot:
            await pilot.pause()

            selection_list = app.query_one("#agents-list", SelectionList)

            # Find spec-writer index
            spec_writer_idx = None
            for i in range(selection_list.option_count):
                if selection_list.get_option_at_index(i).value == "spec-writer":
                    spec_writer_idx = i
                    break
            assert spec_writer_idx is not None

            # Navigate to spec-writer
            await pilot.press("down")
            await pilot.pause()
            for _ in range(spec_writer_idx):
                await pilot.press("down")
                await pilot.pause()

            # Toggle OFF (remove)
            await pilot.press("space")
            await pilot.pause()

            # Toggle ON (reinstall — this copies latest bundled version)
            await pilot.press("space")
            await pilot.pause()

    # After reinstall, manifest should have updated version (0.2.0)
    from ievo.core.project import ProjectManifest

    manifest = ProjectManifest.load(tmp_path)
    assert manifest.agents.get("spec-writer") == "0.2.0"

    # Badge should not be present after reinstall since version now matches registry
    assert app._outdated.get("spec-writer") is None


async def test_no_crash_when_no_project_press_u() -> None:
    """Pressing 'u' with no project loaded causes no crash.

    REQ-008 AC: no crash when manifest is None.
    """
    app = _make_app()
    with patch("ievo.tui.app.find_project_root", return_value=None):
        async with app.run_test(size=(120, 40)) as pilot:
            await pilot.pause()
            await pilot.press("u")
            await pilot.pause()
            # No exception → test passes


# ── REQ-009: Template version toast + t keybinding ───────────


def _make_project_with_cli_version(
    root: Path,
    cli_version: str | None = None,
    agents: dict[str, str] | None = None,
) -> None:
    """Create a project with optional cli_version in manifest.

    When cli_version is None, the field is omitted (simulates old project).
    """
    if agents is None:
        agents = {}

    manifest_data = {
        "project": "test-project",
        "version": "0.1.0",
        "agents": agents,
    }
    if cli_version is not None:
        manifest_data["cli_version"] = cli_version

    (root / ".ievo").mkdir(exist_ok=True)
    (root / ".ievo" / "manifest.yaml").write_text(_yaml.dump(manifest_data, sort_keys=False))

    for name in agents:
        agent_dir = root / "agents" / name
        agent_dir.mkdir(parents=True, exist_ok=True)
        (agent_dir / "EVOLUTION_LOG.md").write_text("# Evolution Log\n\n")


async def test_template_outdated_toast_on_startup(tmp_path: Path) -> None:
    """Warning toast shown when manifest.cli_version differs from running __version__.

    REQ-009 AC-4: startup template toast when cli_version outdated.
    """
    _make_project_with_cli_version(tmp_path, cli_version="0.0.1")
    app = _make_app()
    notifications: list = []

    original_notify = IevoApp.notify

    def capture_notify(self, message, **kwargs):
        notifications.append({"message": message, **kwargs})
        return original_notify(self, message, **kwargs)

    with (
        patch("ievo.tui.app.find_project_root", return_value=tmp_path),
        patch("ievo.tui.app.__version__", "0.2.0", create=True),
        patch.object(IevoApp, "notify", capture_notify),
    ):
        async with app.run_test(size=(120, 40), notifications=True) as pilot:
            await pilot.pause()

    warning_notifications = [n for n in notifications if n.get("severity") == "warning"]
    template_warnings = [n for n in warning_notifications if "Templates" in n.get("message", "")]
    assert len(template_warnings) >= 1, (
        f"Expected template warning but got: {[n['message'] for n in warning_notifications]}"
    )
    assert "Templates may be outdated" in template_warnings[0]["message"]


async def test_no_template_toast_when_up_to_date(tmp_path: Path) -> None:
    """No template toast when manifest.cli_version matches running __version__.

    REQ-009 AC-4b: no toast when up-to-date.
    """
    from ievo.version import __version__

    _make_project_with_cli_version(tmp_path, cli_version=__version__)
    app = _make_app()
    notifications: list = []

    original_notify = IevoApp.notify

    def capture_notify(self, message, **kwargs):
        notifications.append({"message": message, **kwargs})
        return original_notify(self, message, **kwargs)

    with (
        patch("ievo.tui.app.find_project_root", return_value=tmp_path),
        patch.object(IevoApp, "notify", capture_notify),
    ):
        async with app.run_test(size=(120, 40), notifications=True) as pilot:
            await pilot.pause()

    template_warnings = [
        n
        for n in notifications
        if n.get("severity") == "warning" and "Templates" in n.get("message", "")
    ]
    assert len(template_warnings) == 0


async def test_no_template_toast_when_cli_version_empty(tmp_path: Path) -> None:
    """No template toast for old projects without cli_version field.

    REQ-009 AC-3c: empty cli_version → no nagging.
    """
    _make_project_with_cli_version(tmp_path, cli_version=None)  # omit cli_version
    app = _make_app()
    notifications: list = []

    original_notify = IevoApp.notify

    def capture_notify(self, message, **kwargs):
        notifications.append({"message": message, **kwargs})
        return original_notify(self, message, **kwargs)

    with (
        patch("ievo.tui.app.find_project_root", return_value=tmp_path),
        patch.object(IevoApp, "notify", capture_notify),
    ):
        async with app.run_test(size=(120, 40), notifications=True) as pilot:
            await pilot.pause()

    template_warnings = [
        n
        for n in notifications
        if n.get("severity") == "warning" and "Templates" in n.get("message", "")
    ]
    assert len(template_warnings) == 0


async def test_t_key_refreshes_templates(tmp_path: Path) -> None:
    """Pressing 't' copies all templates and updates cli_version in manifest.

    REQ-009 AC-5c: t refreshes templates when outdated.
    """
    from ievo.core.project import ProjectManifest
    from ievo.core.templates import TEMPLATE_MAP

    _make_project_with_cli_version(tmp_path, cli_version="0.0.1")
    app = _make_app()
    notifications: list = []

    original_notify = IevoApp.notify

    def capture_notify(self, message, **kwargs):
        notifications.append({"message": message, **kwargs})
        return original_notify(self, message, **kwargs)

    with (
        patch("ievo.tui.app.find_project_root", return_value=tmp_path),
        patch("ievo.tui.app.__version__", "0.2.0", create=True),
        patch.object(IevoApp, "notify", capture_notify),
    ):
        async with app.run_test(size=(120, 40), notifications=True) as pilot:
            await pilot.pause()
            await pilot.press("t")
            await pilot.pause()

    # All template files should now exist
    for _bundled_name, dest_rel in TEMPLATE_MAP:
        dest = tmp_path / dest_rel
        assert dest.exists(), f"Expected {dest_rel} after refresh"

    # Manifest should be updated
    manifest = ProjectManifest.load(tmp_path)
    assert manifest.cli_version == "0.2.0"

    # Success notification shown
    info_notifications = [n for n in notifications if n.get("severity") == "information"]
    refreshed_notifications = [
        n for n in info_notifications if "Templates refreshed" in n.get("message", "")
    ]
    assert len(refreshed_notifications) >= 1


async def test_t_key_when_up_to_date(tmp_path: Path) -> None:
    """Pressing 't' shows 'up to date' notification when cli_version matches.

    REQ-009 AC-5b: t when up-to-date shows informational message.
    """
    from ievo.version import __version__

    _make_project_with_cli_version(tmp_path, cli_version=__version__)
    app = _make_app()
    notifications: list = []

    original_notify = IevoApp.notify

    def capture_notify(self, message, **kwargs):
        notifications.append({"message": message, **kwargs})
        return original_notify(self, message, **kwargs)

    with (
        patch("ievo.tui.app.find_project_root", return_value=tmp_path),
        patch.object(IevoApp, "notify", capture_notify),
    ):
        async with app.run_test(size=(120, 40), notifications=True) as pilot:
            await pilot.pause()
            await pilot.press("t")
            await pilot.pause()

    info_notifications = [n for n in notifications if n.get("severity") == "information"]
    up_to_date = [n for n in info_notifications if "up to date" in n.get("message", "").lower()]
    assert len(up_to_date) >= 1


async def test_t_key_idempotent(tmp_path: Path) -> None:
    """Pressing 't' twice when up to date shows 'up to date' both times.

    REQ-009 AC-6: idempotent refresh.
    """
    from ievo.version import __version__

    _make_project_with_cli_version(tmp_path, cli_version=__version__)
    app = _make_app()
    notifications: list = []

    original_notify = IevoApp.notify

    def capture_notify(self, message, **kwargs):
        notifications.append({"message": message, **kwargs})
        return original_notify(self, message, **kwargs)

    with (
        patch("ievo.tui.app.find_project_root", return_value=tmp_path),
        patch.object(IevoApp, "notify", capture_notify),
    ):
        async with app.run_test(size=(120, 40), notifications=True) as pilot:
            await pilot.pause()
            await pilot.press("t")
            await pilot.pause()
            await pilot.press("t")
            await pilot.pause()

    info_notifications = [n for n in notifications if n.get("severity") == "information"]
    up_to_date = [n for n in info_notifications if "up to date" in n.get("message", "").lower()]
    assert len(up_to_date) >= 2


async def test_t_key_no_project_noop() -> None:
    """Pressing 't' with no project causes no crash and no notification.

    REQ-009 AC-7c: no project → t is a no-op.
    """
    app = _make_app()
    notifications: list = []

    original_notify = IevoApp.notify

    def capture_notify(self, message, **kwargs):
        notifications.append({"message": message, **kwargs})
        return original_notify(self, message, **kwargs)

    with (
        patch("ievo.tui.app.find_project_root", return_value=None),
        patch.object(IevoApp, "notify", capture_notify),
    ):
        async with app.run_test(size=(120, 40)) as pilot:
            await pilot.pause()
            await pilot.press("t")
            await pilot.pause()

    # No notification from t press (only startup messages possible, none here)
    # Since no project, watch_project_root returns early without notifying
    assert all("Templates" not in n.get("message", "") for n in notifications)


async def test_template_and_agent_toasts_coexist(tmp_path: Path) -> None:
    """Both template outdated toast and agent outdated toast appear independently.

    REQ-009 AC-4c: template toast is independent of agent toast.
    """
    # spec-writer 0.1.0 is outdated vs registry 0.2.0, AND cli_version is outdated
    _make_project_with_cli_version(tmp_path, cli_version="0.0.1", agents={"spec-writer": "0.1.0"})
    app = _make_app()
    notifications: list = []

    original_notify = IevoApp.notify

    def capture_notify(self, message, **kwargs):
        notifications.append({"message": message, **kwargs})
        return original_notify(self, message, **kwargs)

    with (
        patch("ievo.tui.app.find_project_root", return_value=tmp_path),
        patch("ievo.tui.app.__version__", "0.2.0", create=True),
        patch.object(IevoApp, "notify", capture_notify),
    ):
        async with app.run_test(size=(120, 40), notifications=True) as pilot:
            await pilot.pause()

    warning_notifications = [n for n in notifications if n.get("severity") == "warning"]
    agent_warnings = [
        n for n in warning_notifications if "agent(s) outdated" in n.get("message", "")
    ]
    template_warnings = [n for n in warning_notifications if "Templates" in n.get("message", "")]

    assert len(agent_warnings) >= 1, "Expected agent outdated toast"
    assert len(template_warnings) >= 1, "Expected template outdated toast"
