"""Tests for ievo orchestrate command."""

from __future__ import annotations

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

import pytest
import typer
import yaml as _yaml

from ievo.commands.orchestrate import (
    _build_orchestrator_command,
    _count_open_questions,
    _count_ready,
    _list_questions,
    _print_summary,
    orchestrate,
)
from ievo.core.agent import AgentModel, AgentPackage


def test_count_ready_with_entries(tmp_path: Path) -> None:
    spec_index = tmp_path / "SPEC_INDEX.md"
    spec_index.write_text(
        "| REQ-001 | Auth | ready | high |\n"
        "| REQ-002 | Profile | implemented | medium |\n"
        "| REQ-003 | Payments | ready | critical |\n"
    )
    assert _count_ready(spec_index) == 2


def test_count_ready_none(tmp_path: Path) -> None:
    spec_index = tmp_path / "SPEC_INDEX.md"
    spec_index.write_text("| REQ-001 | Auth | implemented | high |\n")
    assert _count_ready(spec_index) == 0


def test_count_ready_missing_file(tmp_path: Path) -> None:
    assert _count_ready(tmp_path / "nonexistent.md") == 0


def test_count_open_questions_with_questions(tmp_path: Path) -> None:
    q_dir = tmp_path / "spec" / "questions"
    q_dir.mkdir(parents=True)
    (q_dir / "Q-001.md").write_text("# Q-001\nStatus: open\n")
    (q_dir / "Q-002.md").write_text("# Q-002\nStatus: resolved\n")
    (q_dir / "Q-003.md").write_text("# Q-003\nStatus: open\n")
    assert _count_open_questions(tmp_path) == 2


def test_count_open_questions_no_dir(tmp_path: Path) -> None:
    assert _count_open_questions(tmp_path) == 0


def test_count_open_questions_empty(tmp_path: Path) -> None:
    (tmp_path / "spec" / "questions").mkdir(parents=True)
    assert _count_open_questions(tmp_path) == 0


def test_build_orchestrator_command(tmp_path: Path) -> None:
    pkg = AgentPackage(name="coder", model=AgentModel(primary="sonnet"))
    cmd = _build_orchestrator_command(tmp_path, tmp_path / "agents" / "coder", pkg, "sonnet")

    assert cmd[0] == "claude"
    assert "-p" in cmd
    assert "--model" in cmd
    assert "sonnet" in cmd
    # Prompt should contain orchestration instructions
    prompt = cmd[-1]
    assert "ROLE.md" in prompt
    assert "iEVO.md" in prompt
    assert "SPEC_INDEX.md" in prompt


def test_build_orchestrator_command_opus(tmp_path: Path) -> None:
    pkg = AgentPackage(name="coder")
    cmd = _build_orchestrator_command(tmp_path, tmp_path / "agents" / "coder", pkg, "opus")

    idx = cmd.index("--model")
    assert cmd[idx + 1] == "opus"


def test_build_orchestrator_command_nonstandard_model(tmp_path: Path) -> None:
    """Non-standard model skips --model flag."""
    pkg = AgentPackage(name="coder")
    cmd = _build_orchestrator_command(tmp_path, tmp_path / "agents" / "coder", pkg, "custom-model")
    assert "--model" not in cmd


def test_print_summary_no_crash(tmp_path: Path, capsys) -> None:
    """Smoke test: _print_summary should not crash."""
    spec_index = tmp_path / "SPEC_INDEX.md"
    spec_index.write_text("| REQ-001 | ready |\n| REQ-002 | implemented |\n| REQ-003 | blocked |\n")
    # Should not raise
    _print_summary(spec_index, tmp_path)


def test_print_summary_missing_file(tmp_path: Path) -> None:
    _print_summary(tmp_path / "missing.md", tmp_path)  # should not raise


# ── _list_questions ──────────────────────────────────────────


def test_list_questions_no_dir(tmp_path: Path) -> None:
    """No questions dir — returns without error."""
    _list_questions(tmp_path)


def test_list_questions_with_open(tmp_path: Path) -> None:
    """Prints only open questions."""
    q_dir = tmp_path / "spec" / "questions"
    q_dir.mkdir(parents=True)
    (q_dir / "Q-001.md").write_text("# Q-001\nStatus: open\n")
    (q_dir / "Q-002.md").write_text("# Q-002\nStatus: resolved\n")
    _list_questions(tmp_path)  # should print Q-001 only


# ── Helper: create orchestration project ─────────────────────


def _make_orch_project(
    tmp_path: Path,
    ready_count: int = 1,
    agent_name: str = "coder",
) -> Path:
    """Create a project with agent and spec index for orchestration tests."""
    root = tmp_path
    manifest = {
        "project": "test",
        "version": "0.1.0",
        "agents": {agent_name: "0.1.0"},
    }
    (root / ".ievo").mkdir(exist_ok=True)
    (root / ".ievo/manifest.yaml").write_text(_yaml.dump(manifest, sort_keys=False))

    agent_dir = root / "agents" / agent_name
    agent_dir.mkdir(parents=True)
    agent_yaml = {"name": agent_name, "model": {"primary": "sonnet"}}
    (agent_dir / "agent.yaml").write_text(_yaml.dump(agent_yaml, sort_keys=False))

    spec_dir = root / "spec"
    spec_dir.mkdir(parents=True, exist_ok=True)
    lines = []
    for i in range(ready_count):
        lines.append(f"| REQ-{i + 1:03d} | Feature {i + 1} | ready | high |")
    if lines:
        (spec_dir / "SPEC_INDEX.md").write_text("\n".join(lines) + "\n")
    else:
        (spec_dir / "SPEC_INDEX.md").write_text("| REQ-001 | Done | implemented | high |\n")

    return root


# ── orchestrate() entry point tests ──────────────────────────


def test_orchestrate_agent_not_installed(tmp_path: Path) -> None:
    """Agent not in manifest raises Exit."""
    root = _make_orch_project(tmp_path, agent_name="coder")
    with (
        patch("ievo.commands.orchestrate.require_project", return_value=root),
        pytest.raises(typer.Exit),
    ):
        orchestrate(
            max_iterations=1,
            pause=0,
            stop_on_block=True,
            agent="nonexistent",
            model=None,
        )


def test_orchestrate_spec_index_missing(tmp_path: Path) -> None:
    """Missing SPEC_INDEX.md raises Exit."""
    root = _make_orch_project(tmp_path)
    (root / "spec" / "SPEC_INDEX.md").unlink()

    with (
        patch("ievo.commands.orchestrate.require_project", return_value=root),
        pytest.raises(typer.Exit),
    ):
        orchestrate(
            max_iterations=1,
            pause=0,
            stop_on_block=True,
            agent="coder",
            model=None,
        )


def test_orchestrate_agent_yaml_not_found(tmp_path: Path) -> None:
    """Missing agent.yaml raises Exit."""
    root = _make_orch_project(tmp_path)
    (root / "agents" / "coder" / "agent.yaml").unlink()

    with (
        patch("ievo.commands.orchestrate.require_project", return_value=root),
        pytest.raises(typer.Exit),
    ):
        orchestrate(
            max_iterations=1,
            pause=0,
            stop_on_block=True,
            agent="coder",
            model=None,
        )


def test_orchestrate_no_ready_reqs(tmp_path: Path) -> None:
    """No ready requirements raises Exit(0)."""
    root = _make_orch_project(tmp_path, ready_count=0)

    with (
        patch("ievo.commands.orchestrate.require_project", return_value=root),
        pytest.raises(typer.Exit),
    ):
        orchestrate(
            max_iterations=1,
            pause=0,
            stop_on_block=True,
            agent="coder",
            model=None,
        )


def test_orchestrate_runs_loop(tmp_path: Path) -> None:
    """Happy path: subprocess is called for each ready req."""
    root = _make_orch_project(tmp_path, ready_count=1)

    mock_result = MagicMock()
    mock_result.returncode = 0

    with (
        patch("ievo.commands.orchestrate.require_project", return_value=root),
        patch("subprocess.run", return_value=mock_result) as mock_sub,
    ):
        orchestrate(
            max_iterations=1,
            pause=0,
            stop_on_block=True,
            agent="coder",
            model=None,
        )
        mock_sub.assert_called_once()


def test_orchestrate_subprocess_not_found(tmp_path: Path) -> None:
    """Claude CLI not found raises Exit."""
    root = _make_orch_project(tmp_path, ready_count=1)

    with (
        patch("ievo.commands.orchestrate.require_project", return_value=root),
        patch("subprocess.run", side_effect=FileNotFoundError),
        pytest.raises(typer.Exit),
    ):
        orchestrate(
            max_iterations=1,
            pause=0,
            stop_on_block=True,
            agent="coder",
            model=None,
        )


def test_orchestrate_agent_error_code(tmp_path: Path) -> None:
    """Non-zero return code breaks the loop."""
    root = _make_orch_project(tmp_path, ready_count=2)

    mock_result = MagicMock()
    mock_result.returncode = 1

    with (
        patch("ievo.commands.orchestrate.require_project", return_value=root),
        patch("subprocess.run", return_value=mock_result),
    ):
        orchestrate(
            max_iterations=5,
            pause=0,
            stop_on_block=True,
            agent="coder",
            model=None,
        )


def test_orchestrate_stop_on_block(tmp_path: Path) -> None:
    """Open questions with stop_on_block stops the loop."""
    root = _make_orch_project(tmp_path, ready_count=2)

    # Create an open question
    q_dir = root / "spec" / "questions"
    q_dir.mkdir(parents=True)
    (q_dir / "Q-001.md").write_text("# Q-001\nStatus: open\n")

    mock_result = MagicMock()
    mock_result.returncode = 0

    with (
        patch("ievo.commands.orchestrate.require_project", return_value=root),
        patch("subprocess.run", return_value=mock_result) as mock_sub,
    ):
        orchestrate(
            max_iterations=5,
            pause=0,
            stop_on_block=True,
            agent="coder",
            model=None,
        )
        # Should have stopped after first iteration
        assert mock_sub.call_count == 1


def test_orchestrate_no_more_ready_breaks(tmp_path: Path) -> None:
    """Loop exits early when no more ready requirements."""
    root = _make_orch_project(tmp_path, ready_count=1)
    spec_index = root / "spec" / "SPEC_INDEX.md"

    call_count = 0

    def mock_subprocess(*args, **kwargs):
        nonlocal call_count
        call_count += 1
        # After first run, mark requirement as implemented
        spec_index.write_text("| REQ-001 | Feature 1 | implemented | high |\n")
        result = MagicMock()
        result.returncode = 0
        return result

    with (
        patch("ievo.commands.orchestrate.require_project", return_value=root),
        patch("subprocess.run", side_effect=mock_subprocess),
    ):
        orchestrate(
            max_iterations=5,
            pause=0,
            stop_on_block=True,
            agent="coder",
            model=None,
        )
    assert call_count == 1


def test_orchestrate_pause_between_iterations(tmp_path: Path) -> None:
    """time.sleep is called between iterations."""
    root = _make_orch_project(tmp_path, ready_count=3)

    mock_result = MagicMock()
    mock_result.returncode = 0

    with (
        patch("ievo.commands.orchestrate.require_project", return_value=root),
        patch("subprocess.run", return_value=mock_result),
        patch("ievo.commands.orchestrate.time.sleep") as mock_sleep,
    ):
        orchestrate(
            max_iterations=2,
            pause=3,
            stop_on_block=False,
            agent="coder",
            model=None,
        )
        # Sleep should be called between iterations (at least once)
        mock_sleep.assert_called_with(3)


def test_orchestrate_shows_deprecation_warning(tmp_path: Path) -> None:
    root = _make_orch_project(tmp_path, ready_count=0)

    with (
        patch("ievo.commands.orchestrate.require_project", return_value=root),
        patch("ievo.utils.deprecation.warn") as mock_warn,
        pytest.raises(typer.Exit),
    ):
        orchestrate(
            max_iterations=1,
            pause=0,
            stop_on_block=True,
            agent="coder",
            model=None,
        )

    mock_warn.assert_called_once()
    assert "deprecated" in mock_warn.call_args[0][0]
