"""
Testy obsługi klawisza Escape:
- _prompt_confirmation() — Esc → _EscapeInterrupt, n/Enter jak dotąd
- _call_tool_cancellable() — non-TTY: przezroczyste przekazanie do session.call_tool
- client.run() — Esc w promptcie lub podczas tool: rollback messages + [Przerwano]
"""

import asyncio
import sys
import os
import pytest
from unittest.mock import AsyncMock, MagicMock, patch

sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))

import client
from client import _prompt_confirmation, _call_tool_cancellable, _EscapeInterrupt
from mcp_agent0 import state
from tests.test_client import _config_patch, _make_fake_tool
from tests.test_autenter import (
    _tool_response,
    _text_response,
    _session_with_tool,
    _patch_session,
    _patch_http,
)


def setup_function():
    state["autenter"] = False


# ---------------------------------------------------------------------------
# Unit testy: _prompt_confirmation — ścieżka non-TTY (fallback do input())
# ---------------------------------------------------------------------------

def test_prompt_non_tty_enter_returns_empty():
    with patch("sys.stdin") as mock_stdin, \
         patch("builtins.input", return_value=""):
        mock_stdin.isatty.return_value = False
        result = _prompt_confirmation("prompt")
    assert result == ""


def test_prompt_non_tty_n_returns_n():
    with patch("sys.stdin") as mock_stdin, \
         patch("builtins.input", return_value="n"):
        mock_stdin.isatty.return_value = False
        result = _prompt_confirmation("prompt")
    assert result == "n"


def test_prompt_non_tty_arbitrary_string_returned_as_is():
    with patch("sys.stdin") as mock_stdin, \
         patch("builtins.input", return_value="cokolwiek"):
        mock_stdin.isatty.return_value = False
        result = _prompt_confirmation("prompt")
    assert result == "cokolwiek"


# ---------------------------------------------------------------------------
# Unit testy: _prompt_confirmation — ścieżka TTY (termios/tty)
# ---------------------------------------------------------------------------

def _tty_patches(char):
    """Zwraca stos kontekstów patchujących termios/tty ze wskazanym znakiem."""
    return [
        patch("sys.stdin") ,
        patch("termios.tcgetattr", return_value=[]),
        patch("termios.tcsetattr"),
        patch("tty.setraw"),
        patch("builtins.print"),
    ], char


def _run_prompt_with_char(ch):
    """Pomocnik: uruchamia _prompt_confirmation w trybie TTY z podanym znakiem."""
    with patch("sys.stdin") as mock_stdin, \
         patch("termios.tcgetattr", return_value=[]), \
         patch("termios.tcsetattr"), \
         patch("tty.setraw"), \
         patch("builtins.print"):
        mock_stdin.isatty.return_value = True
        mock_stdin.fileno.return_value = 0
        mock_stdin.read.return_value = ch
        return _prompt_confirmation("prompt")


def test_prompt_tty_enter_returns_empty():
    assert _run_prompt_with_char("\r") == ""


def test_prompt_tty_newline_returns_empty():
    assert _run_prompt_with_char("\n") == ""


def test_prompt_tty_space_returns_empty():
    assert _run_prompt_with_char(" ") == ""


def test_prompt_tty_n_returns_n():
    assert _run_prompt_with_char("n") == "n"


def test_prompt_tty_uppercase_n_returns_n():
    assert _run_prompt_with_char("N") == "n"


def test_prompt_tty_escape_raises():
    with pytest.raises(_EscapeInterrupt):
        _run_prompt_with_char("\x1b")


def test_prompt_tty_escape_not_mistaken_for_n():
    """Upewnia się, że \\x1b nie przechodzi jako 'n'."""
    with pytest.raises(_EscapeInterrupt):
        _run_prompt_with_char("\x1b")


# ---------------------------------------------------------------------------
# Unit testy: _call_tool_cancellable — ścieżka non-TTY
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_call_tool_cancellable_non_tty_delegates_to_session():
    fake_result = MagicMock(content=[MagicMock(text="wynik")], isError=False)
    session = MagicMock()
    session.call_tool = AsyncMock(return_value=fake_result)

    with patch("sys.stdin") as mock_stdin:
        mock_stdin.isatty.return_value = False
        loop = asyncio.get_event_loop()
        result = await _call_tool_cancellable(session, "echo", {"x": 1}, loop)

    session.call_tool.assert_called_once_with("echo", {"x": 1})
    assert result is fake_result


@pytest.mark.asyncio
async def test_call_tool_cancellable_non_tty_passes_args():
    session = MagicMock()
    session.call_tool = AsyncMock(return_value=MagicMock(content=[MagicMock(text="ok")]))

    with patch("sys.stdin") as mock_stdin:
        mock_stdin.isatty.return_value = False
        loop = asyncio.get_event_loop()
        await _call_tool_cancellable(session, "files__read", {"path": "/tmp/x"}, loop)

    session.call_tool.assert_called_once_with("files__read", {"path": "/tmp/x"})


# ---------------------------------------------------------------------------
# Integracja: Esc w promptcie potwierdzenia (autenter=False)
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_escape_at_confirmation_stops_tool_execution():
    """Esc w promptcie → call_tool dla echo nie jest wywoływany."""
    state["autenter"] = False
    session = _session_with_tool("echo")
    anthropic_mock = MagicMock()
    anthropic_mock.messages.create = AsyncMock(
        side_effect=[_tool_response("echo"), _text_response()]
    )

    call_n = 0

    async def fake_executor(executor, fn):
        nonlocal call_n
        call_n += 1
        if call_n == 1:
            return "zrób coś"       # user input
        if call_n == 2:
            raise _EscapeInterrupt()  # Esc w promptcie
        raise EOFError()              # następna pętla → wyjście

    with _config_patch(), \
         patch("client.anthropic.AsyncAnthropic", return_value=anthropic_mock), \
         _patch_http(), \
         _patch_session(session), \
         patch("asyncio.get_event_loop") as mock_loop:

        loop_mock = MagicMock()
        mock_loop.return_value = loop_mock
        loop_mock.run_in_executor = fake_executor

        await client.run()

    # tylko rejestracja — echo nie wywołane
    assert session.call_tool.call_count == 1
    assert session.call_tool.call_args_list[0].args[0] == "management__add_mcp_server"


@pytest.mark.asyncio
async def test_escape_at_confirmation_no_second_anthropic_call():
    """Esc w promptcie → Anthropic nie jest pytany drugi raz (brak tool_result w messages)."""
    state["autenter"] = False
    session = _session_with_tool("echo")
    anthropic_mock = MagicMock()
    anthropic_mock.messages.create = AsyncMock(
        side_effect=[_tool_response("echo"), _text_response()]
    )

    call_n = 0

    async def fake_executor(executor, fn):
        nonlocal call_n
        call_n += 1
        if call_n == 1:
            return "zrób coś"
        if call_n == 2:
            raise _EscapeInterrupt()
        raise EOFError()

    with _config_patch(), \
         patch("client.anthropic.AsyncAnthropic", return_value=anthropic_mock), \
         _patch_http(), \
         _patch_session(session), \
         patch("asyncio.get_event_loop") as mock_loop:

        loop_mock = MagicMock()
        mock_loop.return_value = loop_mock
        loop_mock.run_in_executor = fake_executor

        await client.run()

    assert anthropic_mock.messages.create.call_count == 1


@pytest.mark.asyncio
async def test_escape_at_confirmation_prints_przerwano(capsys):
    """Esc w promptcie → wydrukowany jest '[Przerwano'."""
    state["autenter"] = False
    session = _session_with_tool("echo")
    anthropic_mock = MagicMock()
    anthropic_mock.messages.create = AsyncMock(
        side_effect=[_tool_response("echo"), _text_response()]
    )

    call_n = 0

    async def fake_executor(executor, fn):
        nonlocal call_n
        call_n += 1
        if call_n == 1:
            return "zrób coś"
        if call_n == 2:
            raise _EscapeInterrupt()
        raise EOFError()

    with _config_patch(), \
         patch("client.anthropic.AsyncAnthropic", return_value=anthropic_mock), \
         _patch_http(), \
         _patch_session(session), \
         patch("asyncio.get_event_loop") as mock_loop:

        loop_mock = MagicMock()
        mock_loop.return_value = loop_mock
        loop_mock.run_in_executor = fake_executor

        await client.run()

    assert "Przerwano" in capsys.readouterr().out


# ---------------------------------------------------------------------------
# Integracja: Esc podczas wykonania narzędzia (autenter=True)
# ---------------------------------------------------------------------------

@pytest.mark.asyncio
async def test_escape_during_execution_stops_loop():
    """Esc podczas call_tool → Anthropic nie jest pytany drugi raz."""
    state["autenter"] = True
    session = _session_with_tool("echo")
    anthropic_mock = MagicMock()
    anthropic_mock.messages.create = AsyncMock(
        side_effect=[_tool_response("echo"), _text_response()]
    )

    call_n = 0

    async def fake_executor(executor, fn):
        nonlocal call_n
        call_n += 1
        if call_n == 1:
            return "zrób coś"
        raise EOFError()

    with _config_patch(), \
         patch("client.anthropic.AsyncAnthropic", return_value=anthropic_mock), \
         _patch_http(), \
         _patch_session(session), \
         patch("asyncio.get_event_loop") as mock_loop, \
         patch("client._call_tool_cancellable", side_effect=_EscapeInterrupt):

        loop_mock = MagicMock()
        mock_loop.return_value = loop_mock
        loop_mock.run_in_executor = fake_executor

        await client.run()

    assert anthropic_mock.messages.create.call_count == 1


@pytest.mark.asyncio
async def test_escape_during_execution_prints_przerwano(capsys):
    """Esc podczas call_tool → wydrukowany jest '[Przerwano'."""
    state["autenter"] = True
    session = _session_with_tool("echo")
    anthropic_mock = MagicMock()
    anthropic_mock.messages.create = AsyncMock(
        side_effect=[_tool_response("echo"), _text_response()]
    )

    call_n = 0

    async def fake_executor(executor, fn):
        nonlocal call_n
        call_n += 1
        if call_n == 1:
            return "zrób coś"
        raise EOFError()

    with _config_patch(), \
         patch("client.anthropic.AsyncAnthropic", return_value=anthropic_mock), \
         _patch_http(), \
         _patch_session(session), \
         patch("asyncio.get_event_loop") as mock_loop, \
         patch("client._call_tool_cancellable", side_effect=_EscapeInterrupt):

        loop_mock = MagicMock()
        mock_loop.return_value = loop_mock
        loop_mock.run_in_executor = fake_executor

        await client.run()

    assert "Przerwano" in capsys.readouterr().out


@pytest.mark.asyncio
async def test_escape_during_execution_conversation_continues():
    """Po Esc w trakcie tool — pętla rozmowy wznawia się (kolejny input obsłużony)."""
    state["autenter"] = True
    session = _session_with_tool("echo")
    anthropic_mock = MagicMock()
    anthropic_mock.messages.create = AsyncMock(
        side_effect=[_tool_response("echo"), _text_response("rozumiem")]
    )

    call_n = 0

    async def fake_executor(executor, fn):
        nonlocal call_n
        call_n += 1
        if call_n == 1:
            return "zrób coś"   # pierwsze zapytanie
        if call_n == 2:
            return "coś innego"  # kolejne zapytanie po Esc
        raise EOFError()

    with _config_patch(), \
         patch("client.anthropic.AsyncAnthropic", return_value=anthropic_mock), \
         _patch_http(), \
         _patch_session(session), \
         patch("asyncio.get_event_loop") as mock_loop, \
         patch("client._call_tool_cancellable", side_effect=_EscapeInterrupt):

        loop_mock = MagicMock()
        mock_loop.return_value = loop_mock
        loop_mock.run_in_executor = fake_executor

        await client.run()

    # Drugie wejście uruchamia drugie zapytanie do Anthropic
    assert anthropic_mock.messages.create.call_count == 2
