"""Tests for concurrent stdin handling in talk REPL.

Spec source: vectl step ``talk-ux-fixes.talk-concurrent-input``
User can type /stop, /quit, /help while anima.interact call is in-flight.
"""

import asyncio
import inspect
import json
from dataclasses import replace
from unittest.mock import MagicMock
from unittest.mock import patch

import pytest

from anima.shell.cli_commands.talk_deps import default_deps
from anima.shell.cli_commands import talk_runtime
from anima.shell.cli_commands import talk_session


async def _read_stdin_loop_compat(
    *,
    prompt_session: object,
    input_queue: asyncio.Queue[str | None],
    read_input,
) -> None:
    """Call talk_runtime.read_stdin_loop across signature variants."""

    read_loop = talk_runtime.read_stdin_loop
    params = inspect.signature(read_loop).parameters
    if "read_input" in params:
        kwargs = {
            "prompt_session": prompt_session,
            "input_queue": input_queue,
            "read_input": read_input,
        }
        await read_loop(**kwargs)
        return

    if "read_input_func" in params:
        kwargs = {
            "prompt_session": prompt_session,
            "input_queue": input_queue,
            "read_input_func": read_input,
        }
        await read_loop(**kwargs)
        return

    with patch.object(talk_runtime, "read_input", read_input, create=True):
        kwargs = {
            "prompt_session": prompt_session,
            "input_queue": input_queue,
        }
        await read_loop(**kwargs)


async def _wait_for_interact_or_input_or_stream_compat(**wait_kwargs):
    """Call wait helper across deps/non-deps signatures."""

    wait_fn = talk_runtime.wait_for_interact_or_input_or_stream
    wait_params = inspect.signature(wait_fn).parameters

    deps = wait_kwargs.get("deps", default_deps())

    def _print_detach_compat(*args, **kwargs) -> None:
        if args and not kwargs:
            agent_id, json_output, console = args[:3]
            newline_prefix = False
        else:
            agent_id = kwargs.get("agent_id", args[0] if args else "")
            json_output = kwargs.get("json_output", args[1] if len(args) > 1 else False)
            console = kwargs.get("console", args[2] if len(args) > 2 else None)
            newline_prefix = kwargs.get("newline_prefix", False)
        deps.print_detach(
            agent_id=agent_id,
            json_output=bool(json_output),
            console=console,
            newline_prefix=bool(newline_prefix),
        )

    def _print_help_compat(*args, **kwargs) -> None:
        if args and not kwargs:
            json_output, console = args[:2]
        else:
            json_output = kwargs.get("json_output", args[0] if args else False)
            console = kwargs.get("console", args[1] if len(args) > 1 else None)
        try:
            deps.print_help(json_output=json_output, console=console)
        except TypeError:
            deps.print_help(json_output, console)

    def _handle_interact_result_compat(*args, **kwargs):
        if args:
            mcp_result = args[0]
            agent_id = args[1]
            json_output = args[2]
            console = args[3]
            was_awaiting_approval = args[4]
            print_approval_prompt = args[5]
        else:
            mcp_result = kwargs["mcp_result"]
            agent_id = kwargs["agent_id"]
            json_output = kwargs["json_output"]
            console = kwargs["console"]
            was_awaiting_approval = kwargs["was_awaiting_approval"]
            print_approval_prompt = kwargs["print_approval_prompt"]
        return talk_session.handle_interact_result(
            mcp_result,
            agent_id,
            json_output,
            console,
            was_awaiting_approval,
            print_approval_prompt,
            deps=deps,
            suppress_display=bool(kwargs.get("suppress_display", False)),
        )

    compat_kwargs = {
        "deps": deps,
        "execute_stop_turn_func": (
            lambda session, json_output, console: talk_session.execute_stop_turn(
                session,
                json_output,
                console,
                deps=deps,
            )
        ),
        "cleanup_stdin_func": talk_runtime.cleanup_stdin,
        "print_detach_func": _print_detach_compat,
        "emit_with_prompt_coordination_func": deps.emit_with_prompt_coordination,
        "print_help_func": _print_help_compat,
        "handle_interact_result_func": _handle_interact_result_compat,
    }
    for name, value in compat_kwargs.items():
        if name in wait_params and name not in wait_kwargs:
            wait_kwargs[name] = value

    if "deps" not in wait_params and "deps" in wait_kwargs:
        wait_kwargs.pop("deps")

    return await wait_fn(**wait_kwargs)


class TestConcurrentInputQueue:
    """Tests for input queue processing in talk REPL."""

    @pytest.mark.asyncio
    async def test_input_queue_processes_messages_sequentially(self):
        """Messages from input queue are processed in order."""
        queue: asyncio.Queue[str | None] = asyncio.Queue()

        # Put messages in queue
        await queue.put("msg1")
        await queue.put("msg2")
        await queue.put("msg3")

        # Verify order
        assert await queue.get() == "msg1"
        assert await queue.get() == "msg2"
        assert await queue.get() == "msg3"

    @pytest.mark.asyncio
    async def test_input_queue_handles_none_as_eof(self):
        """None in queue signals EOF."""
        queue: asyncio.Queue[str | None] = asyncio.Queue()

        await queue.put("message")
        await queue.put(None)

        assert await queue.get() == "message"
        assert await queue.get() is None

    @pytest.mark.asyncio
    async def test_stop_command_syntax_detection(self):
        """/stop command is recognized by parse_slash_command."""
        from anima.core.talk_commands import parse_talk_slash_command

        command, arg = parse_talk_slash_command("/stop")
        assert command == "stop"
        assert arg is None

        command, arg = parse_talk_slash_command("/STOP")
        assert command == "stop"

        command, arg = parse_talk_slash_command("  /stop  ")
        assert command == "stop"

    @pytest.mark.asyncio
    async def test_quit_command_detection(self):
        """/quit command is detected by strip().lower() check."""
        assert "  /quit  ".strip().lower() == "/quit"
        assert "/QUIT".strip().lower() == "/quit"
        assert "/Quit".strip().lower() == "/quit"

    @pytest.mark.asyncio
    async def test_help_command_detection(self):
        """/help command is detected by strip().lower() check."""
        assert "  /help  ".strip().lower() == "/help"
        assert "/HELP".strip().lower() == "/help"
        assert "/Help".strip().lower() == "/help"


class TestTaskCancellation:
    """Tests for asyncio task cancellation patterns used in concurrent input."""

    @pytest.mark.asyncio
    async def test_interact_task_can_be_cancelled(self):
        """A long-running interact task can be cancelled."""
        cancelled = asyncio.Event()

        async def long_running():
            try:
                await asyncio.sleep(100.0)  # Long delay
            except asyncio.CancelledError:
                cancelled.set()
                raise

        task = asyncio.create_task(long_running())

        # Give task time to start
        await asyncio.sleep(0.01)

        # Cancel the task
        task.cancel()

        try:
            await task
        except asyncio.CancelledError:
            pass

        # Verify cancellation was caught
        assert cancelled.is_set()

    @pytest.mark.asyncio
    async def test_wait_first_completed_pattern(self):
        """asyncio.wait with FIRST_COMPLETED works correctly."""
        slow_task_started = asyncio.Event()
        fast_task_result = asyncio.Event()

        async def slow_operation():
            slow_task_started.set()
            await asyncio.sleep(10.0)
            return "slow"

        async def fast_operation():
            await asyncio.sleep(0.01)
            fast_task_result.set()
            return "fast"

        slow = asyncio.create_task(slow_operation())
        fast = asyncio.create_task(fast_operation())

        # Wait for first to complete
        done, pending = await asyncio.wait(
            [slow, fast], return_when=asyncio.FIRST_COMPLETED
        )

        # Fast should complete first
        assert fast in done
        assert slow in pending

        # Cancel pending
        slow.cancel()
        try:
            await slow
        except asyncio.CancelledError:
            pass

    @pytest.mark.asyncio
    async def test_stop_cancels_interact_and_calls_stop_turn(self):
        """/stop cancels in-flight interact and proceeds to stop_turn."""
        interactions = []

        async def mock_interact():
            interactions.append("interact_start")
            await asyncio.sleep(10.0)  # Simulate slow response
            interactions.append("interact_done")
            return MagicMock(
                content=[
                    MagicMock(text=json.dumps({"response": "done", "turn_number": 1}))
                ]
            )

        async def mock_stop_turn():
            interactions.append("stop_turn_called")
            return MagicMock(
                content=[MagicMock(text=json.dumps({"status": "stopped"}))]
            )

        # Simulate the pattern
        interact_task = asyncio.create_task(mock_interact())

        # Wait a bit then cancel (simulating /stop arriving during wait)
        await asyncio.sleep(0.05)
        interact_task.cancel()

        try:
            await interact_task
        except asyncio.CancelledError:
            interactions.append("interact_cancelled")

        # Now call stop_turn
        await mock_stop_turn()

        # Verify sequence: interact started, got cancelled, stop_turn called
        assert "interact_start" in interactions
        assert "interact_cancelled" in interactions
        assert "stop_turn_called" in interactions
        assert "interact_done" not in interactions  # Should not complete


class TestStdinTimeoutPollingRegression:
    """Regression tests for stdin timeout polling behavior."""

    @pytest.mark.asyncio
    async def test_read_stdin_loop_does_not_resubmit_pending_input(self):
        """A pending stdin read is reused across timeout polls.

        Spec source: vectl step fix-talk-stdin-executor-starvation.
        A timeout in wait_for must not trigger a new run_in_executor submission
        while the previous console.input call is still pending.
        """
        read_calls = 0
        input_complete = asyncio.Event()
        read_started = asyncio.Event()

        async def _fake_read_input(_prompt_session) -> str:
            nonlocal read_calls
            read_calls += 1
            read_started.set()
            await input_complete.wait()
            return "/quit"

        queue: asyncio.Queue[str | None] = asyncio.Queue()

        stdin_task = asyncio.create_task(
            _read_stdin_loop_compat(
                prompt_session=MagicMock(),
                input_queue=queue,
                read_input=_fake_read_input,
            )
        )

        # Wait long enough for multiple polling windows.
        await asyncio.wait_for(read_started.wait(), timeout=1.0)
        await asyncio.sleep(0.2)
        assert read_calls == 1

        input_complete.set()
        assert await queue.get() == "/quit"

        stdin_task.cancel()
        await stdin_task


class TestSlashHelpOutput:
    """Tests for /help command output during concurrent mode."""

    def test_slash_help_lines_constant(self):
        """SLASH_HELP_LINES contains expected commands."""
        from anima.shell.cli_commands.talk_cmd import SLASH_HELP_LINES

        help_text = "\n".join(SLASH_HELP_LINES)

        # All expected commands present
        assert "/help" in help_text
        assert "/stop" in help_text
        assert "/approve" in help_text
        assert "/reject" in help_text
        assert "/quit" in help_text

        # Descriptive text present
        assert "Cancel current agent turn" in help_text
        assert "Detach from agent" in help_text

    @pytest.mark.asyncio
    async def test_help_command_returns_immediately(self):
        """/help command returns immediately without waiting for interact."""
        # This is a documentation test - the behavior is:
        # When user types /help, it's handled in the main loop
        # BEFORE the interact wait loop starts
        # So no waiting for interact completion

        from anima.shell.cli_commands.talk_cmd import SLASH_HELP_LINES

        help_output = "\n".join(SLASH_HELP_LINES)

        # Verify help is instant - no async operation needed
        assert "/help" in help_output
        assert "/stop" in help_output
        assert "/quit" in help_output


class TestSlashStopBehavior:
    """Tests for /stop command behavior documentation."""

    @pytest.mark.asyncio
    async def test_stop_command_calls_stop_turn(self):
        """Runtime: /stop requests cooperative stop before next turn starts."""
        queue: asyncio.Queue[str | None] = asyncio.Queue()
        await queue.put("/stop")
        await queue.put("follow-up")

        stop_turn_calls: list[tuple[object, bool, object]] = []
        stop_requested = asyncio.Event()

        async def _fake_interact() -> MagicMock:
            await stop_requested.wait()
            return MagicMock(
                content=[
                    MagicMock(
                        text=json.dumps(
                            {
                                "response": "[Current turn cancelled by /stop]",
                                "turn_number": 1,
                            }
                        )
                    )
                ]
            )

        async def _fake_execute_stop_turn(
            session: object,
            json_output: bool,
            console: object,
            *,
            deps: object,
        ) -> None:
            stop_turn_calls.append((session, json_output, console))
            stop_requested.set()

        with (
            patch(
                "anima.shell.cli_commands.talk_session.execute_stop_turn",
                _fake_execute_stop_turn,
            ),
            patch(
                "anima.shell.cli_commands.talk_runtime.cleanup_stdin",
                lambda stdin_task: None,
            ),
        ):
            (
                should_return,
                had_error,
                _was_awaiting,
            ) = await asyncio.wait_for(
                _wait_for_interact_or_input_or_stream_compat(
                    interact_task=asyncio.create_task(_fake_interact()),
                    stream_task=None,
                    input_queue=queue,
                    session=MagicMock(),
                    agent_id="agent-1",
                    json_output=False,
                    console=MagicMock(),
                    was_awaiting_approval=False,
                    print_approval_prompt=lambda _reason=None: None,
                    stdin_task=asyncio.create_task(asyncio.sleep(10.0)),
                    deps=default_deps(),
                ),
                timeout=1.0,
            )

        assert should_return is False
        assert had_error is False
        assert len(stop_turn_calls) == 1
        _, stop_json_output, _ = stop_turn_calls[0]
        assert stop_json_output is False
        assert await queue.get() == "follow-up"

    @pytest.mark.asyncio
    async def test_stop_preserves_session(self):
        """Runtime: /stop returns to the loop without terminating session."""
        queue: asyncio.Queue[str | None] = asyncio.Queue()
        await queue.put("/stop")
        await queue.put("next input")

        stop_requested = asyncio.Event()

        async def _fake_interact() -> MagicMock:
            await stop_requested.wait()
            return MagicMock(
                content=[
                    MagicMock(
                        text=json.dumps(
                            {
                                "response": "[Current turn cancelled by /stop]",
                                "turn_number": 1,
                            }
                        )
                    )
                ]
            )

        async def _fake_execute_stop_turn(
            _session: object,
            _json_output: bool,
            _console: object,
            *,
            deps: object,
        ) -> None:
            _ = deps
            stop_requested.set()

        with (
            patch(
                "anima.shell.cli_commands.talk_session.execute_stop_turn",
                _fake_execute_stop_turn,
            ),
            patch(
                "anima.shell.cli_commands.talk_runtime.cleanup_stdin",
                lambda stdin_task: (_ for _ in ()).throw(
                    AssertionError("cleanup called")
                ),
            ),
        ):
            (
                should_return,
                had_error,
                _was_awaiting,
            ) = await asyncio.wait_for(
                _wait_for_interact_or_input_or_stream_compat(
                    interact_task=asyncio.create_task(_fake_interact()),
                    stream_task=None,
                    input_queue=queue,
                    session=MagicMock(),
                    agent_id="agent-1",
                    json_output=False,
                    console=MagicMock(),
                    was_awaiting_approval=False,
                    print_approval_prompt=lambda _reason=None: None,
                    stdin_task=asyncio.create_task(asyncio.sleep(10.0)),
                    deps=default_deps(),
                ),
                timeout=1.0,
            )

        assert should_return is False
        assert had_error is False
        assert await queue.get() == "next input"

    @pytest.mark.asyncio
    async def test_queued_input_updates_thinking_indicator_count(self, mock_mcp):
        """Queued input while interact is in-flight updates thinking count."""
        mock_mcp.patch(tool_response='{"response":"done","turn_number":1}')

        class _FakeTextContent:
            def __init__(self, text: str) -> None:
                self.text = text

        queue: asyncio.Queue[str | None] = asyncio.Queue()
        await queue.put("queued follow-up")
        render_counts: list[int] = []

        async def _fake_interact() -> object:
            await asyncio.sleep(0.05)
            return MagicMock(
                content=[_FakeTextContent('{"response":"done","turn_number":1}')]
            )

        with patch("mcp.types.TextContent", _FakeTextContent):
            (
                should_return,
                had_error,
                _,
            ) = await _wait_for_interact_or_input_or_stream_compat(
                interact_task=asyncio.create_task(_fake_interact()),
                stream_task=None,
                input_queue=queue,
                session=MagicMock(),
                agent_id="agent-1",
                json_output=False,
                console=MagicMock(),
                was_awaiting_approval=False,
                print_approval_prompt=lambda _reason=None: None,
                stdin_task=asyncio.create_task(asyncio.sleep(10.0)),
                deps=default_deps(),
                render_thinking_indicator=render_counts.append,
            )

        assert should_return is False
        assert had_error is False
        assert render_counts == [1]
        assert await queue.get() == "queued follow-up"


class TestSlashQuitBehavior:
    """Tests for /quit command behavior documentation."""

    @pytest.mark.asyncio
    async def test_quit_exits_cleanly(self):
        """Runtime: /quit cancels stdin and exits the current wait."""
        queue: asyncio.Queue[str | None] = asyncio.Queue()
        await queue.put("/quit")

        interact_cancelled = asyncio.Event()

        async def _fake_interact() -> None:
            try:
                await asyncio.sleep(10.0)
            except asyncio.CancelledError:
                interact_cancelled.set()
                raise

        cleanup_calls: list[object] = []
        detach_calls: list[tuple[str, bool, bool]] = []

        async def _fake_cleanup_stdin(stdin_task: object) -> None:
            cleanup_calls.append(stdin_task)

        def _fake_print_detach(
            *args,
            **kwargs,
        ) -> None:
            if "agent_id" in kwargs and "json_output" in kwargs:
                agent_id = kwargs["agent_id"]
                json_output = kwargs["json_output"]
                newline_prefix = bool(kwargs.get("newline_prefix", False))
            else:
                agent_id = args[0]
                json_output = args[1]
                newline_prefix = bool(kwargs.get("newline_prefix", False))
            detach_calls.append((agent_id, json_output, newline_prefix))

        deps = replace(default_deps(), print_detach=_fake_print_detach)

        with (
            patch(
                "anima.shell.cli_commands.talk_runtime.cleanup_stdin",
                _fake_cleanup_stdin,
            ),
        ):
            (
                should_return,
                had_error,
                _was_awaiting,
            ) = await _wait_for_interact_or_input_or_stream_compat(
                interact_task=asyncio.create_task(_fake_interact()),
                stream_task=None,
                input_queue=queue,
                session=MagicMock(),
                agent_id="agent-1",
                json_output=False,
                console=MagicMock(),
                was_awaiting_approval=False,
                print_approval_prompt=lambda _reason=None: None,
                stdin_task=asyncio.create_task(asyncio.sleep(10.0)),
                deps=deps,
            )

        assert should_return is True
        assert had_error is False
        assert interact_cancelled.is_set()
        assert len(cleanup_calls) == 1
        assert detach_calls == [("agent-1", False, False)]
        assert queue.empty() is True

    @pytest.mark.asyncio
    async def test_quit_during_interact_cancels_interact(self):
        """Runtime: /quit cancels interact and stdin wait for session detachment."""
        queue: asyncio.Queue[str | None] = asyncio.Queue()
        queue.put_nowait("/quit")
        queue.put_nowait("queued message")

        interact_cancelled = asyncio.Event()

        async def _fake_interact() -> None:
            try:
                await asyncio.sleep(10.0)
            except asyncio.CancelledError:
                interact_cancelled.set()
                raise

        cleanup_calls: list[object] = []

        async def _fake_cleanup_stdin(stdin_task: object) -> None:
            cleanup_calls.append(stdin_task)

        deps = replace(
            default_deps(),
            print_detach=lambda *_args, **_kwargs: None,
        )

        with patch(
            "anima.shell.cli_commands.talk_runtime.cleanup_stdin",
            _fake_cleanup_stdin,
        ):
            (
                should_return,
                had_error,
                _was_awaiting,
            ) = await _wait_for_interact_or_input_or_stream_compat(
                interact_task=asyncio.create_task(_fake_interact()),
                stream_task=None,
                input_queue=queue,
                session=MagicMock(),
                agent_id="agent-1",
                json_output=False,
                console=MagicMock(),
                was_awaiting_approval=False,
                print_approval_prompt=lambda _reason=None: None,
                stdin_task=asyncio.create_task(asyncio.sleep(10.0)),
                deps=deps,
            )

        assert should_return is True
        assert had_error is False
        assert interact_cancelled.is_set()
        assert len(cleanup_calls) == 1
        assert await queue.get() == "queued message"


# Integration-level documentation test
class TestConcurrentInputDocumentation:
    """Documentation of concurrent input architecture."""

    @pytest.mark.asyncio
    async def test_architecture_components(self):
        """Verify queue-driven stdin loop runtime behavior."""
        from anima.shell.cli_commands.talk_cmd import SLASH_HELP_LINES

        assert "/help" in "\n".join(SLASH_HELP_LINES)

        queue: asyncio.Queue[str | None] = asyncio.Queue()
        read_inputs = ["/help", "message", "/quit"]

        async def _fake_read_input(_prompt_session: object) -> str | None:
            return read_inputs.pop(0)

        stdin_task = asyncio.create_task(
            _read_stdin_loop_compat(
                prompt_session=MagicMock(),
                input_queue=queue,
                read_input=_fake_read_input,
            )
        )

        assert await queue.get() == "/help"
        assert await queue.get() == "message"
        assert await queue.get() == "/quit"
        assert await queue.get() is None
        await stdin_task

        assert len(read_inputs) == 0

    @pytest.mark.asyncio
    async def test_no_blocking_on_interact(self):
        """/help is handled before interact completion is required."""
        queue: asyncio.Queue[str | None] = asyncio.Queue()
        await queue.put("/help")
        await queue.put("/quit")

        interact_cancelled = asyncio.Event()
        help_calls: list[bool] = []
        detach_calls: list[tuple[str, bool, bool]] = []

        async def _fake_interact() -> None:
            try:
                await asyncio.sleep(10.0)
            except asyncio.CancelledError:
                interact_cancelled.set()
                raise

        async def _fake_cleanup_stdin(_stdin_task: object) -> None:
            detach_calls.append(("cleanup", False, False))

        deps = replace(
            default_deps(),
            print_help=lambda *, json_output, console: help_calls.append(True),
            print_detach=lambda *args, **kwargs: detach_calls.append(
                (
                    kwargs.get("agent_id", args[0] if args else "agent-1"),
                    kwargs.get("json_output", args[1] if len(args) > 1 else False),
                    bool(kwargs.get("newline_prefix", False)),
                )
            ),
        )

        with patch(
            "anima.shell.cli_commands.talk_runtime.cleanup_stdin",
            _fake_cleanup_stdin,
        ):
            should_return, had_error, _ = await asyncio.wait_for(
                _wait_for_interact_or_input_or_stream_compat(
                    interact_task=asyncio.create_task(_fake_interact()),
                    stream_task=None,
                    input_queue=queue,
                    session=MagicMock(),
                    agent_id="agent-1",
                    json_output=False,
                    console=MagicMock(),
                    was_awaiting_approval=False,
                    print_approval_prompt=lambda _reason=None: None,
                    stdin_task=asyncio.create_task(asyncio.sleep(10.0)),
                    deps=deps,
                ),
                timeout=1.0,
            )

        assert should_return is True
        assert had_error is False
        assert help_calls == [True]
        assert interact_cancelled.is_set()
        assert detach_calls == [("cleanup", False, False), ("agent-1", False, False)]


if __name__ == "__main__":
    import sys

    sys.exit(pytest.main([__file__, "-v"]))
