"""End-to-end tests for MCP server exercising all tools with mocked HTTP responses.

This module tests the complete flow from tool call through the client to HTTP requests,
mocking only the HTTP transport layer to verify the full integration.

Test Categories:
- Session Management: CRUD operations for AgenticSessions
- Observability: Logs, transcripts, and metrics retrieval
- Labels: Adding, removing, and filtering by labels
- Bulk Operations: Multi-session operations with confirmation
- Cluster Management: Multi-cluster configuration and auth
- Error Handling: HTTP errors, validation, and edge cases
"""

import json
from unittest.mock import MagicMock, patch

import httpx
import pytest

from mcp_acp.client import ACPClient
from mcp_acp.server import call_tool, list_tools

# ── Fixtures ─────────────────────────────────────────────────────────────────


@pytest.fixture
def mock_clusters_config():
    """Create mock clusters configuration with a single test cluster."""
    cluster = MagicMock()
    cluster.server = "https://api.test.example.com"
    cluster.default_project = "test-project"
    cluster.description = "Test Cluster"
    cluster.token = "test-token-abc123"

    config = MagicMock()
    config.clusters = {"test-cluster": cluster}
    config.default_cluster = "test-cluster"
    return config


@pytest.fixture
def mock_settings():
    """Create mock settings."""
    settings = MagicMock()
    settings.config_path = None
    return settings


def make_response(status_code: int = 200, json_data: dict | None = None, text: str = "") -> httpx.Response:
    """Create a mock httpx Response.

    Args:
        status_code: HTTP status code
        json_data: JSON response body (takes precedence over text)
        text: Plain text response body
    """
    if json_data is not None:
        content = json.dumps(json_data).encode()
        headers = {"content-type": "application/json"}
    else:
        content = text.encode()
        headers = {"content-type": "text/plain"}

    return httpx.Response(
        status_code=status_code,
        content=content,
        headers=headers,
        request=httpx.Request("GET", "https://api.test.example.com"),
    )


class MockHTTPClient:
    """Mock HTTP client that returns predefined responses based on method and path.

    Supports:
    - Setting responses for specific method/path combinations
    - Recording all HTTP calls for verification
    - Default successful response for unconfigured paths
    """

    def __init__(self):
        self.responses: dict[tuple[str, str], httpx.Response] = {}
        self.calls: list[dict] = []
        self.is_closed = False

    def set_response(self, method: str, path_contains: str, response: httpx.Response):
        """Set a response for a specific method/path combination."""
        self.responses[(method.upper(), path_contains)] = response

    async def request(self, method: str, url: str, **kwargs) -> httpx.Response:
        """Mock request that returns configured responses."""
        self.calls.append({"method": method, "url": url, "kwargs": kwargs})

        # Try to find a matching response (more specific paths first)
        for (m, path), response in sorted(self.responses.items(), key=lambda x: len(x[0][1]), reverse=True):
            if method.upper() == m and path in url:
                return response

        # Default successful response
        return make_response(200, {"success": True})

    async def aclose(self):
        """Close the mock client."""
        self.is_closed = True

    def get_calls_for(self, method: str, path_contains: str = "") -> list[dict]:
        """Get all recorded calls matching method and optional path substring."""
        return [c for c in self.calls if c["method"].upper() == method.upper() and path_contains in c["url"]]

    def assert_called_with(self, method: str, path_contains: str):
        """Assert that at least one call was made with the given method and path."""
        matching = self.get_calls_for(method, path_contains)
        assert matching, f"No {method} call to '{path_contains}' found. Calls: {self.calls}"


@pytest.fixture
def mock_http_client():
    """Create a fresh mock HTTP client for each test."""
    return MockHTTPClient()


@pytest.fixture
def client_with_mock_http(mock_settings, mock_clusters_config, mock_http_client):
    """Create an ACP client with mocked HTTP transport."""
    with patch("mcp_acp.client.load_settings", return_value=mock_settings):
        with patch("mcp_acp.client.load_clusters_config", return_value=mock_clusters_config):
            client = ACPClient()
            client._http_client = mock_http_client
            return client


# ── Test: List Tools ─────────────────────────────────────────────────────────


class TestListToolsE2E:
    """Verify all expected tools are registered."""

    @pytest.mark.asyncio
    async def test_all_tools_registered(self):
        """All 26 tools should be available."""
        tools = await list_tools()
        assert len(tools) == 26

        tool_names = {t.name for t in tools}
        expected_tools = {
            # Session management
            "acp_list_sessions",
            "acp_get_session",
            "acp_create_session",
            "acp_create_session_from_template",
            "acp_delete_session",
            "acp_restart_session",
            "acp_clone_session",
            "acp_update_session",
            # Observability
            "acp_get_session_logs",
            "acp_get_session_transcript",
            "acp_get_session_metrics",
            # Labels
            "acp_label_resource",
            "acp_unlabel_resource",
            "acp_list_sessions_by_label",
            "acp_bulk_label_resources",
            "acp_bulk_unlabel_resources",
            # Bulk operations
            "acp_bulk_delete_sessions",
            "acp_bulk_stop_sessions",
            "acp_bulk_restart_sessions",
            "acp_bulk_delete_sessions_by_label",
            "acp_bulk_stop_sessions_by_label",
            "acp_bulk_restart_sessions_by_label",
            # Cluster management
            "acp_list_clusters",
            "acp_whoami",
            "acp_switch_cluster",
            "acp_login",
        }
        assert tool_names == expected_tools


# ── Test: Session Management Tools ───────────────────────────────────────────


class TestSessionManagementE2E:
    """E2E tests for session management tools."""

    @pytest.mark.asyncio
    async def test_list_sessions(self, client_with_mock_http, mock_http_client):
        """Test listing sessions with HTTP mock."""
        mock_http_client.set_response(
            "GET",
            "/v1/sessions",
            make_response(
                200,
                {
                    "items": [
                        {
                            "id": "session-001",
                            "status": "running",
                            "displayName": "Test Session",
                            "createdAt": "2024-01-15T10:30:00Z",
                        },
                        {
                            "id": "session-002",
                            "status": "stopped",
                            "displayName": "Stopped Session",
                            "createdAt": "2024-01-14T08:00:00Z",
                        },
                    ]
                },
            ),
        )

        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool("acp_list_sessions", {"project": "test-project"})

        assert len(result) == 1
        assert "session-001" in result[0].text
        assert "session-002" in result[0].text

    @pytest.mark.asyncio
    async def test_list_sessions_with_filters(self, client_with_mock_http, mock_http_client):
        """Test listing sessions with status filter."""
        mock_http_client.set_response(
            "GET",
            "/v1/sessions",
            make_response(
                200,
                {
                    "items": [
                        {"id": "running-session", "status": "running", "createdAt": "2024-01-15T10:30:00Z"},
                        {"id": "stopped-session", "status": "stopped", "createdAt": "2024-01-14T08:00:00Z"},
                    ]
                },
            ),
        )

        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool("acp_list_sessions", {"project": "test-project", "status": "running", "limit": 10})

        assert len(result) == 1
        # Only running session should be shown after client-side filter
        assert "running-session" in result[0].text

    @pytest.mark.asyncio
    async def test_get_session(self, client_with_mock_http, mock_http_client):
        """Test getting a single session."""
        mock_http_client.set_response(
            "GET",
            "/v1/sessions/session-001",
            make_response(
                200,
                {
                    "id": "session-001",
                    "status": "running",
                    "displayName": "My Test Session",
                    "model": "claude-sonnet-4",
                    "createdAt": "2024-01-15T10:30:00Z",
                    "task": "Fix the bug in auth module",
                },
            ),
        )

        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool("acp_get_session", {"project": "test-project", "session": "session-001"})

        assert len(result) == 1
        assert "session-001" in result[0].text
        assert "running" in result[0].text.lower() or "My Test Session" in result[0].text

    @pytest.mark.asyncio
    async def test_create_session(self, client_with_mock_http, mock_http_client):
        """Test creating a new session."""
        mock_http_client.set_response(
            "POST",
            "/v1/sessions",
            make_response(
                201,
                {
                    "id": "new-session-xyz",
                    "status": "creating",
                    "displayName": "Bug Fix Session",
                },
            ),
        )

        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool(
                "acp_create_session",
                {
                    "project": "test-project",
                    "initial_prompt": "Fix the authentication bug",
                    "display_name": "Bug Fix Session",
                    "model": "claude-sonnet-4",
                },
            )

        assert len(result) == 1
        assert "new-session-xyz" in result[0].text

    @pytest.mark.asyncio
    async def test_create_session_dry_run(self, client_with_mock_http, mock_http_client):
        """Test create session dry run mode."""
        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool(
                "acp_create_session",
                {
                    "project": "test-project",
                    "initial_prompt": "Test prompt",
                    "dry_run": True,
                },
            )

        assert len(result) == 1
        assert "dry" in result[0].text.lower() or "would" in result[0].text.lower()
        # No HTTP calls should be made for dry run
        assert len([c for c in mock_http_client.calls if "POST" in c["method"]]) == 0

    @pytest.mark.asyncio
    async def test_create_session_from_template(self, client_with_mock_http, mock_http_client):
        """Test creating session from template."""
        mock_http_client.set_response(
            "POST",
            "/v1/sessions",
            make_response(201, {"id": "template-session-abc"}),
        )

        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool(
                "acp_create_session_from_template",
                {
                    "project": "test-project",
                    "template": "bugfix",
                    "display_name": "Fix auth bug",
                },
            )

        assert len(result) == 1
        assert "template-session-abc" in result[0].text

    @pytest.mark.asyncio
    async def test_delete_session(self, client_with_mock_http, mock_http_client):
        """Test deleting a session."""
        mock_http_client.set_response("DELETE", "/v1/sessions/session-to-delete", make_response(204))

        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool(
                "acp_delete_session",
                {"project": "test-project", "session": "session-to-delete"},
            )

        assert len(result) == 1
        assert "delete" in result[0].text.lower() or "success" in result[0].text.lower()

    @pytest.mark.asyncio
    async def test_delete_session_dry_run(self, client_with_mock_http, mock_http_client):
        """Test delete session dry run."""
        mock_http_client.set_response(
            "GET",
            "/v1/sessions/session-001",
            make_response(200, {"id": "session-001", "status": "running", "createdAt": "2024-01-15T10:30:00Z"}),
        )

        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool(
                "acp_delete_session",
                {"project": "test-project", "session": "session-001", "dry_run": True},
            )

        assert len(result) == 1
        assert "dry" in result[0].text.lower() or "would" in result[0].text.lower()

    @pytest.mark.asyncio
    async def test_restart_session(self, client_with_mock_http, mock_http_client):
        """Test restarting a stopped session."""
        mock_http_client.set_response(
            "PATCH",
            "/v1/sessions/stopped-session",
            make_response(200, {"id": "stopped-session", "status": "running"}),
        )

        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool(
                "acp_restart_session",
                {"project": "test-project", "session": "stopped-session"},
            )

        assert len(result) == 1
        assert "restart" in result[0].text.lower() or "success" in result[0].text.lower()

    @pytest.mark.asyncio
    async def test_clone_session(self, client_with_mock_http, mock_http_client):
        """Test cloning a session."""
        # First GET to fetch source session config
        mock_http_client.set_response(
            "GET",
            "/v1/sessions/source-session",
            make_response(
                200,
                {
                    "id": "source-session",
                    "task": "Original task",
                    "model": "claude-sonnet-4",
                    "repos": [{"url": "https://github.com/test/repo"}],
                },
            ),
        )
        # Then POST to create clone
        mock_http_client.set_response(
            "POST",
            "/v1/sessions",
            make_response(201, {"id": "cloned-session-xyz"}),
        )

        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool(
                "acp_clone_session",
                {
                    "project": "test-project",
                    "source_session": "source-session",
                    "new_display_name": "My Clone",
                },
            )

        assert len(result) == 1
        assert "cloned-session-xyz" in result[0].text

    @pytest.mark.asyncio
    async def test_update_session(self, client_with_mock_http, mock_http_client):
        """Test updating session metadata."""
        mock_http_client.set_response(
            "PATCH",
            "/v1/sessions/session-001",
            make_response(200, {"id": "session-001", "displayName": "New Name"}),
        )

        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool(
                "acp_update_session",
                {
                    "project": "test-project",
                    "session": "session-001",
                    "display_name": "New Name",
                    "timeout": 3600,
                },
            )

        assert len(result) == 1
        assert "update" in result[0].text.lower()


# ── Test: Observability Tools ────────────────────────────────────────────────


class TestObservabilityE2E:
    """E2E tests for observability tools (logs, transcript, metrics)."""

    @pytest.mark.asyncio
    async def test_get_session_logs(self, client_with_mock_http, mock_http_client):
        """Test retrieving session logs."""
        log_content = """2024-01-15 10:30:00 INFO Starting session
2024-01-15 10:30:01 INFO Loading model claude-sonnet-4
2024-01-15 10:30:05 INFO Processing initial prompt
2024-01-15 10:31:00 INFO Tool call: Read file.py"""

        mock_http_client.set_response(
            "GET",
            "/v1/sessions/session-001/logs",
            make_response(200, None, log_content),
        )

        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool(
                "acp_get_session_logs",
                {"project": "test-project", "session": "session-001", "tail_lines": 100},
            )

        assert len(result) == 1
        assert "Starting session" in result[0].text or "session-001" in result[0].text

    @pytest.mark.asyncio
    async def test_get_session_transcript(self, client_with_mock_http, mock_http_client):
        """Test retrieving session transcript."""
        mock_http_client.set_response(
            "GET",
            "/v1/sessions/session-001/transcript",
            make_response(
                200,
                {
                    "messages": [
                        {"role": "user", "content": "Fix the login bug"},
                        {"role": "assistant", "content": "I'll analyze the auth module..."},
                    ]
                },
            ),
        )

        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool(
                "acp_get_session_transcript",
                {"project": "test-project", "session": "session-001", "format": "json"},
            )

        assert len(result) == 1
        assert "login bug" in result[0].text or "auth module" in result[0].text

    @pytest.mark.asyncio
    async def test_get_session_metrics(self, client_with_mock_http, mock_http_client):
        """Test retrieving session metrics."""
        mock_http_client.set_response(
            "GET",
            "/v1/sessions/session-001/metrics",
            make_response(
                200,
                {
                    "total_tokens": 15000,
                    "input_tokens": 5000,
                    "output_tokens": 10000,
                    "duration_seconds": 300,
                    "tool_calls": 25,
                    "api_calls": 8,
                },
            ),
        )

        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool(
                "acp_get_session_metrics",
                {"project": "test-project", "session": "session-001"},
            )

        assert len(result) == 1
        # Check that metrics values appear in output
        text = result[0].text
        assert "15000" in text or "15,000" in text or "tokens" in text.lower()


# ── Test: Label Tools ────────────────────────────────────────────────────────


class TestLabelToolsE2E:
    """E2E tests for label management tools."""

    @pytest.mark.asyncio
    async def test_label_resource(self, client_with_mock_http, mock_http_client):
        """Test adding labels to a session."""
        mock_http_client.set_response(
            "PATCH",
            "/v1/sessions/session-001",
            make_response(200, {"id": "session-001", "labels": {"env": "prod", "team": "platform"}}),
        )

        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool(
                "acp_label_resource",
                {
                    "project": "test-project",
                    "name": "session-001",
                    "labels": {"env": "prod", "team": "platform"},
                },
            )

        assert len(result) == 1
        assert "label" in result[0].text.lower()

    @pytest.mark.asyncio
    async def test_unlabel_resource(self, client_with_mock_http, mock_http_client):
        """Test removing labels from a session."""
        mock_http_client.set_response(
            "PATCH",
            "/v1/sessions/session-001",
            make_response(200, {"id": "session-001"}),
        )

        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool(
                "acp_unlabel_resource",
                {
                    "project": "test-project",
                    "name": "session-001",
                    "label_keys": ["env", "team"],
                },
            )

        assert len(result) == 1
        assert "label" in result[0].text.lower()

    @pytest.mark.asyncio
    async def test_list_sessions_by_label(self, client_with_mock_http, mock_http_client):
        """Test listing sessions filtered by labels."""
        mock_http_client.set_response(
            "GET",
            "/v1/sessions",
            make_response(
                200,
                {
                    "items": [
                        {"id": "labeled-session-1", "status": "running", "createdAt": "2024-01-15T10:30:00Z"},
                        {"id": "labeled-session-2", "status": "stopped", "createdAt": "2024-01-14T08:00:00Z"},
                    ]
                },
            ),
        )

        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool(
                "acp_list_sessions_by_label",
                {"project": "test-project", "labels": {"env": "prod"}},
            )

        assert len(result) == 1
        assert "labeled-session" in result[0].text


# ── Test: Bulk Operations ────────────────────────────────────────────────────


class TestBulkOperationsE2E:
    """E2E tests for bulk operation tools."""

    @pytest.mark.asyncio
    async def test_bulk_delete_requires_confirm(self, client_with_mock_http):
        """Test that bulk delete requires confirmation."""
        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool(
                "acp_bulk_delete_sessions",
                {"project": "test-project", "sessions": ["s1", "s2"]},
            )

        assert "requires confirm=true" in result[0].text

    @pytest.mark.asyncio
    async def test_bulk_delete_with_confirm(self, client_with_mock_http, mock_http_client):
        """Test bulk delete with confirmation."""
        mock_http_client.set_response("DELETE", "/v1/sessions/s1", make_response(204))
        mock_http_client.set_response("DELETE", "/v1/sessions/s2", make_response(204))

        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool(
                "acp_bulk_delete_sessions",
                {"project": "test-project", "sessions": ["s1", "s2"], "confirm": True},
            )

        assert len(result) == 1
        assert "deleted" in result[0].text.lower() or "2" in result[0].text

    @pytest.mark.asyncio
    async def test_bulk_delete_dry_run(self, client_with_mock_http, mock_http_client):
        """Test bulk delete dry run mode."""
        mock_http_client.set_response(
            "GET", "/v1/sessions/s1", make_response(200, {"id": "s1", "status": "running", "createdAt": "2024-01-01"})
        )
        mock_http_client.set_response(
            "GET", "/v1/sessions/s2", make_response(200, {"id": "s2", "status": "stopped", "createdAt": "2024-01-01"})
        )

        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool(
                "acp_bulk_delete_sessions",
                {"project": "test-project", "sessions": ["s1", "s2"], "dry_run": True},
            )

        assert len(result) == 1
        # Dry run should not require confirm
        assert "requires confirm" not in result[0].text

    @pytest.mark.asyncio
    async def test_bulk_stop_sessions(self, client_with_mock_http, mock_http_client):
        """Test bulk stop sessions."""
        mock_http_client.set_response("PATCH", "/v1/sessions/s1", make_response(200, {"id": "s1", "status": "stopped"}))
        mock_http_client.set_response("PATCH", "/v1/sessions/s2", make_response(200, {"id": "s2", "status": "stopped"}))

        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool(
                "acp_bulk_stop_sessions",
                {"project": "test-project", "sessions": ["s1", "s2"], "confirm": True},
            )

        assert len(result) == 1
        assert "stopped" in result[0].text.lower() or "2" in result[0].text

    @pytest.mark.asyncio
    async def test_bulk_restart_sessions(self, client_with_mock_http, mock_http_client):
        """Test bulk restart sessions."""
        mock_http_client.set_response("PATCH", "/v1/sessions/s1", make_response(200, {"id": "s1", "status": "running"}))
        mock_http_client.set_response("PATCH", "/v1/sessions/s2", make_response(200, {"id": "s2", "status": "running"}))

        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool(
                "acp_bulk_restart_sessions",
                {"project": "test-project", "sessions": ["s1", "s2"], "confirm": True},
            )

        assert len(result) == 1
        assert "restart" in result[0].text.lower() or "2" in result[0].text

    @pytest.mark.asyncio
    async def test_bulk_label_resources(self, client_with_mock_http, mock_http_client):
        """Test bulk labeling."""
        mock_http_client.set_response("PATCH", "/v1/sessions/s1", make_response(200, {"id": "s1"}))
        mock_http_client.set_response("PATCH", "/v1/sessions/s2", make_response(200, {"id": "s2"}))

        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool(
                "acp_bulk_label_resources",
                {
                    "project": "test-project",
                    "sessions": ["s1", "s2"],
                    "labels": {"env": "test"},
                    "confirm": True,
                },
            )

        assert len(result) == 1

    @pytest.mark.asyncio
    async def test_bulk_unlabel_resources(self, client_with_mock_http, mock_http_client):
        """Test bulk unlabeling."""
        mock_http_client.set_response("PATCH", "/v1/sessions/s1", make_response(200, {"id": "s1"}))
        mock_http_client.set_response("PATCH", "/v1/sessions/s2", make_response(200, {"id": "s2"}))

        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool(
                "acp_bulk_unlabel_resources",
                {
                    "project": "test-project",
                    "sessions": ["s1", "s2"],
                    "label_keys": ["env"],
                    "confirm": True,
                },
            )

        assert len(result) == 1


# ── Test: Bulk Operations by Label ───────────────────────────────────────────


class TestBulkByLabelE2E:
    """E2E tests for bulk operations filtered by label."""

    @pytest.mark.asyncio
    async def test_bulk_delete_by_label(self, client_with_mock_http, mock_http_client):
        """Test bulk delete sessions matching labels."""
        # List sessions by label
        mock_http_client.set_response(
            "GET",
            "/v1/sessions",
            make_response(
                200,
                {
                    "items": [
                        {"id": "labeled-1", "status": "stopped", "createdAt": "2024-01-01"},
                        {"id": "labeled-2", "status": "stopped", "createdAt": "2024-01-01"},
                    ]
                },
            ),
        )
        mock_http_client.set_response("DELETE", "/v1/sessions/labeled-1", make_response(204))
        mock_http_client.set_response("DELETE", "/v1/sessions/labeled-2", make_response(204))

        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool(
                "acp_bulk_delete_sessions_by_label",
                {"project": "test-project", "labels": {"env": "test"}, "confirm": True},
            )

        assert len(result) == 1
        assert "deleted" in result[0].text.lower()

    @pytest.mark.asyncio
    async def test_bulk_stop_by_label(self, client_with_mock_http, mock_http_client):
        """Test bulk stop sessions matching labels."""
        mock_http_client.set_response(
            "GET",
            "/v1/sessions",
            make_response(200, {"items": [{"id": "running-1", "status": "running", "createdAt": "2024-01-01"}]}),
        )
        mock_http_client.set_response(
            "PATCH", "/v1/sessions/running-1", make_response(200, {"id": "running-1", "status": "stopped"})
        )

        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool(
                "acp_bulk_stop_sessions_by_label",
                {"project": "test-project", "labels": {"team": "qa"}, "confirm": True},
            )

        assert len(result) == 1
        assert "stopped" in result[0].text.lower()

    @pytest.mark.asyncio
    async def test_bulk_restart_by_label(self, client_with_mock_http, mock_http_client):
        """Test bulk restart sessions matching labels."""
        mock_http_client.set_response(
            "GET",
            "/v1/sessions",
            make_response(200, {"items": [{"id": "stopped-1", "status": "stopped", "createdAt": "2024-01-01"}]}),
        )
        mock_http_client.set_response(
            "PATCH", "/v1/sessions/stopped-1", make_response(200, {"id": "stopped-1", "status": "running"})
        )

        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool(
                "acp_bulk_restart_sessions_by_label",
                {"project": "test-project", "labels": {"priority": "high"}, "confirm": True},
            )

        assert len(result) == 1
        assert "restart" in result[0].text.lower()


# ── Test: Cluster Management Tools ───────────────────────────────────────────


class TestClusterManagementE2E:
    """E2E tests for cluster management tools."""

    @pytest.mark.asyncio
    async def test_list_clusters(self, client_with_mock_http):
        """Test listing configured clusters."""
        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool("acp_list_clusters", {})

        assert len(result) == 1
        assert "test-cluster" in result[0].text

    @pytest.mark.asyncio
    async def test_whoami(self, client_with_mock_http):
        """Test whoami shows current config."""
        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool("acp_whoami", {})

        assert len(result) == 1
        text = result[0].text.lower()
        assert "cluster" in text or "authenticated" in text or "test" in text

    @pytest.mark.asyncio
    async def test_switch_cluster(self, client_with_mock_http):
        """Test switching cluster context."""
        # Add a second cluster to switch to
        second_cluster = MagicMock()
        second_cluster.server = "https://api.prod.example.com"
        second_cluster.default_project = "prod-project"
        second_cluster.token = "prod-token"
        client_with_mock_http.clusters_config.clusters["prod-cluster"] = second_cluster

        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool("acp_switch_cluster", {"cluster": "prod-cluster"})

        assert len(result) == 1
        assert "switched" in result[0].text.lower() or "prod" in result[0].text.lower()

    @pytest.mark.asyncio
    async def test_switch_cluster_unknown(self, client_with_mock_http):
        """Test switching to unknown cluster fails gracefully."""
        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool("acp_switch_cluster", {"cluster": "nonexistent"})

        assert len(result) == 1
        assert "unknown" in result[0].text.lower() or "not found" in result[0].text.lower()

    @pytest.mark.asyncio
    async def test_login(self, client_with_mock_http):
        """Test login to cluster."""
        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool(
                "acp_login",
                {"cluster": "test-cluster", "token": "new-auth-token"},
            )

        assert len(result) == 1
        assert "authenticated" in result[0].text.lower() or "success" in result[0].text.lower()


# ── Test: Error Handling ─────────────────────────────────────────────────────


class TestErrorHandlingE2E:
    """E2E tests for error handling scenarios."""

    @pytest.mark.asyncio
    async def test_http_error_response(self, client_with_mock_http, mock_http_client):
        """Test handling of HTTP error responses."""
        mock_http_client.set_response(
            "GET",
            "/v1/sessions/nonexistent",
            make_response(404, {"error": "Session not found"}),
        )

        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool(
                "acp_get_session",
                {"project": "test-project", "session": "nonexistent"},
            )

        assert len(result) == 1
        assert "error" in result[0].text.lower() or "not found" in result[0].text.lower()

    @pytest.mark.asyncio
    async def test_unknown_tool(self, client_with_mock_http):
        """Test calling an unknown tool."""
        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool("acp_unknown_tool", {})

        assert len(result) == 1
        assert "unknown tool" in result[0].text.lower()

    @pytest.mark.asyncio
    async def test_validation_error_invalid_project(self, client_with_mock_http):
        """Test validation error for invalid project name."""
        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool(
                "acp_list_sessions",
                {"project": "INVALID_PROJECT_NAME!@#"},
            )

        assert len(result) == 1
        # Should show validation error about invalid characters
        assert "error" in result[0].text.lower() or "invalid" in result[0].text.lower()

    @pytest.mark.asyncio
    async def test_bulk_operation_limit_exceeded(self, client_with_mock_http):
        """Test that bulk operations enforce max item limit."""
        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool(
                "acp_bulk_delete_sessions",
                {
                    "project": "test-project",
                    "sessions": ["s1", "s2", "s3", "s4", "s5"],  # More than max 3
                    "confirm": True,
                },
            )

        assert len(result) == 1
        # Should fail with limit error
        assert "limit" in result[0].text.lower() or "3" in result[0].text


# ── Test: Project Auto-fill ──────────────────────────────────────────────────


class TestProjectAutoFillE2E:
    """E2E tests for automatic project filling from cluster config."""

    @pytest.mark.asyncio
    async def test_project_auto_filled_from_cluster(self, client_with_mock_http, mock_http_client):
        """Test that project is auto-filled when not provided."""
        mock_http_client.set_response(
            "GET",
            "/v1/sessions",
            make_response(200, {"items": []}),
        )

        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            # Call without project - should use default from cluster config
            result = await call_tool("acp_list_sessions", {})

        assert len(result) == 1
        # Verify the request used the default project from cluster config
        assert any("test-project" in str(call.get("kwargs", {}).get("headers", {})) for call in mock_http_client.calls)


# ── Test: Complete Workflow ──────────────────────────────────────────────────


class TestCompleteWorkflowE2E:
    """E2E tests simulating complete user workflows."""

    @pytest.mark.asyncio
    async def test_session_lifecycle(self, client_with_mock_http, mock_http_client):
        """Test complete session lifecycle: create -> get -> update -> stop -> delete."""
        # 1. Create session
        mock_http_client.set_response("POST", "/v1/sessions", make_response(201, {"id": "lifecycle-session"}))

        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            create_result = await call_tool(
                "acp_create_session",
                {"project": "test-project", "initial_prompt": "Test workflow"},
            )
        assert "lifecycle-session" in create_result[0].text

        # 2. Get session details
        mock_http_client.set_response(
            "GET",
            "/v1/sessions/lifecycle-session",
            make_response(
                200, {"id": "lifecycle-session", "status": "running", "displayName": "Test", "createdAt": "2024-01-01"}
            ),
        )

        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            get_result = await call_tool(
                "acp_get_session",
                {"project": "test-project", "session": "lifecycle-session"},
            )
        assert "lifecycle-session" in get_result[0].text

        # 3. Update session
        mock_http_client.set_response(
            "PATCH",
            "/v1/sessions/lifecycle-session",
            make_response(200, {"id": "lifecycle-session", "displayName": "Updated Name"}),
        )

        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            update_result = await call_tool(
                "acp_update_session",
                {"project": "test-project", "session": "lifecycle-session", "display_name": "Updated Name"},
            )
        assert "update" in update_result[0].text.lower()

        # 4. Delete session
        mock_http_client.set_response("DELETE", "/v1/sessions/lifecycle-session", make_response(204))

        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            delete_result = await call_tool(
                "acp_delete_session",
                {"project": "test-project", "session": "lifecycle-session"},
            )
        assert "delete" in delete_result[0].text.lower() or "success" in delete_result[0].text.lower()


# ── Test: Additional Edge Cases ──────────────────────────────────────────────


class TestAdditionalEdgeCasesE2E:
    """Additional edge case tests for comprehensive coverage."""

    @pytest.mark.asyncio
    async def test_create_session_with_repos(self, client_with_mock_http, mock_http_client):
        """Test creating a session with repository URLs."""
        mock_http_client.set_response("POST", "/v1/sessions", make_response(201, {"id": "repo-session"}))

        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool(
                "acp_create_session",
                {
                    "project": "test-project",
                    "initial_prompt": "Review the code",
                    "repos": ["https://github.com/org/repo1", "https://github.com/org/repo2"],
                },
            )

        assert len(result) == 1
        assert "repo-session" in result[0].text
        mock_http_client.assert_called_with("POST", "/v1/sessions")

    @pytest.mark.asyncio
    async def test_invalid_template_name(self, client_with_mock_http):
        """Test validation error for invalid template name."""
        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool(
                "acp_create_session_from_template",
                {
                    "project": "test-project",
                    "template": "nonexistent-template",
                    "display_name": "Test",
                },
            )

        assert len(result) == 1
        assert "unknown template" in result[0].text.lower() or "error" in result[0].text.lower()

    @pytest.mark.asyncio
    async def test_update_session_no_fields(self, client_with_mock_http):
        """Test update session fails when no fields provided."""
        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool(
                "acp_update_session",
                {"project": "test-project", "session": "session-001"},
            )

        assert len(result) == 1
        assert "no fields" in result[0].text.lower() or "error" in result[0].text.lower()

    @pytest.mark.asyncio
    async def test_bulk_by_label_no_matches(self, client_with_mock_http, mock_http_client):
        """Test bulk operation by label when no sessions match."""
        mock_http_client.set_response("GET", "/v1/sessions", make_response(200, {"items": []}))

        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool(
                "acp_bulk_delete_sessions_by_label",
                {"project": "test-project", "labels": {"env": "nonexistent"}, "confirm": True},
            )

        assert len(result) == 1
        assert "no sessions" in result[0].text.lower() or "0" in result[0].text

    @pytest.mark.asyncio
    async def test_empty_sessions_list(self, client_with_mock_http, mock_http_client):
        """Test listing sessions returns empty list gracefully."""
        mock_http_client.set_response("GET", "/v1/sessions", make_response(200, {"items": []}))

        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            result = await call_tool("acp_list_sessions", {"project": "test-project"})

        assert len(result) == 1
        assert "0" in result[0].text or "no" in result[0].text.lower()

    @pytest.mark.asyncio
    async def test_delete_verifies_http_call(self, client_with_mock_http, mock_http_client):
        """Test delete session makes correct HTTP call."""
        mock_http_client.set_response("DELETE", "/v1/sessions/verify-delete", make_response(204))

        with patch("mcp_acp.server.get_client", return_value=client_with_mock_http):
            await call_tool(
                "acp_delete_session",
                {"project": "test-project", "session": "verify-delete"},
            )

        mock_http_client.assert_called_with("DELETE", "/v1/sessions/verify-delete")
