from __future__ import annotations

import asyncio
import asyncio.subprocess as aio_subprocess
import contextlib
import io
import os
from pathlib import Path
from typing import Any, cast

from acp import PROTOCOL_VERSION, Client, RequestError, connect_to_agent
from acp.schema import ClientCapabilities, Implementation
import pexpect
import pytest

from tests import TESTS_ROOT
from tests.e2e.common import ansi_tolerant_pattern

BROWSER_AUTH_NAME = "Sign in through Mistral AI Studio"
BROWSER_AUTH_DESCRIPTION = (
    "Sign into Mistral Vibe through your Mistral AI Studio account."
)


class _AcpSmokeClient(Client):
    def on_connect(self, conn: Any) -> None:
        pass

    async def request_permission(self, *args: Any, **kwargs: Any) -> Any:
        msg = "session/request_permission"
        raise RequestError.method_not_found(msg)

    async def write_text_file(self, *args: Any, **kwargs: Any) -> Any:
        msg = "fs/write_text_file"
        raise RequestError.method_not_found(msg)

    async def read_text_file(self, *args: Any, **kwargs: Any) -> Any:
        msg = "fs/read_text_file"
        raise RequestError.method_not_found(msg)

    async def create_terminal(self, *args: Any, **kwargs: Any) -> Any:
        msg = "terminal/create"
        raise RequestError.method_not_found(msg)

    async def terminal_output(self, *args: Any, **kwargs: Any) -> Any:
        msg = "terminal/output"
        raise RequestError.method_not_found(msg)

    async def release_terminal(self, *args: Any, **kwargs: Any) -> Any:
        msg = "terminal/release"
        raise RequestError.method_not_found(msg)

    async def wait_for_terminal_exit(self, *args: Any, **kwargs: Any) -> Any:
        msg = "terminal/wait_for_exit"
        raise RequestError.method_not_found(msg)

    async def kill_terminal(self, *args: Any, **kwargs: Any) -> Any:
        msg = "terminal/kill"
        raise RequestError.method_not_found(msg)

    async def ext_method(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
        _ = params
        raise RequestError.method_not_found(method)

    async def ext_notification(self, method: str, params: dict[str, Any]) -> None:
        _ = params
        raise RequestError.method_not_found(method)

    async def session_update(self, *_args: Any, **_kwargs: Any) -> None:
        pass


@pytest.fixture
def vibe_home_dir(tmp_path: Path) -> Path:
    return tmp_path / ".vibe"


async def _spawn_vibe_acp(env: dict[str, str]) -> asyncio.subprocess.Process:
    return await asyncio.create_subprocess_exec(
        "uv",
        "run",
        "vibe-acp",
        stdin=aio_subprocess.PIPE,
        stdout=aio_subprocess.PIPE,
        stderr=aio_subprocess.PIPE,
        cwd=TESTS_ROOT.parent,
        env=env,
    )


async def _terminate_process(proc: asyncio.subprocess.Process) -> None:
    if proc.returncode is None:
        with contextlib.suppress(ProcessLookupError):
            proc.terminate()
        with contextlib.suppress(TimeoutError):
            await asyncio.wait_for(proc.wait(), timeout=5)

    if proc.returncode is None:
        with contextlib.suppress(ProcessLookupError):
            proc.kill()
            await proc.wait()


def _build_env(vibe_home_dir: Path, *, include_api_key: bool) -> dict[str, str]:
    env = os.environ.copy()
    env["PYTHONUNBUFFERED"] = "1"
    env["VIBE_HOME"] = str(vibe_home_dir)

    vibe_home_dir.mkdir(parents=True, exist_ok=True)
    config_file = vibe_home_dir / "config.toml"
    if not config_file.exists():
        config_file.write_text("enable_telemetry = false\n")

    if include_api_key:
        env["MISTRAL_API_KEY"] = "mock"
    else:
        env.pop("MISTRAL_API_KEY", None)

    return env


def _build_client_capabilities(
    *, terminal_auth: bool = False, delegated_browser_auth: bool = False
) -> ClientCapabilities:
    if not terminal_auth and not delegated_browser_auth:
        return ClientCapabilities()

    field_meta: dict[str, bool] = {}
    if terminal_auth:
        field_meta["terminal-auth"] = True
    if delegated_browser_auth:
        field_meta["browser-auth-delegated"] = True
    return ClientCapabilities(field_meta=field_meta)


async def _connect_and_initialize(
    *,
    vibe_home_dir: Path,
    include_api_key: bool,
    terminal_auth: bool = False,
    delegated_browser_auth: bool = False,
) -> tuple[asyncio.subprocess.Process, Any, Any]:
    env = _build_env(vibe_home_dir, include_api_key=include_api_key)
    proc = await _spawn_vibe_acp(env)

    try:
        assert proc.stdin is not None
        assert proc.stdout is not None

        conn = connect_to_agent(_AcpSmokeClient(), proc.stdin, proc.stdout)
        initialize_response = await asyncio.wait_for(
            conn.initialize(
                protocol_version=PROTOCOL_VERSION,
                client_capabilities=_build_client_capabilities(
                    terminal_auth=terminal_auth,
                    delegated_browser_auth=delegated_browser_auth,
                ),
                client_info=Implementation(
                    name="pytest-smoke", title="Pytest Smoke", version="0.0.0"
                ),
            ),
            timeout=10,
        )
    except Exception:
        await _terminate_process(proc)
        raise

    return proc, initialize_response, conn


@pytest.mark.asyncio
async def test_vibe_acp_initialize_and_new_session(vibe_home_dir: Path) -> None:
    proc, initialize_response, conn = await _connect_and_initialize(
        vibe_home_dir=vibe_home_dir, include_api_key=True
    )

    try:
        assert initialize_response.protocol_version == PROTOCOL_VERSION
        assert initialize_response.agent_info.name == "@mistralai/mistral-vibe"
        assert initialize_response.agent_info.title == "Mistral Vibe"

        session = await asyncio.wait_for(
            conn.new_session(cwd=str(Path.cwd()), mcp_servers=[]), timeout=10
        )

        assert session.session_id
    finally:
        await _terminate_process(proc)


@pytest.mark.asyncio
async def test_vibe_acp_bootstraps_default_files(vibe_home_dir: Path) -> None:
    proc, _initialize_response, conn = await _connect_and_initialize(
        vibe_home_dir=vibe_home_dir, include_api_key=True
    )

    try:
        await asyncio.wait_for(
            conn.new_session(cwd=str(Path.cwd()), mcp_servers=[]), timeout=10
        )
    finally:
        await _terminate_process(proc)
    assert (vibe_home_dir / "config.toml").is_file()
    assert (vibe_home_dir / "vibehistory").is_file()


@pytest.mark.asyncio
async def test_vibe_acp_initialize_exposes_browser_auth(vibe_home_dir: Path) -> None:
    proc, initialize_response, _conn = await _connect_and_initialize(
        vibe_home_dir=vibe_home_dir, include_api_key=True
    )

    try:
        assert initialize_response.auth_methods is not None
        assert len(initialize_response.auth_methods) == 1
        auth_method = initialize_response.auth_methods[0]
        assert auth_method.id == "browser-auth"
        assert auth_method.name == BROWSER_AUTH_NAME
        assert auth_method.description == BROWSER_AUTH_DESCRIPTION
    finally:
        await _terminate_process(proc)


@pytest.mark.asyncio
async def test_vibe_acp_initialize_exposes_delegated_browser_auth_when_supported(
    vibe_home_dir: Path,
) -> None:
    proc, initialize_response, _conn = await _connect_and_initialize(
        vibe_home_dir=vibe_home_dir, include_api_key=True, delegated_browser_auth=True
    )

    try:
        assert initialize_response.auth_methods is not None
        assert len(initialize_response.auth_methods) == 2

        browser_auth_method = initialize_response.auth_methods[0]
        assert browser_auth_method.id == "browser-auth"
        assert browser_auth_method.name == BROWSER_AUTH_NAME
        assert browser_auth_method.description == BROWSER_AUTH_DESCRIPTION

        delegated_browser_auth_method = initialize_response.auth_methods[1]
        assert delegated_browser_auth_method.id == "browser-auth-delegated"
        assert delegated_browser_auth_method.name == BROWSER_AUTH_NAME
        assert delegated_browser_auth_method.description == BROWSER_AUTH_DESCRIPTION
    finally:
        await _terminate_process(proc)


@pytest.mark.asyncio
async def test_vibe_acp_initialize_exposes_terminal_auth_when_supported(
    vibe_home_dir: Path,
) -> None:
    proc, initialize_response, _conn = await _connect_and_initialize(
        vibe_home_dir=vibe_home_dir, include_api_key=True, terminal_auth=True
    )

    try:
        assert initialize_response.auth_methods is not None
        assert len(initialize_response.auth_methods) == 2

        browser_auth_method = initialize_response.auth_methods[0]
        assert browser_auth_method.id == "browser-auth"
        assert browser_auth_method.name == BROWSER_AUTH_NAME
        assert browser_auth_method.description == BROWSER_AUTH_DESCRIPTION

        auth_method = initialize_response.auth_methods[1]
        assert auth_method.id == "vibe-setup"
        assert auth_method.field_meta is not None

        terminal_auth = auth_method.field_meta["terminal-auth"]
        assert terminal_auth["label"] == "Mistral Vibe Setup"
        assert terminal_auth["command"]
        assert terminal_auth["args"]
        assert terminal_auth["args"][-1:] == ["--setup"]
    finally:
        await _terminate_process(proc)


@pytest.mark.timeout(30)
def test_vibe_acp_setup_shows_onboarding_and_exits_on_cancel(
    vibe_home_dir: Path,
) -> None:
    env = cast("os._Environ[str]", _build_env(vibe_home_dir, include_api_key=False))
    env["TERM"] = "xterm-256color"

    captured = io.StringIO()
    child = pexpect.spawn(
        "uv",
        ["run", "vibe-acp", "--setup"],
        cwd=str(TESTS_ROOT.parent),
        env=env,
        encoding="utf-8",
        timeout=10,
        dimensions=(36, 120),
    )
    child.logfile_read = captured

    try:
        child.expect(ansi_tolerant_pattern("Welcome to"), timeout=15)
        child.sendcontrol("c")
        child.expect(pexpect.EOF, timeout=10)
    finally:
        if child.isalive():
            child.terminate(force=True)
        if not child.closed:
            child.close()

    output = captured.getvalue()
    assert "Setup cancelled" in output


@pytest.mark.asyncio
async def test_vibe_acp_survives_broken_config(vibe_home_dir: Path) -> None:
    vibe_home_dir.mkdir(parents=True, exist_ok=True)
    (vibe_home_dir / "config.toml").write_text("{{{{invalid toml content!!")

    proc, _initialize_response, conn = await _connect_and_initialize(
        vibe_home_dir=vibe_home_dir, include_api_key=True
    )

    try:
        # new_session should return a structured JSON-RPC error, not crash the server
        with pytest.raises(RequestError):
            await asyncio.wait_for(
                conn.new_session(cwd=str(Path.cwd()), mcp_servers=[]), timeout=10
            )
        assert proc.returncode is None, "Server crashed after broken config"

        (vibe_home_dir / "config.toml").write_text("")
        session = await asyncio.wait_for(
            conn.new_session(cwd=str(Path.cwd()), mcp_servers=[]), timeout=10
        )
        assert session.session_id
    finally:
        await _terminate_process(proc)


@pytest.mark.asyncio
async def test_vibe_acp_new_session_fails_without_api_key(vibe_home_dir: Path) -> None:
    proc, _initialize_response, conn = await _connect_and_initialize(
        vibe_home_dir=vibe_home_dir, include_api_key=False
    )

    try:
        with pytest.raises(RequestError, match="Missing API key"):
            await asyncio.wait_for(
                conn.new_session(cwd=str(Path.cwd()), mcp_servers=[]), timeout=10
            )
    finally:
        await _terminate_process(proc)
