"""Tests for streaming functions (_stream_claude and _stream_codex)."""

import json
import os
from unittest.mock import MagicMock, patch

import pytest

from ml_ralph_cli.cli import _stream_claude, _stream_codex


class TestStreamClaude:
    """Tests for _stream_claude function."""

    def test_constructs_correct_command(self, temp_project):
        """Claude command includes all required flags."""
        os.chdir(temp_project)

        captured_cmd = None

        def capture_popen(cmd, **kwargs):
            nonlocal captured_cmd
            captured_cmd = cmd
            mock_process = MagicMock()
            mock_process.stdout = iter([])
            mock_process.wait.return_value = 0
            return mock_process

        with patch("subprocess.Popen", side_effect=capture_popen):
            _stream_claude("test prompt")

        assert captured_cmd is not None
        assert "claude" in captured_cmd
        assert "-p" in captured_cmd
        assert "test prompt" in captured_cmd
        assert "--output-format" in captured_cmd
        assert "stream-json" in captured_cmd
        assert "--allowedTools" in captured_cmd

    def test_returns_result_from_result_event(self, temp_project):
        """Returns the result from a result event."""
        os.chdir(temp_project)

        events = [
            json.dumps({"type": "result", "result": "Task done. <iteration_complete>"}),
        ]

        def mock_popen(cmd, **kwargs):
            mock_process = MagicMock()
            mock_process.stdout = iter([e + "\n" for e in events])
            mock_process.wait.return_value = 0
            return mock_process

        with patch("subprocess.Popen", side_effect=mock_popen):
            result = _stream_claude("test prompt")

        assert result == "Task done. <iteration_complete>"

    def test_handles_multiple_events(self, temp_project):
        """Processes multiple events in sequence."""
        os.chdir(temp_project)

        events = [
            json.dumps(
                {
                    "type": "assistant",
                    "message": {"content": [{"type": "text", "text": "Working..."}]},
                }
            ),
            json.dumps({"type": "result", "result": "Done."}),
        ]

        def mock_popen(cmd, **kwargs):
            mock_process = MagicMock()
            mock_process.stdout = iter([e + "\n" for e in events])
            mock_process.wait.return_value = 0
            return mock_process

        with patch("subprocess.Popen", side_effect=mock_popen):
            result = _stream_claude("test prompt")

        assert result == "Done."

    def test_handles_non_json_lines(self, temp_project, capsys):
        """Non-JSON lines are printed as-is."""
        os.chdir(temp_project)

        lines = [
            "Plain text line",
            json.dumps({"type": "result", "result": "Done."}),
        ]

        def mock_popen(cmd, **kwargs):
            mock_process = MagicMock()
            mock_process.stdout = iter([line + "\n" for line in lines])
            mock_process.wait.return_value = 0
            return mock_process

        with patch("subprocess.Popen", side_effect=mock_popen):
            _stream_claude("test prompt")

        captured = capsys.readouterr()
        assert "Plain text line" in captured.out

    def test_handles_empty_lines(self, temp_project):
        """Empty lines are skipped."""
        os.chdir(temp_project)

        lines = [
            "",
            "   ",
            json.dumps({"type": "result", "result": "Done."}),
        ]

        def mock_popen(cmd, **kwargs):
            mock_process = MagicMock()
            mock_process.stdout = iter([line + "\n" for line in lines])
            mock_process.wait.return_value = 0
            return mock_process

        with patch("subprocess.Popen", side_effect=mock_popen):
            result = _stream_claude("test prompt")

        assert result == "Done."

    def test_file_not_found_error(self, temp_project, capsys):
        """FileNotFoundError shows helpful message."""
        import typer

        os.chdir(temp_project)

        with patch("subprocess.Popen", side_effect=FileNotFoundError):
            with pytest.raises(typer.Exit):
                _stream_claude("test prompt")

        captured = capsys.readouterr()
        assert "Claude CLI not found" in captured.out

    def test_returns_empty_if_no_result(self, temp_project):
        """Returns empty string if no result event."""
        os.chdir(temp_project)

        events = [
            json.dumps(
                {
                    "type": "assistant",
                    "message": {"content": [{"type": "text", "text": "Working..."}]},
                }
            ),
        ]

        def mock_popen(cmd, **kwargs):
            mock_process = MagicMock()
            mock_process.stdout = iter([e + "\n" for e in events])
            mock_process.wait.return_value = 0
            return mock_process

        with patch("subprocess.Popen", side_effect=mock_popen):
            result = _stream_claude("test prompt")

        assert result == ""


class TestStreamCodex:
    """Tests for _stream_codex function."""

    def test_constructs_correct_command_in_git_repo(self, temp_project):
        """Codex command in git repo doesn't include --skip-git-repo-check."""
        os.chdir(temp_project)

        captured_cmd = None

        def capture_popen(cmd, **kwargs):
            nonlocal captured_cmd
            captured_cmd = cmd
            mock_process = MagicMock()
            mock_process.stdout = iter([])
            mock_process.wait.return_value = 0
            return mock_process

        # Mock git check to return success (in git repo)
        with patch("subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(returncode=0)
            with patch("subprocess.Popen", side_effect=capture_popen):
                _stream_codex("test prompt", "workspace-write")

        assert captured_cmd is not None
        assert "codex" in captured_cmd
        assert "exec" in captured_cmd
        assert "--json" in captured_cmd
        assert "--full-auto" in captured_cmd
        assert "--sandbox" in captured_cmd
        assert "workspace-write" in captured_cmd
        assert "--skip-git-repo-check" not in captured_cmd

    def test_adds_skip_git_check_outside_repo(self, temp_dir):
        """Codex command outside git repo includes --skip-git-repo-check."""
        os.chdir(temp_dir)

        captured_cmd = None

        def capture_popen(cmd, **kwargs):
            nonlocal captured_cmd
            captured_cmd = cmd
            mock_process = MagicMock()
            mock_process.stdout = iter([])
            mock_process.wait.return_value = 0
            return mock_process

        # Mock git check to return failure (not in git repo)
        with patch("subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(returncode=1)
            with patch("subprocess.Popen", side_effect=capture_popen):
                _stream_codex("test prompt", "workspace-write")

        assert captured_cmd is not None
        assert "--skip-git-repo-check" in captured_cmd

    def test_sandbox_modes(self, temp_project):
        """Different sandbox modes are passed correctly."""
        os.chdir(temp_project)

        for sandbox in ["read-only", "workspace-write", "danger-full-access"]:
            captured_cmd = None

            def capture_popen(cmd, **kwargs):
                nonlocal captured_cmd
                captured_cmd = cmd
                mock_process = MagicMock()
                mock_process.stdout = iter([])
                mock_process.wait.return_value = 0
                return mock_process

            with patch("subprocess.run") as mock_run:
                mock_run.return_value = MagicMock(returncode=0)
                with patch("subprocess.Popen", side_effect=capture_popen):
                    _stream_codex("test prompt", sandbox)

            assert sandbox in captured_cmd

    def test_returns_agent_message_text(self, temp_project):
        """Returns the text from agent_message event."""
        os.chdir(temp_project)

        events = [
            json.dumps({"type": "thread.started", "thread_id": "test-123"}),
            json.dumps(
                {
                    "type": "item.completed",
                    "item": {
                        "id": "item_1",
                        "type": "agent_message",
                        "text": "Done. <iteration_complete>",
                    },
                }
            ),
            json.dumps(
                {
                    "type": "turn.completed",
                    "usage": {
                        "input_tokens": 100,
                        "cached_input_tokens": 50,
                        "output_tokens": 10,
                    },
                }
            ),
        ]

        def mock_popen(cmd, **kwargs):
            mock_process = MagicMock()
            mock_process.stdout = iter([e + "\n" for e in events])
            mock_process.wait.return_value = 0
            return mock_process

        with patch("subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(returncode=0)
            with patch("subprocess.Popen", side_effect=mock_popen):
                result = _stream_codex("test prompt", "workspace-write")

        assert result == "Done. <iteration_complete>"

    def test_handles_multiple_events(self, temp_project, capsys):
        """Processes multiple events in sequence."""
        os.chdir(temp_project)

        events = [
            json.dumps({"type": "thread.started", "thread_id": "abc-123"}),
            json.dumps({"type": "turn.started"}),
            json.dumps(
                {
                    "type": "item.completed",
                    "item": {
                        "id": "item_0",
                        "type": "reasoning",
                        "text": "Thinking about it...",
                    },
                }
            ),
            json.dumps(
                {
                    "type": "item.started",
                    "item": {
                        "id": "item_1",
                        "type": "command_execution",
                        "command": "ls -la",
                        "status": "in_progress",
                    },
                }
            ),
            json.dumps(
                {
                    "type": "item.completed",
                    "item": {
                        "id": "item_1",
                        "type": "command_execution",
                        "command": "ls -la",
                        "aggregated_output": "file.txt\n",
                        "exit_code": 0,
                        "status": "completed",
                    },
                }
            ),
            json.dumps(
                {
                    "type": "item.completed",
                    "item": {
                        "id": "item_2",
                        "type": "agent_message",
                        "text": "All done!",
                    },
                }
            ),
            json.dumps(
                {
                    "type": "turn.completed",
                    "usage": {
                        "input_tokens": 1000,
                        "cached_input_tokens": 500,
                        "output_tokens": 50,
                    },
                }
            ),
        ]

        def mock_popen(cmd, **kwargs):
            mock_process = MagicMock()
            mock_process.stdout = iter([e + "\n" for e in events])
            mock_process.wait.return_value = 0
            return mock_process

        with patch("subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(returncode=0)
            with patch("subprocess.Popen", side_effect=mock_popen):
                result = _stream_codex("test prompt", "workspace-write")

        captured = capsys.readouterr()
        assert "abc-123" in captured.out  # Thread ID
        assert "Bash" in captured.out  # Command
        assert "All done!" in captured.out  # Agent message
        assert result == "All done!"

    def test_handles_non_json_lines(self, temp_project, capsys):
        """Non-JSON lines are printed as-is."""
        os.chdir(temp_project)

        lines = [
            "Some debug output",
            json.dumps(
                {
                    "type": "item.completed",
                    "item": {"id": "item_1", "type": "agent_message", "text": "Done."},
                }
            ),
        ]

        def mock_popen(cmd, **kwargs):
            mock_process = MagicMock()
            mock_process.stdout = iter([line + "\n" for line in lines])
            mock_process.wait.return_value = 0
            return mock_process

        with patch("subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(returncode=0)
            with patch("subprocess.Popen", side_effect=mock_popen):
                _stream_codex("test prompt", "workspace-write")

        captured = capsys.readouterr()
        assert "Some debug output" in captured.out

    def test_handles_empty_lines(self, temp_project):
        """Empty lines are skipped."""
        os.chdir(temp_project)

        lines = [
            "",
            "   ",
            json.dumps(
                {
                    "type": "item.completed",
                    "item": {"id": "item_1", "type": "agent_message", "text": "Done."},
                }
            ),
        ]

        def mock_popen(cmd, **kwargs):
            mock_process = MagicMock()
            mock_process.stdout = iter([line + "\n" for line in lines])
            mock_process.wait.return_value = 0
            return mock_process

        with patch("subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(returncode=0)
            with patch("subprocess.Popen", side_effect=mock_popen):
                result = _stream_codex("test prompt", "workspace-write")

        assert result == "Done."

    def test_file_not_found_error(self, temp_project, capsys):
        """FileNotFoundError shows helpful message."""
        import typer

        os.chdir(temp_project)

        with patch("subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(returncode=0)
            with patch("subprocess.Popen", side_effect=FileNotFoundError):
                with pytest.raises(typer.Exit):
                    _stream_codex("test prompt", "workspace-write")

        captured = capsys.readouterr()
        assert "Codex CLI not found" in captured.out

    def test_returns_empty_if_no_agent_message(self, temp_project):
        """Returns empty string if no agent_message event."""
        os.chdir(temp_project)

        events = [
            json.dumps({"type": "thread.started", "thread_id": "test-123"}),
            json.dumps(
                {
                    "type": "item.completed",
                    "item": {
                        "id": "item_0",
                        "type": "reasoning",
                        "text": "Thinking...",
                    },
                }
            ),
            json.dumps(
                {
                    "type": "turn.completed",
                    "usage": {
                        "input_tokens": 100,
                        "cached_input_tokens": 50,
                        "output_tokens": 10,
                    },
                }
            ),
        ]

        def mock_popen(cmd, **kwargs):
            mock_process = MagicMock()
            mock_process.stdout = iter([e + "\n" for e in events])
            mock_process.wait.return_value = 0
            return mock_process

        with patch("subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(returncode=0)
            with patch("subprocess.Popen", side_effect=mock_popen):
                result = _stream_codex("test prompt", "workspace-write")

        assert result == ""

    def test_last_agent_message_is_returned(self, temp_project):
        """If multiple agent_messages, the last one is returned."""
        os.chdir(temp_project)

        events = [
            json.dumps(
                {
                    "type": "item.completed",
                    "item": {
                        "id": "item_1",
                        "type": "agent_message",
                        "text": "First message",
                    },
                }
            ),
            json.dumps(
                {
                    "type": "item.completed",
                    "item": {
                        "id": "item_2",
                        "type": "agent_message",
                        "text": "Second message",
                    },
                }
            ),
        ]

        def mock_popen(cmd, **kwargs):
            mock_process = MagicMock()
            mock_process.stdout = iter([e + "\n" for e in events])
            mock_process.wait.return_value = 0
            return mock_process

        with patch("subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(returncode=0)
            with patch("subprocess.Popen", side_effect=mock_popen):
                result = _stream_codex("test prompt", "workspace-write")

        assert result == "Second message"


class TestEdgeCases:
    """Edge case tests for streaming functions."""

    def test_claude_malformed_json(self, temp_project, capsys):
        """Malformed JSON is printed as-is."""
        os.chdir(temp_project)

        lines = [
            '{"type": "assistant", "message": {',  # Incomplete JSON
            json.dumps({"type": "result", "result": "Done."}),
        ]

        def mock_popen(cmd, **kwargs):
            mock_process = MagicMock()
            mock_process.stdout = iter([line + "\n" for line in lines])
            mock_process.wait.return_value = 0
            return mock_process

        with patch("subprocess.Popen", side_effect=mock_popen):
            _stream_claude("test prompt")

        captured = capsys.readouterr()
        assert '{"type": "assistant"' in captured.out

    def test_codex_malformed_json(self, temp_project, capsys):
        """Malformed JSON is printed as-is."""
        os.chdir(temp_project)

        lines = [
            '{"type": "thread.started", "thread_id":',  # Incomplete JSON
            json.dumps(
                {
                    "type": "item.completed",
                    "item": {"id": "item_1", "type": "agent_message", "text": "Done."},
                }
            ),
        ]

        def mock_popen(cmd, **kwargs):
            mock_process = MagicMock()
            mock_process.stdout = iter([line + "\n" for line in lines])
            mock_process.wait.return_value = 0
            return mock_process

        with patch("subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(returncode=0)
            with patch("subprocess.Popen", side_effect=mock_popen):
                _stream_codex("test prompt", "workspace-write")

        captured = capsys.readouterr()
        assert '{"type": "thread.started"' in captured.out

    def test_claude_with_special_characters_in_prompt(self, temp_project):
        """Prompts with special characters are handled."""
        os.chdir(temp_project)

        captured_cmd = None

        def capture_popen(cmd, **kwargs):
            nonlocal captured_cmd
            captured_cmd = cmd
            mock_process = MagicMock()
            mock_process.stdout = iter([])
            mock_process.wait.return_value = 0
            return mock_process

        special_prompt = "Test with 'quotes' and \"double quotes\" and $variables"
        with patch("subprocess.Popen", side_effect=capture_popen):
            _stream_claude(special_prompt)

        assert special_prompt in captured_cmd

    def test_codex_with_special_characters_in_prompt(self, temp_project):
        """Prompts with special characters are handled."""
        os.chdir(temp_project)

        captured_cmd = None

        def capture_popen(cmd, **kwargs):
            nonlocal captured_cmd
            captured_cmd = cmd
            mock_process = MagicMock()
            mock_process.stdout = iter([])
            mock_process.wait.return_value = 0
            return mock_process

        special_prompt = "Test with 'quotes' and \"double quotes\" and $variables"
        with patch("subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(returncode=0)
            with patch("subprocess.Popen", side_effect=capture_popen):
                _stream_codex(special_prompt, "workspace-write")

        assert special_prompt in captured_cmd

    def test_claude_unicode_in_output(self, temp_project, capsys):
        """Unicode characters in output are handled."""
        os.chdir(temp_project)

        events = [
            json.dumps(
                {
                    "type": "assistant",
                    "message": {
                        "content": [{"type": "text", "text": "Hello! \u2764 \u2728"}]
                    },
                }
            ),
            json.dumps({"type": "result", "result": "Done."}),
        ]

        def mock_popen(cmd, **kwargs):
            mock_process = MagicMock()
            mock_process.stdout = iter([e + "\n" for e in events])
            mock_process.wait.return_value = 0
            return mock_process

        with patch("subprocess.Popen", side_effect=mock_popen):
            _stream_claude("test prompt")

        captured = capsys.readouterr()
        assert "\u2764" in captured.out or "Hello!" in captured.out

    def test_codex_unicode_in_output(self, temp_project, capsys):
        """Unicode characters in output are handled."""
        os.chdir(temp_project)

        events = [
            json.dumps(
                {
                    "type": "item.completed",
                    "item": {
                        "id": "item_1",
                        "type": "agent_message",
                        "text": "Hello! \u2764 \u2728",
                    },
                }
            ),
        ]

        def mock_popen(cmd, **kwargs):
            mock_process = MagicMock()
            mock_process.stdout = iter([e + "\n" for e in events])
            mock_process.wait.return_value = 0
            return mock_process

        with patch("subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(returncode=0)
            with patch("subprocess.Popen", side_effect=mock_popen):
                _stream_codex("test prompt", "workspace-write")

        captured = capsys.readouterr()
        assert "\u2764" in captured.out or "Hello!" in captured.out
