"""Integration tests for ml-ralph CLI."""

import os
from unittest.mock import MagicMock, patch

from typer.testing import CliRunner

from ml_ralph_cli.cli import app

runner = CliRunner()


class TestFullWorkflow:
    """End-to-end workflow tests."""

    def test_init_then_run_workflow(self, temp_project):
        """Complete workflow: init -> run."""
        os.chdir(temp_project)

        # Step 1: Initialize
        result = runner.invoke(app, ["init"])
        assert result.exit_code == 0
        assert (temp_project / ".ml-ralph" / "RALPH.md").exists()

        # Step 2: Run (mocked)
        with patch("ml_ralph_cli.cli._stream_claude") as mock_stream:
            mock_stream.return_value = "<project_complete>"
            result = runner.invoke(app, ["run", "--max-iterations", "5"])
            assert result.exit_code == 0
            assert "Project complete" in result.output
            # Should only call once since we returned project_complete
            assert mock_stream.call_count == 1

    def test_codex_workflow(self, temp_project):
        """Complete workflow with Codex."""
        os.chdir(temp_project)

        # Initialize
        result = runner.invoke(app, ["init"])
        assert result.exit_code == 0

        # Run with Codex (mocked)
        with patch("ml_ralph_cli.cli._stream_codex") as mock_stream:
            mock_stream.return_value = "<iteration_complete>"
            result = runner.invoke(
                app,
                [
                    "run",
                    "--tool",
                    "codex",
                    "--sandbox",
                    "workspace-write",
                    "--max-iterations",
                    "2",
                ],
            )
            assert result.exit_code == 0
            assert mock_stream.call_count == 2

    def test_iteration_complete_continues_loop(self, temp_project):
        """<iteration_complete> continues to next iteration."""
        os.chdir(temp_project)
        runner.invoke(app, ["init"])

        call_count = 0

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

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

    def test_max_iterations_limit(self, temp_project):
        """Max iterations stops the loop."""
        os.chdir(temp_project)
        runner.invoke(app, ["init"])

        with patch(
            "ml_ralph_cli.cli._stream_claude", return_value="<iteration_complete>"
        ):
            result = runner.invoke(app, ["run", "--max-iterations", "3"])
            assert result.exit_code == 0
            assert "Reached max iterations" in result.output


class TestProjectComplete:
    """Tests for project completion detection."""

    def test_project_complete_in_result(self, temp_project):
        """Detects <project_complete> in result."""
        os.chdir(temp_project)
        runner.invoke(app, ["init"])

        with patch("ml_ralph_cli.cli._stream_claude") as mock:
            mock.return_value = "Task done. <project_complete>"
            result = runner.invoke(app, ["run", "--max-iterations", "5"])
            assert "Project complete" in result.output
            assert mock.call_count == 1

    def test_project_complete_with_surrounding_text(self, temp_project):
        """Detects <project_complete> with surrounding text."""
        os.chdir(temp_project)
        runner.invoke(app, ["init"])

        with patch("ml_ralph_cli.cli._stream_claude") as mock:
            mock.return_value = "All success criteria met! <project_complete> Goodbye."
            result = runner.invoke(app, ["run", "--max-iterations", "5"])
            assert "Project complete" in result.output

    def test_iteration_complete_does_not_stop(self, temp_project):
        """<iteration_complete> alone doesn't stop the loop."""
        os.chdir(temp_project)
        runner.invoke(app, ["init"])

        with patch(
            "ml_ralph_cli.cli._stream_claude", return_value="<iteration_complete>"
        ):
            result = runner.invoke(app, ["run", "--max-iterations", "3"])
            # Should reach max iterations, not project complete
            assert "Reached max iterations" in result.output


class TestErrorHandling:
    """Tests for error handling scenarios."""

    def test_claude_not_installed(self, initialized_project, capsys):
        """Handles Claude CLI not being installed."""
        os.chdir(initialized_project)

        with patch("subprocess.Popen", side_effect=FileNotFoundError):
            result = runner.invoke(
                app, ["run", "--tool", "claude", "--max-iterations", "1"]
            )
            assert result.exit_code == 1

    def test_codex_not_installed(self, initialized_project, capsys):
        """Handles Codex CLI not being installed."""
        os.chdir(initialized_project)

        # Mock git check to succeed but Popen to fail
        with patch("subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(returncode=0)
            with patch("subprocess.Popen", side_effect=FileNotFoundError):
                result = runner.invoke(
                    app, ["run", "--tool", "codex", "--max-iterations", "1"]
                )
                assert result.exit_code == 1


class TestSkillsInstallation:
    """Tests for skills installation."""

    def test_skills_installed_to_claude(self, temp_dir):
        """Skills are installed to .claude/skills."""
        os.chdir(temp_dir)
        result = runner.invoke(app, ["init"])
        assert result.exit_code == 0

        claude_skills = temp_dir / ".claude" / "skills"
        assert claude_skills.exists()

    def test_skills_installed_to_codex(self, temp_dir):
        """Skills are installed to .codex/skills."""
        os.chdir(temp_dir)
        result = runner.invoke(app, ["init"])
        assert result.exit_code == 0

        codex_skills = temp_dir / ".codex" / "skills"
        assert codex_skills.exists()

    def test_skills_overwritten_on_reinit(self, temp_dir):
        """Skills are overwritten when re-initializing with --force."""
        os.chdir(temp_dir)

        # First init
        runner.invoke(app, ["init"])

        # Modify a skill file
        skill_dir = temp_dir / ".claude" / "skills" / "ml-ralph"
        if skill_dir.exists():
            marker_file = skill_dir / "marker.txt"
            marker_file.write_text("custom content")

        # Reinit with --force
        runner.invoke(app, ["init", "--force"])

        # Marker should be gone (directory was replaced)
        if skill_dir.exists():
            assert not (skill_dir / "marker.txt").exists()


class TestPromptConstruction:
    """Tests for prompt construction."""

    def test_prompt_includes_ralph_md_reference(self, initialized_project):
        """Prompt tells agent to read RALPH.md."""
        os.chdir(initialized_project)

        captured_prompt = None

        def capture_stream(prompt, *args, **kwargs):
            nonlocal captured_prompt
            captured_prompt = prompt
            return "<project_complete>"

        with patch("ml_ralph_cli.cli._stream_claude", side_effect=capture_stream):
            runner.invoke(app, ["run", "--max-iterations", "1"])

        assert captured_prompt is not None
        assert "RALPH.md" in captured_prompt
        assert "<iteration_complete>" in captured_prompt
        assert "<project_complete>" in captured_prompt

    def test_same_prompt_for_both_tools(self, initialized_project):
        """Same prompt is used for both Claude and Codex."""
        os.chdir(initialized_project)

        claude_prompt = None
        codex_prompt = None

        def capture_claude(prompt, *args, **kwargs):
            nonlocal claude_prompt
            claude_prompt = prompt
            return "<project_complete>"

        def capture_codex(prompt, sandbox, *args, **kwargs):
            nonlocal codex_prompt
            codex_prompt = prompt
            return "<project_complete>"

        with patch("ml_ralph_cli.cli._stream_claude", side_effect=capture_claude):
            runner.invoke(app, ["run", "--tool", "claude", "--max-iterations", "1"])

        with patch("ml_ralph_cli.cli._stream_codex", side_effect=capture_codex):
            runner.invoke(app, ["run", "--tool", "codex", "--max-iterations", "1"])

        assert claude_prompt is not None
        assert codex_prompt is not None
        assert claude_prompt == codex_prompt


class TestOutputFormatting:
    """Tests for output formatting."""

    def test_iteration_headers_shown(self, initialized_project):
        """Iteration headers are displayed."""
        os.chdir(initialized_project)

        with patch("ml_ralph_cli.cli._stream_claude", return_value=""):
            result = runner.invoke(app, ["run", "--max-iterations", "3"])
            assert "Iteration 1" in result.output
            assert "Iteration 2" in result.output
            assert "Iteration 3" in result.output

    def test_tool_name_shown_claude(self, initialized_project):
        """Claude tool name is shown in output."""
        os.chdir(initialized_project)

        with patch(
            "ml_ralph_cli.cli._stream_claude", return_value="<project_complete>"
        ):
            result = runner.invoke(
                app, ["run", "--tool", "claude", "--max-iterations", "1"]
            )
            assert "claude" in result.output.lower()

    def test_tool_name_and_sandbox_shown_codex(self, initialized_project):
        """Codex tool name and sandbox mode are shown in output."""
        os.chdir(initialized_project)

        with patch("ml_ralph_cli.cli._stream_codex", return_value="<project_complete>"):
            result = runner.invoke(
                app,
                [
                    "run",
                    "--tool",
                    "codex",
                    "--sandbox",
                    "danger-full-access",
                    "--max-iterations",
                    "1",
                ],
            )
            assert "codex" in result.output.lower()
            assert "danger-full-access" in result.output
