"""Tests for ievo run command."""

from __future__ import annotations

from pathlib import Path
from unittest.mock import patch

import pytest
import typer
import yaml as _yaml

from ievo.commands.run import _build_claude_command, _find_sandbox_dir, run
from ievo.core.agent import AgentModel, AgentPackage
from ievo.core.docker import DockerCheckResult, DockerStatus


def _make_agent(tmp_path: Path, model: str = "sonnet") -> AgentPackage:
    """Create agent dir with ROLE.md and memory, return package."""
    (tmp_path / "ROLE.md").write_text("# Agent\n\nInstructions here.\n")
    (tmp_path / "memory").mkdir()
    (tmp_path / "memory" / "CONTEXT.md").write_text("# CONTEXT\n\nProject: test\n")
    (tmp_path / "memory" / "DECISIONS.md").write_text("# DECISIONS\n\n")  # header-only, skip
    return AgentPackage(name="test", model=AgentModel(primary=model))


# ── _build_claude_command ────────────────────────────────────


def test_build_command_basic(tmp_path: Path) -> None:
    root = tmp_path / "project"
    root.mkdir()
    agent_dir = tmp_path / "agent"
    agent_dir.mkdir()
    pkg = _make_agent(agent_dir)

    cmd = _build_claude_command(root, agent_dir, pkg, "sonnet", None, None)

    assert cmd[0] == "claude"
    assert "--model" in cmd
    assert "sonnet" in cmd
    assert "--system-prompt" in cmd


def test_build_command_with_message(tmp_path: Path) -> None:
    root = tmp_path / "project"
    root.mkdir()
    agent_dir = tmp_path / "agent"
    agent_dir.mkdir()
    pkg = _make_agent(agent_dir)

    cmd = _build_claude_command(root, agent_dir, pkg, "sonnet", "Hello agent", None)

    assert "-p" in cmd
    assert "Hello agent" in cmd


def test_build_command_with_prompt_file(tmp_path: Path) -> None:
    root = tmp_path / "project"
    root.mkdir()
    agent_dir = tmp_path / "agent"
    agent_dir.mkdir()
    pkg = _make_agent(agent_dir)
    prompt_file = tmp_path / "prompt.txt"
    prompt_file.write_text("Do something")

    cmd = _build_claude_command(root, agent_dir, pkg, "sonnet", None, prompt_file)

    assert "-p" in cmd
    assert "Do something" in cmd


def test_build_command_model_override(tmp_path: Path) -> None:
    root = tmp_path / "project"
    root.mkdir()
    agent_dir = tmp_path / "agent"
    agent_dir.mkdir()
    pkg = _make_agent(agent_dir, model="sonnet")

    cmd = _build_claude_command(root, agent_dir, pkg, "opus", None, None)

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


def test_build_command_includes_role_md(tmp_path: Path) -> None:
    root = tmp_path / "project"
    root.mkdir()
    agent_dir = tmp_path / "agent"
    agent_dir.mkdir()
    pkg = _make_agent(agent_dir)

    cmd = _build_claude_command(root, agent_dir, pkg, "sonnet", None, None)

    idx = cmd.index("--system-prompt")
    assert "Instructions here" in cmd[idx + 1]


def test_build_command_skips_empty_memory(tmp_path: Path) -> None:
    root = tmp_path / "project"
    root.mkdir()
    agent_dir = tmp_path / "agent"
    agent_dir.mkdir()
    pkg = _make_agent(agent_dir)

    cmd = _build_claude_command(root, agent_dir, pkg, "sonnet", None, None)

    # CONTEXT.md has content, DECISIONS.md is header-only (skipped)
    if "--append-system-prompt" in cmd:
        idx = cmd.index("--append-system-prompt")
        memory_content = cmd[idx + 1]
        assert "CONTEXT" in memory_content
        assert "DECISIONS" not in memory_content


def test_build_command_no_role_md(tmp_path: Path) -> None:
    root = tmp_path / "project"
    root.mkdir()
    agent_dir = tmp_path / "agent"
    agent_dir.mkdir()
    (agent_dir / "memory").mkdir()
    pkg = AgentPackage(name="no-role")

    cmd = _build_claude_command(root, agent_dir, pkg, "sonnet", None, None)

    assert "--system-prompt" not in cmd


def test_build_command_nonstandard_model(tmp_path: Path) -> None:
    root = tmp_path / "project"
    root.mkdir()
    agent_dir = tmp_path / "agent"
    agent_dir.mkdir()
    pkg = _make_agent(agent_dir, model="custom-model")

    cmd = _build_claude_command(root, agent_dir, pkg, "custom-model", None, None)

    assert "--model" not in cmd


def test_build_command_no_memory_dir(tmp_path: Path) -> None:
    root = tmp_path / "project"
    root.mkdir()
    agent_dir = tmp_path / "agent"
    agent_dir.mkdir()
    (agent_dir / "ROLE.md").write_text("# Agent\n")
    pkg = AgentPackage(name="no-mem")

    cmd = _build_claude_command(root, agent_dir, pkg, "sonnet", None, None)

    assert "--append-system-prompt" not in cmd


# ── run() — local mode ───────────────────────────────────────


def test_run_agent_not_installed(tmp_project: Path) -> None:
    with (
        patch("ievo.commands.run.require_project", return_value=tmp_project),
        pytest.raises(typer.Exit),
    ):
        run(agent="nonexistent", message=None, prompt_file=None, model=None, local=True)


def test_run_agent_yaml_not_found(tmp_project_with_agent: Path) -> None:
    root = tmp_project_with_agent
    (root / "agents" / "spec-writer" / "agent.yaml").unlink()
    with (
        patch("ievo.commands.run.require_project", return_value=root),
        pytest.raises(typer.Exit),
    ):
        run(agent="spec-writer", message=None, prompt_file=None, model=None, local=True)


def test_run_local_success(tmp_project_with_agent: Path) -> None:
    root = tmp_project_with_agent
    with (
        patch("ievo.commands.run.require_project", return_value=root),
        patch("subprocess.run") as mock_sub,
    ):
        run(agent="spec-writer", message=None, prompt_file=None, model=None, local=True)
        mock_sub.assert_called_once()
        cmd = mock_sub.call_args[0][0]
        assert cmd[0] == "claude"


def test_run_local_model_override_cli(tmp_project_with_agent: Path) -> None:
    root = tmp_project_with_agent
    with (
        patch("ievo.commands.run.require_project", return_value=root),
        patch("subprocess.run"),
    ):
        run(agent="spec-writer", message=None, prompt_file=None, model="opus", local=True)


def test_run_model_override_from_manifest(tmp_project_with_agent: Path) -> None:
    root = tmp_project_with_agent
    manifest_data = _yaml.safe_load((root / "ievo.yaml").read_text())
    manifest_data["overrides"] = {"spec-writer": {"model": {"primary": "opus"}}}
    (root / "ievo.yaml").write_text(_yaml.dump(manifest_data, sort_keys=False))

    with (
        patch("ievo.commands.run.require_project", return_value=root),
        patch("subprocess.run"),
    ):
        run(agent="spec-writer", message=None, prompt_file=None, model=None, local=True)


def test_run_with_mcp_warnings(tmp_project_with_agent: Path) -> None:
    root = tmp_project_with_agent
    with (
        patch("ievo.commands.run.require_project", return_value=root),
        patch("ievo.commands.run.ensure_mcp_configured", return_value=["Missing API_KEY"]),
        patch("subprocess.run"),
    ):
        run(agent="spec-writer", message=None, prompt_file=None, model=None, local=True)


def test_run_with_plugin_warnings(tmp_project_with_agent: Path) -> None:
    root = tmp_project_with_agent
    agent_dir = root / "agents" / "spec-writer"
    agent_data = _yaml.safe_load((agent_dir / "agent.yaml").read_text())
    agent_data["plugins"] = [{"name": "claude-mem", "marketplace": "thedotmack", "required": True}]
    (agent_dir / "agent.yaml").write_text(_yaml.dump(agent_data, sort_keys=False))

    with (
        patch("ievo.commands.run.require_project", return_value=root),
        patch("subprocess.run"),
    ):
        run(agent="spec-writer", message=None, prompt_file=None, model=None, local=True)


def test_run_local_subprocess_file_not_found(tmp_project_with_agent: Path) -> None:
    root = tmp_project_with_agent
    with (
        patch("ievo.commands.run.require_project", return_value=root),
        patch("subprocess.run", side_effect=FileNotFoundError),
        pytest.raises(typer.Exit),
    ):
        run(agent="spec-writer", message=None, prompt_file=None, model=None, local=True)


def test_run_local_keyboard_interrupt(tmp_project_with_agent: Path) -> None:
    root = tmp_project_with_agent
    with (
        patch("ievo.commands.run.require_project", return_value=root),
        patch("subprocess.run", side_effect=KeyboardInterrupt),
    ):
        run(agent="spec-writer", message=None, prompt_file=None, model=None, local=True)


def test_run_local_with_message(tmp_project_with_agent: Path) -> None:
    root = tmp_project_with_agent
    with (
        patch("ievo.commands.run.require_project", return_value=root),
        patch("subprocess.run") as mock_sub,
    ):
        run(agent="spec-writer", message="Hello agent", prompt_file=None, model=None, local=True)
        cmd = mock_sub.call_args[0][0]
        assert "-p" in cmd
        assert "Hello agent" in cmd


def test_run_no_mcp_deps(tmp_project_with_agent: Path) -> None:
    root = tmp_project_with_agent
    agent_dir = root / "agents" / "spec-writer"
    agent_data = _yaml.safe_load((agent_dir / "agent.yaml").read_text())
    agent_data["mcp"] = []
    (agent_dir / "agent.yaml").write_text(_yaml.dump(agent_data, sort_keys=False))

    with (
        patch("ievo.commands.run.require_project", return_value=root),
        patch("subprocess.run"),
    ):
        run(agent="spec-writer", message=None, prompt_file=None, model=None, local=True)


def test_run_non_required_plugin(tmp_project_with_agent: Path) -> None:
    root = tmp_project_with_agent
    agent_dir = root / "agents" / "spec-writer"
    agent_data = _yaml.safe_load((agent_dir / "agent.yaml").read_text())
    agent_data["plugins"] = [{"name": "optional-plugin", "marketplace": "test", "required": False}]
    (agent_dir / "agent.yaml").write_text(_yaml.dump(agent_data, sort_keys=False))

    with (
        patch("ievo.commands.run.require_project", return_value=root),
        patch("subprocess.run"),
    ):
        run(agent="spec-writer", message=None, prompt_file=None, model=None, local=True)


def test_run_override_exists_but_no_model_primary(tmp_project_with_agent: Path) -> None:
    root = tmp_project_with_agent
    manifest_data = _yaml.safe_load((root / "ievo.yaml").read_text())
    manifest_data["overrides"] = {"spec-writer": {"some_other_setting": True}}
    (root / "ievo.yaml").write_text(_yaml.dump(manifest_data, sort_keys=False))

    with (
        patch("ievo.commands.run.require_project", return_value=root),
        patch("subprocess.run") as mock_sub,
    ):
        run(agent="spec-writer", message=None, prompt_file=None, model=None, local=True)
        cmd = mock_sub.call_args[0][0]
        idx = cmd.index("--model")
        assert cmd[idx + 1] == "sonnet"


# ── run() — Docker mode ─────────────────────────────────────


def test_run_docker_success(tmp_project_with_agent: Path) -> None:
    root = tmp_project_with_agent
    with (
        patch("ievo.commands.run.require_project", return_value=root),
        patch(
            "ievo.commands.run.check_docker",
            return_value=DockerCheckResult(status=DockerStatus.READY, message="Docker 24.0"),
        ),
        patch("ievo.commands.run.image_exists", return_value=True),
        patch("subprocess.run") as mock_sub,
    ):
        run(agent="spec-writer", message=None, prompt_file=None, model=None, local=False)
        mock_sub.assert_called_once()
        cmd = mock_sub.call_args[0][0]
        assert cmd[0] == "docker"


def test_run_docker_not_installed_fallback(tmp_project_with_agent: Path) -> None:
    root = tmp_project_with_agent
    with (
        patch("ievo.commands.run.require_project", return_value=root),
        patch(
            "ievo.commands.run.check_docker",
            return_value=DockerCheckResult(status=DockerStatus.NOT_INSTALLED, message="not found"),
        ),
        patch("subprocess.run") as mock_sub,
    ):
        run(agent="spec-writer", message=None, prompt_file=None, model=None, local=False)
        cmd = mock_sub.call_args[0][0]
        assert cmd[0] == "claude"


def test_run_docker_not_running_fallback(tmp_project_with_agent: Path) -> None:
    root = tmp_project_with_agent
    with (
        patch("ievo.commands.run.require_project", return_value=root),
        patch(
            "ievo.commands.run.check_docker",
            return_value=DockerCheckResult(
                status=DockerStatus.NOT_RUNNING, message="daemon stopped"
            ),
        ),
        patch("subprocess.run") as mock_sub,
    ):
        run(agent="spec-writer", message=None, prompt_file=None, model=None, local=False)
        cmd = mock_sub.call_args[0][0]
        assert cmd[0] == "claude"


def test_run_docker_auto_build_success(tmp_project_with_agent: Path) -> None:
    root = tmp_project_with_agent
    (root / "sandbox").mkdir()
    (root / "sandbox" / "Dockerfile").write_text("FROM python:3.13-slim\n")

    with (
        patch("ievo.commands.run.require_project", return_value=root),
        patch(
            "ievo.commands.run.check_docker",
            return_value=DockerCheckResult(status=DockerStatus.READY, message="Docker 24.0"),
        ),
        patch("ievo.commands.run.image_exists", return_value=False),
        patch("ievo.commands.run.build_sandbox_image", return_value=True) as mock_build,
        patch("subprocess.run") as mock_sub,
    ):
        run(agent="spec-writer", message=None, prompt_file=None, model=None, local=False)
        mock_build.assert_called_once()
        cmd = mock_sub.call_args[0][0]
        assert cmd[0] == "docker"


def test_run_docker_auto_build_failure_fallback(tmp_project_with_agent: Path) -> None:
    root = tmp_project_with_agent
    (root / "sandbox").mkdir()
    (root / "sandbox" / "Dockerfile").write_text("FROM python:3.13-slim\n")

    with (
        patch("ievo.commands.run.require_project", return_value=root),
        patch(
            "ievo.commands.run.check_docker",
            return_value=DockerCheckResult(status=DockerStatus.READY, message="Docker 24.0"),
        ),
        patch("ievo.commands.run.image_exists", return_value=False),
        patch("ievo.commands.run.build_sandbox_image", return_value=False),
        patch("subprocess.run") as mock_sub,
    ):
        run(agent="spec-writer", message=None, prompt_file=None, model=None, local=False)
        cmd = mock_sub.call_args[0][0]
        assert cmd[0] == "claude"


def test_run_docker_no_dockerfile_fallback(tmp_project_with_agent: Path) -> None:
    root = tmp_project_with_agent
    with (
        patch("ievo.commands.run.require_project", return_value=root),
        patch(
            "ievo.commands.run.check_docker",
            return_value=DockerCheckResult(status=DockerStatus.READY, message="Docker 24.0"),
        ),
        patch("ievo.commands.run.image_exists", return_value=False),
        patch("ievo.commands.run._find_sandbox_dir", return_value=None),
        patch("subprocess.run") as mock_sub,
    ):
        run(agent="spec-writer", message=None, prompt_file=None, model=None, local=False)
        cmd = mock_sub.call_args[0][0]
        assert cmd[0] == "claude"


def test_run_docker_file_not_found(tmp_project_with_agent: Path) -> None:
    root = tmp_project_with_agent
    with (
        patch("ievo.commands.run.require_project", return_value=root),
        patch(
            "ievo.commands.run.check_docker",
            return_value=DockerCheckResult(status=DockerStatus.READY, message="Docker 24.0"),
        ),
        patch("ievo.commands.run.image_exists", return_value=True),
        patch("subprocess.run", side_effect=FileNotFoundError),
        pytest.raises(typer.Exit),
    ):
        run(agent="spec-writer", message=None, prompt_file=None, model=None, local=False)


def test_run_docker_keyboard_interrupt(tmp_project_with_agent: Path) -> None:
    root = tmp_project_with_agent
    with (
        patch("ievo.commands.run.require_project", return_value=root),
        patch(
            "ievo.commands.run.check_docker",
            return_value=DockerCheckResult(status=DockerStatus.READY, message="Docker 24.0"),
        ),
        patch("ievo.commands.run.image_exists", return_value=True),
        patch("subprocess.run", side_effect=KeyboardInterrupt),
    ):
        run(agent="spec-writer", message=None, prompt_file=None, model=None, local=False)


# ── _find_sandbox_dir ────────────────────────────────────────


def test_find_sandbox_dir_in_project(tmp_path: Path) -> None:
    root = tmp_path / "project"
    root.mkdir()
    sandbox = root / "sandbox"
    sandbox.mkdir()
    (sandbox / "Dockerfile").write_text("FROM python:3.13\n")

    result = _find_sandbox_dir(root)
    assert result == sandbox


def test_find_sandbox_dir_not_found(tmp_path: Path) -> None:
    root = tmp_path / "project"
    root.mkdir()
    result = _find_sandbox_dir(root)
    # May find the cli repo's sandbox or return None
    # depending on whether we're running from the cli repo
    # Either way, don't assert None — just verify it returns Path or None
    assert result is None or isinstance(result, Path)


def test_find_sandbox_dir_falls_back_to_package(tmp_path: Path) -> None:
    """When project root has no sandbox/, fall back to cli repo's sandbox/."""
    root = tmp_path / "project"
    root.mkdir()
    # No sandbox/ in project root — should fall back to package-relative path
    result = _find_sandbox_dir(root)
    # The cli repo has sandbox/Dockerfile, so this should find it
    if result is not None:
        assert (result / "Dockerfile").exists()
