"""Tests for the ml-ralph CLI commands."""

import os
from unittest.mock import patch

import pytest
from typer.testing import CliRunner

from ml_ralph_cli.cli import app

runner = CliRunner()


class TestInitCommand:
    """Tests for the 'ml-ralph init' command."""

    def test_init_creates_ml_ralph_dir(self, temp_dir):
        """init creates .ml-ralph directory."""
        os.chdir(temp_dir)
        result = runner.invoke(app, ["init"])
        assert result.exit_code == 0
        assert (temp_dir / ".ml-ralph").exists()

    def test_init_creates_ralph_md(self, temp_dir):
        """init creates .ml-ralph/RALPH.md."""
        os.chdir(temp_dir)
        result = runner.invoke(app, ["init"])
        assert result.exit_code == 0
        assert (temp_dir / ".ml-ralph" / "RALPH.md").exists()

    def test_init_creates_claude_md(self, temp_dir):
        """init creates CLAUDE.md."""
        os.chdir(temp_dir)
        result = runner.invoke(app, ["init"])
        assert result.exit_code == 0
        assert (temp_dir / "CLAUDE.md").exists()

    def test_init_creates_agents_md(self, temp_dir):
        """init creates AGENTS.md."""
        os.chdir(temp_dir)
        result = runner.invoke(app, ["init"])
        assert result.exit_code == 0
        assert (temp_dir / "AGENTS.md").exists()

    def test_init_creates_claude_skills(self, temp_dir):
        """init creates .claude/skills directory."""
        os.chdir(temp_dir)
        result = runner.invoke(app, ["init"])
        assert result.exit_code == 0
        assert (temp_dir / ".claude" / "skills").exists()

    def test_init_creates_codex_skills(self, temp_dir):
        """init creates .codex/skills directory."""
        os.chdir(temp_dir)
        result = runner.invoke(app, ["init"])
        assert result.exit_code == 0
        assert (temp_dir / ".codex" / "skills").exists()

    def test_init_fails_if_claude_md_exists(self, temp_dir):
        """init fails if CLAUDE.md already exists without --force."""
        os.chdir(temp_dir)
        (temp_dir / "CLAUDE.md").write_text("existing content")
        result = runner.invoke(app, ["init"])
        assert result.exit_code == 1
        assert "already exists" in result.output

    def test_init_fails_if_agents_md_exists(self, temp_dir):
        """init fails if AGENTS.md already exists without --force."""
        os.chdir(temp_dir)
        (temp_dir / "AGENTS.md").write_text("existing content")
        result = runner.invoke(app, ["init"])
        assert result.exit_code == 1
        assert "already exists" in result.output

    def test_init_force_overwrites(self, temp_dir):
        """init --force overwrites existing files."""
        os.chdir(temp_dir)
        (temp_dir / "CLAUDE.md").write_text("old content")
        (temp_dir / "AGENTS.md").write_text("old content")
        result = runner.invoke(app, ["init", "--force"])
        assert result.exit_code == 0
        # Verify files were overwritten (should have new content from templates)
        assert (temp_dir / "CLAUDE.md").exists()
        assert (temp_dir / "AGENTS.md").exists()

    def test_init_force_short_flag(self, temp_dir):
        """-f works as shorthand for --force."""
        os.chdir(temp_dir)
        (temp_dir / "CLAUDE.md").write_text("old content")
        result = runner.invoke(app, ["init", "-f"])
        assert result.exit_code == 0

    def test_init_output_message(self, temp_dir):
        """init shows success message."""
        os.chdir(temp_dir)
        result = runner.invoke(app, ["init"])
        assert "Ralph initialized" in result.output

    def test_init_idempotent_with_force(self, temp_dir):
        """Running init --force twice works."""
        os.chdir(temp_dir)
        result1 = runner.invoke(app, ["init"])
        assert result1.exit_code == 0
        result2 = runner.invoke(app, ["init", "--force"])
        assert result2.exit_code == 0


class TestRunCommand:
    """Tests for the 'ml-ralph run' command."""

    def test_run_fails_without_init(self, temp_dir):
        """run fails if not initialized."""
        os.chdir(temp_dir)
        result = runner.invoke(app, ["run", "--max-iterations", "1"])
        assert result.exit_code == 1
        assert "Not initialized" in result.output

    def test_run_requires_ralph_md(self, temp_dir):
        """run requires .ml-ralph/RALPH.md to exist."""
        os.chdir(temp_dir)
        (temp_dir / ".ml-ralph").mkdir()
        # Don't create RALPH.md
        result = runner.invoke(app, ["run", "--max-iterations", "1"])
        assert result.exit_code == 1
        assert "Not initialized" in result.output

    def test_run_default_tool_is_claude(self, initialized_project):
        """Default tool is claude."""
        os.chdir(initialized_project)
        with patch("ml_ralph_cli.cli._stream_claude") as mock_stream:
            mock_stream.return_value = ""
            runner.invoke(app, ["run", "--max-iterations", "1"])
            mock_stream.assert_called_once()

    def test_run_with_codex_tool(self, initialized_project):
        """--tool codex uses codex streaming."""
        os.chdir(initialized_project)
        with patch("ml_ralph_cli.cli._stream_codex") as mock_stream:
            mock_stream.return_value = ""
            runner.invoke(app, ["run", "--tool", "codex", "--max-iterations", "1"])
            mock_stream.assert_called_once()

    def test_run_with_claude_tool_explicit(self, initialized_project):
        """--tool claude explicitly uses claude streaming."""
        os.chdir(initialized_project)
        with patch("ml_ralph_cli.cli._stream_claude") as mock_stream:
            mock_stream.return_value = ""
            runner.invoke(app, ["run", "--tool", "claude", "--max-iterations", "1"])
            mock_stream.assert_called_once()

    def test_run_sandbox_option(self, initialized_project):
        """--sandbox option is passed to codex."""
        os.chdir(initialized_project)
        with patch("ml_ralph_cli.cli._stream_codex") as mock_stream:
            mock_stream.return_value = ""
            runner.invoke(
                app,
                [
                    "run",
                    "--tool",
                    "codex",
                    "--sandbox",
                    "danger-full-access",
                    "--max-iterations",
                    "1",
                ],
            )
            # Check that sandbox was passed correctly
            mock_stream.assert_called_once()
            call_args = mock_stream.call_args
            # Sandbox is passed as positional arg (second positional)
            assert call_args[0][1] == "danger-full-access"

    def test_run_max_iterations(self, initialized_project):
        """--max-iterations controls loop count."""
        os.chdir(initialized_project)
        call_count = 0

        def mock_stream(*args, **kwargs):
            nonlocal call_count
            call_count += 1
            return ""

        with patch("ml_ralph_cli.cli._stream_claude", side_effect=mock_stream):
            runner.invoke(app, ["run", "--max-iterations", "3"])
            assert call_count == 3

    def test_run_stops_on_project_complete(self, initialized_project):
        """run stops when <project_complete> is returned."""
        os.chdir(initialized_project)
        call_count = 0

        def mock_stream(*args, **kwargs):
            nonlocal call_count
            call_count += 1
            if call_count == 2:
                return "<project_complete>"
            return ""

        with patch("ml_ralph_cli.cli._stream_claude", side_effect=mock_stream):
            result = runner.invoke(app, ["run", "--max-iterations", "10"])
            assert call_count == 2
            assert "Project complete" in result.output

    def test_run_shows_iteration_headers(self, initialized_project):
        """run shows iteration headers."""
        os.chdir(initialized_project)
        with patch("ml_ralph_cli.cli._stream_claude", return_value=""):
            result = runner.invoke(app, ["run", "--max-iterations", "2"])
            assert "Iteration 1" in result.output
            assert "Iteration 2" in result.output

    def test_run_shows_tool_display(self, initialized_project):
        """run shows which tool is being used."""
        os.chdir(initialized_project)
        with patch("ml_ralph_cli.cli._stream_claude", return_value=""):
            result = runner.invoke(
                app, ["run", "--tool", "claude", "--max-iterations", "1"]
            )
            assert "claude" in result.output

    def test_run_codex_shows_sandbox_mode(self, initialized_project):
        """run with codex shows sandbox mode."""
        os.chdir(initialized_project)
        with patch("ml_ralph_cli.cli._stream_codex", return_value=""):
            result = runner.invoke(
                app,
                [
                    "run",
                    "--tool",
                    "codex",
                    "--sandbox",
                    "read-only",
                    "--max-iterations",
                    "1",
                ],
            )
            assert "codex" in result.output
            assert "read-only" in result.output


class TestRunAgentStreaming:
    """Tests for _run_agent_streaming function."""

    def test_dispatches_to_claude(self, initialized_project):
        """Tool 'claude' dispatches to _stream_claude."""
        from ml_ralph_cli.cli import _run_agent_streaming

        os.chdir(initialized_project)
        with patch("ml_ralph_cli.cli._stream_claude", return_value="test") as mock:
            result = _run_agent_streaming("claude", "test prompt")
            mock.assert_called_once_with("test prompt")
            assert result == "test"

    def test_dispatches_to_codex(self, initialized_project):
        """Tool 'codex' dispatches to _stream_codex."""
        from ml_ralph_cli.cli import _run_agent_streaming

        os.chdir(initialized_project)
        with patch("ml_ralph_cli.cli._stream_codex", return_value="test") as mock:
            result = _run_agent_streaming("codex", "test prompt", sandbox="read-only")
            mock.assert_called_once_with("test prompt", "read-only")
            assert result == "test"

    def test_unknown_tool_exits(self, initialized_project):
        """Unknown tool raises Exit exception."""
        import typer
        from ml_ralph_cli.cli import _run_agent_streaming

        os.chdir(initialized_project)
        with pytest.raises(typer.Exit):
            _run_agent_streaming("unknown_tool", "test prompt")


class TestHelpOutput:
    """Tests for help text and documentation."""

    def test_main_help(self):
        """Main help shows available commands."""
        result = runner.invoke(app, ["--help"])
        assert result.exit_code == 0
        assert "init" in result.output
        assert "run" in result.output

    def test_init_help(self):
        """init --help shows options."""
        result = runner.invoke(app, ["init", "--help"])
        assert result.exit_code == 0
        assert "--force" in result.output
        assert "-f" in result.output

    def test_run_help(self):
        """run --help shows all options."""
        result = runner.invoke(app, ["run", "--help"])
        assert result.exit_code == 0
        assert "--tool" in result.output
        assert "--max-iterations" in result.output
        assert "--sandbox" in result.output
        assert "claude" in result.output
        assert "codex" in result.output

    def test_run_help_shows_examples(self):
        """run --help shows usage examples."""
        result = runner.invoke(app, ["run", "--help"])
        assert result.exit_code == 0
        assert (
            "ml-ralph run --tool claude" in result.output or "Examples" in result.output
        )
