"""Tests for ievo.tui.app — TUI dashboard."""

from __future__ import annotations

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

import yaml as _yaml

from ievo.core.project import ProjectManifest
from ievo.tui.app import LOGO, MODEL_COLORS, IevoApp, run_tui

# ── Constants ────────────────────────────────────────────────


def test_logo_is_string() -> None:
    assert isinstance(LOGO, str)
    assert "iEvo" in LOGO or "██" in LOGO


def test_model_colors() -> None:
    assert MODEL_COLORS["opus"] == "red"
    assert MODEL_COLORS["sonnet"] == "magenta"
    assert MODEL_COLORS["haiku"] == "cyan"


# ── IevoApp class ────────────────────────────────────────────


def test_ievo_app_creation() -> None:
    """IevoApp can be instantiated."""
    app = IevoApp()
    assert app.TITLE == "iEvo"
    assert app.SUB_TITLE == "Self-evolving AI agent framework"


def test_ievo_app_bindings() -> None:
    """Verify expected key bindings exist."""
    app = IevoApp()
    binding_keys = {b.key for b in app.BINDINGS}
    assert "q" in binding_keys
    assert "r" in binding_keys
    assert "a" in binding_keys


def test_ievo_app_css_exists() -> None:
    """CSS is not empty."""
    assert IevoApp.CSS
    assert "Screen" in IevoApp.CSS


# ── run_tui ──────────────────────────────────────────────────


def test_run_tui_calls_app_run() -> None:
    """run_tui creates an IevoApp and calls run()."""
    with patch("ievo.tui.app.IevoApp") as mock_cls:
        mock_app = MagicMock()
        mock_cls.return_value = mock_app
        run_tui()
        mock_app.run.assert_called_once()


# ── _load_agents ─────────────────────────────────────────────


def test_load_agents_with_agents(tmp_path: Path) -> None:
    """_load_agents populates DataTable with agent info."""
    _setup_project(tmp_path)
    app = IevoApp()
    mock_table = MagicMock()
    app.query_one = MagicMock(return_value=mock_table)

    manifest = ProjectManifest.load(tmp_path)
    app._load_agents(tmp_path, manifest)

    mock_table.clear.assert_called_once_with(columns=True)
    mock_table.add_columns.assert_called_once()
    mock_table.add_row.assert_called_once()


def test_load_agents_empty(tmp_path: Path) -> None:
    """_load_agents shows 'no agents' for empty manifest."""
    manifest_data = {"project": "test", "version": "0.1.0", "agents": {}}
    (tmp_path / "ievo.yaml").write_text(_yaml.dump(manifest_data, sort_keys=False))

    app = IevoApp()
    mock_table = MagicMock()
    app.query_one = MagicMock(return_value=mock_table)

    manifest = ProjectManifest.load(tmp_path)
    app._load_agents(tmp_path, manifest)

    # Should add a "no agents" row
    mock_table.add_row.assert_called_once()
    call_args = mock_table.add_row.call_args[0]
    assert "no agents" in call_args[0]


def test_load_agents_missing_yaml(tmp_path: Path) -> None:
    """Agent with missing agent.yaml shows defaults."""
    manifest_data = {"project": "test", "version": "0.1.0", "agents": {"broken": "0.1.0"}}
    (tmp_path / "ievo.yaml").write_text(_yaml.dump(manifest_data, sort_keys=False))
    (tmp_path / "agents" / "broken").mkdir(parents=True)

    app = IevoApp()
    mock_table = MagicMock()
    app.query_one = MagicMock(return_value=mock_table)

    manifest = ProjectManifest.load(tmp_path)
    app._load_agents(tmp_path, manifest)

    mock_table.add_row.assert_called_once()


def test_load_agents_with_evo_log(tmp_path: Path) -> None:
    """Agent with evolution entries shows count."""
    _setup_project(tmp_path)
    evo_log = tmp_path / "agents" / "spec-writer" / "EVOLUTION_LOG.md"
    evo_log.write_text(
        "# Evolution Log\n\n## E-001: First\n\nDetails.\n\n## E-002: Second\n\nMore.\n"
    )

    app = IevoApp()
    mock_table = MagicMock()
    app.query_one = MagicMock(return_value=mock_table)

    manifest = ProjectManifest.load(tmp_path)
    app._load_agents(tmp_path, manifest)

    call_args = mock_table.add_row.call_args[0]
    # Fourth argument is evo count
    assert call_args[3] == "2"


# ── _load_pipeline ───────────────────────────────────────────


def test_load_pipeline_with_agents(tmp_path: Path) -> None:
    """Pipeline shows flow diagram."""
    _setup_project(tmp_path)
    app = IevoApp()
    mock_static = MagicMock()
    app.query_one = MagicMock(return_value=mock_static)

    manifest = ProjectManifest.load(tmp_path)
    app._load_pipeline(manifest)

    mock_static.update.assert_called_once()
    content = mock_static.update.call_args[0][0]
    assert "SDD Pipeline" in content
    assert "spec-writer" in content


def test_load_pipeline_empty() -> None:
    """Empty manifest shows 'no agents installed'."""
    app = IevoApp()
    mock_static = MagicMock()
    app.query_one = MagicMock(return_value=mock_static)

    manifest = ProjectManifest(project="test")
    app._load_pipeline(manifest)

    mock_static.update.assert_called_once()
    content = mock_static.update.call_args[0][0]
    assert "No agents installed" in content


def test_load_pipeline_multiple_agents(tmp_path: Path) -> None:
    """Pipeline with multiple agents shows flow with arrows."""
    manifest_data = {
        "project": "test",
        "version": "0.1.0",
        "agents": {"spec-writer": "0.1.0", "coder": "0.1.0", "architect": "0.1.0"},
    }
    (tmp_path / "ievo.yaml").write_text(_yaml.dump(manifest_data, sort_keys=False))

    app = IevoApp()
    mock_static = MagicMock()
    app.query_one = MagicMock(return_value=mock_static)

    manifest = ProjectManifest.load(tmp_path)
    app._load_pipeline(manifest)

    content = mock_static.update.call_args[0][0]
    assert "YOU" in content


# ── _load_evolution ──────────────────────────────────────────


def test_load_evolution_with_entries(tmp_path: Path) -> None:
    """Evolution log entries are displayed."""
    _setup_project(tmp_path)
    evo_log = tmp_path / "agents" / "spec-writer" / "EVOLUTION_LOG.md"
    evo_log.write_text("# Evolution Log\n\n## E-001: Fixed prompt\n\nDetails.\n")

    app = IevoApp()
    mock_log = MagicMock()
    app.query_one = MagicMock(return_value=mock_log)

    manifest = ProjectManifest.load(tmp_path)
    app._load_evolution(tmp_path, manifest)

    assert mock_log.write.call_count >= 1


def test_load_evolution_empty(tmp_path: Path) -> None:
    """No evolution entries shows placeholder."""
    _setup_project(tmp_path)
    # Default EVOLUTION_LOG.md has only header

    app = IevoApp()
    mock_log = MagicMock()
    app.query_one = MagicMock(return_value=mock_log)

    manifest = ProjectManifest.load(tmp_path)
    app._load_evolution(tmp_path, manifest)

    # Should write "No evolution entries" message
    calls = [str(c) for c in mock_log.write.call_args_list]
    assert any("No evolution" in c for c in calls)


def test_load_evolution_no_evo_file(tmp_path: Path) -> None:
    """Missing EVOLUTION_LOG.md shows placeholder."""
    manifest_data = {"project": "test", "version": "0.1.0", "agents": {"broken": "0.1.0"}}
    (tmp_path / "ievo.yaml").write_text(_yaml.dump(manifest_data, sort_keys=False))
    (tmp_path / "agents" / "broken").mkdir(parents=True)

    app = IevoApp()
    mock_log = MagicMock()
    app.query_one = MagicMock(return_value=mock_log)

    manifest = ProjectManifest.load(tmp_path)
    app._load_evolution(tmp_path, manifest)


# ── _load_file_tree ──────────────────────────────────────────


def test_load_file_tree(tmp_path: Path) -> None:
    """File tree is populated from project directory."""
    (tmp_path / "src").mkdir()
    (tmp_path / "src" / "main.py").write_text("# main")
    (tmp_path / "agents").mkdir()

    app = IevoApp()
    mock_tree = MagicMock()
    mock_root = MagicMock()
    mock_tree.root = mock_root
    app.query_one = MagicMock(return_value=mock_tree)

    app._load_file_tree(tmp_path)

    mock_tree.clear.assert_called_once()
    mock_root.set_label.assert_called_once()
    mock_root.expand.assert_called_once()


# ── _add_path_to_tree ────────────────────────────────────────


def test_add_path_to_tree(tmp_path: Path) -> None:
    """Directories and files are added to tree node."""
    (tmp_path / "dir1").mkdir()
    (tmp_path / "dir1" / "file1.py").write_text("x")
    (tmp_path / "file2.txt").write_text("y")
    (tmp_path / ".hidden").write_text("z")  # should be skipped

    app = IevoApp()
    mock_node = MagicMock()
    mock_branch = MagicMock()
    mock_node.add.return_value = mock_branch

    app._add_path_to_tree(mock_node, tmp_path, depth=0, max_depth=2)

    # dir1 should be added as branch, file2.txt as leaf, .hidden skipped
    mock_node.add.assert_called()  # directory
    mock_node.add_leaf.assert_called()  # file


def test_add_path_to_tree_max_depth(tmp_path: Path) -> None:
    """Stops at max_depth."""
    (tmp_path / "deep").mkdir()

    app = IevoApp()
    mock_node = MagicMock()

    app._add_path_to_tree(mock_node, tmp_path, depth=3, max_depth=3)

    mock_node.add.assert_not_called()
    mock_node.add_leaf.assert_not_called()


def test_add_path_to_tree_permission_error(tmp_path: Path) -> None:
    """PermissionError is handled gracefully."""
    app = IevoApp()
    mock_node = MagicMock()

    with patch("pathlib.Path.iterdir", side_effect=PermissionError):
        app._add_path_to_tree(mock_node, tmp_path, depth=0, max_depth=2)

    mock_node.add.assert_not_called()


# ── watch_project_root ───────────────────────────────────────


def test_watch_project_root_none() -> None:
    """None root shows 'no project' message."""
    app = IevoApp()
    mock_info = MagicMock()
    app.query_one = MagicMock(return_value=mock_info)

    app.watch_project_root(None)

    mock_info.update.assert_called_once()
    assert "No project" in mock_info.update.call_args[0][0]


def test_watch_project_root_with_project(tmp_path: Path) -> None:
    """Valid root loads all data."""
    _setup_project(tmp_path)

    app = IevoApp()
    mock_widget = MagicMock()
    mock_widget.root = MagicMock()
    app.query_one = MagicMock(return_value=mock_widget)

    app.watch_project_root(tmp_path)

    # query_one is called multiple times (project-info, agents-table, pipeline, evo-log, tree)
    assert app.query_one.call_count >= 5


# ── action methods ───────────────────────────────────────────


def test_action_tab() -> None:
    """action_tab sets active tab."""
    app = IevoApp()
    mock_tabs = MagicMock()
    app.query_one = MagicMock(return_value=mock_tabs)

    app.action_tab("pipeline")

    assert mock_tabs.active == "pipeline"


def test_action_refresh_data() -> None:
    """action_refresh_data calls find_project_root."""
    app = IevoApp()
    with (
        patch("ievo.tui.app.find_project_root", return_value=None) as mock_find,
        patch.object(app, "watch_project_root"),
    ):
        app.action_refresh_data()
        mock_find.assert_called_once()


# ── compose / on_mount (Textual lifecycle) ──────────────────


def test_compose_and_on_mount() -> None:
    """Full Textual lifecycle covers compose() and on_mount()."""
    import asyncio

    async def _run() -> None:
        app = IevoApp()
        async with app.run_test(size=(120, 40)) as pilot:  # noqa: F841
            assert app.title == "iEvo"
            # compose yields widgets — verify they exist
            agents_table = app.query_one("#agents-table")
            assert agents_table is not None
            pipeline_view = app.query_one("#pipeline-view")
            assert pipeline_view is not None

    asyncio.run(_run())


# ── Helper ───────────────────────────────────────────────────


def _setup_project(root: Path) -> None:
    """Create a minimal project for TUI tests."""
    manifest_data = {
        "project": "test-project",
        "version": "0.1.0",
        "agents": {"spec-writer": "0.1.0"},
    }
    (root / "ievo.yaml").write_text(_yaml.dump(manifest_data, sort_keys=False))

    agent_dir = root / "agents" / "spec-writer"
    agent_dir.mkdir(parents=True)
    agent_yaml = {
        "name": "spec-writer",
        "description": "Converts features into atomic requirements",
        "model": {"primary": "sonnet"},
    }
    (agent_dir / "agent.yaml").write_text(_yaml.dump(agent_yaml, sort_keys=False))
    (agent_dir / "EVOLUTION_LOG.md").write_text("# Evolution Log\n\n")
