package server

import (
	"context"
	"encoding/json"
	"fmt"
	"net"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/mark3labs/mcp-go/client"
	"github.com/mark3labs/mcp-go/client/transport"
	"github.com/mark3labs/mcp-go/mcp"
	mcpserver "github.com/mark3labs/mcp-go/server"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/zap"

	"github.com/smart-mcp-proxy/mcpproxy-go/internal/config"
	"github.com/smart-mcp-proxy/mcpproxy-go/internal/contracts"
)

// TestEnvironment holds all test dependencies
type TestEnvironment struct {
	t           *testing.T
	tempDir     string
	proxyServer *Server
	proxyAddr   string
	mockServers map[string]*MockUpstreamServer
	logger      *zap.Logger
	cleanup     func()
}

// MockUpstreamServer implements a mock MCP server for testing
type MockUpstreamServer struct {
	server     *mcpserver.MCPServer
	tools      []mcp.Tool
	addr       string
	httpServer *http.Server
	stopFunc   func() error
}

// NewTestEnvironment creates a complete test environment
func NewTestEnvironment(t *testing.T) *TestEnvironment {
	// Disable OAuth for e2e tests to avoid network calls to mock servers
	oldValue := os.Getenv("MCPPROXY_DISABLE_OAUTH")
	os.Setenv("MCPPROXY_DISABLE_OAUTH", "true")

	// Create temp directory
	tempDir, err := os.MkdirTemp("", "mcpproxy-e2e-*")
	require.NoError(t, err)

	// Create logger
	logger, err := zap.NewDevelopment()
	require.NoError(t, err)

	env := &TestEnvironment{
		t:           t,
		tempDir:     tempDir,
		mockServers: make(map[string]*MockUpstreamServer),
		logger:      logger,
	}

	// Create data directory with secure permissions (0700 required for Unix socket security)
	dataDir := filepath.Join(tempDir, "data")
	err = os.MkdirAll(dataDir, 0700)
	require.NoError(t, err)

	// Find available port for test server
	ln, err := net.Listen("tcp", ":0")
	require.NoError(t, err)
	testPort := ln.Addr().(*net.TCPAddr).Port
	ln.Close()

	// Create proxy server with test config
	quarantineDisabled := false
	cfg := &config.Config{
		DataDir:           dataDir,
		Listen:            fmt.Sprintf(":%d", testPort),
		APIKey:            "test-api-key-e2e", // Set explicit API key for E2E tests
		ToolResponseLimit: 10000,
		DisableManagement: false,
		ReadOnlyMode:      false,
		AllowServerAdd:    true,
		AllowServerRemove: true,
		EnablePrompts:     true,
		DebugSearch:       true,
		QuarantineEnabled: &quarantineDisabled, // Disable tool-level quarantine in E2E tests (tested separately)
	}

	env.proxyServer, err = NewServer(cfg, logger)
	require.NoError(t, err)

	// Start proxy server in background
	ctx := context.Background()
	err = env.proxyServer.StartServer(ctx)
	require.NoError(t, err)

	// Set proxy address using 127.0.0.1 instead of localhost for reliable connection
	// across all platforms (avoids IPv4/IPv6 resolution issues)
	env.proxyAddr = fmt.Sprintf("http://127.0.0.1:%d/mcp", testPort)
	require.NotEmpty(t, env.proxyAddr)

	// Wait for server to be ready
	env.waitForServerReady()

	env.cleanup = func() {
		// Stop mock servers
		for _, mockServer := range env.mockServers {
			if mockServer.stopFunc != nil {
				_ = mockServer.stopFunc()
			}
		}

		// Stop proxy server
		_ = env.proxyServer.StopServer()
		_ = env.proxyServer.Shutdown()

		// Remove temp directory
		os.RemoveAll(tempDir)

		// Restore original OAuth environment variable
		if oldValue == "" {
			os.Unsetenv("MCPPROXY_DISABLE_OAUTH")
		} else {
			os.Setenv("MCPPROXY_DISABLE_OAUTH", oldValue)
		}
	}

	return env
}

// Cleanup cleans up all test resources
func (env *TestEnvironment) Cleanup() {
	if env.cleanup != nil {
		env.cleanup()
	}
}

// waitForServerReady waits for the proxy server to be ready
func (env *TestEnvironment) waitForServerReady() {
	timeout := time.After(15 * time.Second)
	ticker := time.NewTicker(100 * time.Millisecond)
	defer ticker.Stop()

	for {
		select {
		case <-timeout:
			status := env.proxyServer.GetStatus()
			env.t.Fatalf("Timeout waiting for server to be ready. Status: %+v", status)
		case <-ticker.C:
			if env.proxyServer.IsRunning() {
				// Actually test if the HTTP server is accepting connections
				if env.testServerConnection() {
					// Give it a bit more time to fully initialize
					time.Sleep(500 * time.Millisecond)
					return
				}
			}
		}
	}
}

// testServerConnection tests if the server is actually accepting HTTP connections
func (env *TestEnvironment) testServerConnection() bool {
	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancel()

	req, err := http.NewRequestWithContext(ctx, "GET", env.proxyAddr, http.NoBody)
	if err != nil {
		return false
	}

	client := &http.Client{
		Timeout: 1 * time.Second,
	}

	resp, err := client.Do(req)
	if err != nil {
		return false
	}
	resp.Body.Close()

	// Any response (even an error response) means the server is accepting connections
	return true
}

// CreateMockUpstreamServer creates and starts a mock upstream MCP server
func (env *TestEnvironment) CreateMockUpstreamServer(name string, tools []mcp.Tool) *MockUpstreamServer {
	// Create MCP server
	mcpServer := mcpserver.NewMCPServer(
		name,
		"1.0.0-test",
		mcpserver.WithToolCapabilities(true),
	)

	mockServer := &MockUpstreamServer{
		server: mcpServer,
		tools:  tools,
	}

	// Register tools
	for i := range tools {
		toolCopy := tools[i] // Capture for closure
		mcpServer.AddTool(toolCopy, func(_ context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
			// Mock tool implementation
			result := map[string]interface{}{
				"tool":    toolCopy.Name,
				"args":    request.Params.Arguments,
				"server":  name,
				"success": true,
			}

			jsonResult, _ := json.Marshal(result)
			return mcp.NewToolResultText(string(jsonResult)), nil
		})
	}

	// Start HTTP server on random port
	streamableServer := mcpserver.NewStreamableHTTPServer(mcpServer)

	// Find available port
	ln, err := net.Listen("tcp", ":0")
	require.NoError(env.t, err)
	port := ln.Addr().(*net.TCPAddr).Port
	ln.Close()

	mockServer.addr = fmt.Sprintf("http://localhost:%d", port)

	// Create HTTP server
	httpServer := &http.Server{
		Addr:    fmt.Sprintf(":%d", port),
		Handler: streamableServer,
	}
	mockServer.httpServer = httpServer

	// Start server in background
	go func() {
		err := httpServer.ListenAndServe()
		if err != nil && err != http.ErrServerClosed {
			env.logger.Error("Mock server error", zap.Error(err))
		}
	}()

	// Wait for server to start
	time.Sleep(200 * time.Millisecond)

	mockServer.stopFunc = func() error {
		return httpServer.Shutdown(context.Background())
	}

	env.mockServers[name] = mockServer
	return mockServer
}

// CreateProxyClient creates an MCP client connected to the proxy server
func (env *TestEnvironment) CreateProxyClient() *client.Client {
	httpTransport, err := transport.NewStreamableHTTP(env.proxyAddr)
	require.NoError(env.t, err)

	mcpClient := client.NewClient(httpTransport)
	return mcpClient
}

// ConnectClient connects and initializes an MCP client
func (env *TestEnvironment) ConnectClient(mcpClient *client.Client) *mcp.InitializeResult {
	ctx := context.Background()

	err := mcpClient.Start(ctx)
	require.NoError(env.t, err)

	initRequest := mcp.InitializeRequest{}
	initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
	initRequest.Params.ClientInfo = mcp.Implementation{
		Name:    "mcpproxy-e2e-test",
		Version: "1.0.0",
	}
	initRequest.Params.Capabilities = mcp.ClientCapabilities{}

	serverInfo, err := mcpClient.Initialize(ctx, initRequest)
	require.NoError(env.t, err)

	return serverInfo
}

// Test: Basic server startup and initialization
func TestE2E_ServerStartup(t *testing.T) {
	env := NewTestEnvironment(t)
	defer env.Cleanup()

	// Test that server is running
	assert.True(t, env.proxyServer.IsRunning())
	assert.NotEmpty(t, env.proxyAddr)
}

// Test: MCP client connection to proxy
func TestE2E_ClientConnection(t *testing.T) {
	env := NewTestEnvironment(t)
	defer env.Cleanup()

	// Create and connect client
	mcpClient := env.CreateProxyClient()
	defer mcpClient.Close()

	serverInfo := env.ConnectClient(mcpClient)

	// Verify server info
	assert.Equal(t, "mcpproxy-go", serverInfo.ServerInfo.Name)
	assert.Equal(t, "1.0.0", serverInfo.ServerInfo.Version)
	assert.NotNil(t, serverInfo.Capabilities.Tools)
}

// Test: Tool discovery and listing
func TestE2E_ToolDiscovery(t *testing.T) {
	env := NewTestEnvironment(t)
	defer env.Cleanup()

	// Create mock upstream server with tools
	mockTools := []mcp.Tool{
		{
			Name:        "test_tool_1",
			Description: "A test tool for testing",
			InputSchema: mcp.ToolInputSchema{
				Type: "object",
				Properties: map[string]interface{}{
					"param1": map[string]interface{}{
						"type":        "string",
						"description": "Test parameter",
					},
				},
			},
		},
		{
			Name:        "test_tool_2",
			Description: "Another test tool",
			InputSchema: mcp.ToolInputSchema{
				Type: "object",
				Properties: map[string]interface{}{
					"param2": map[string]interface{}{
						"type":        "number",
						"description": "Numeric parameter",
					},
				},
			},
		},
	}

	mockServer := env.CreateMockUpstreamServer("testserver", mockTools)

	// Connect client to proxy
	mcpClient := env.CreateProxyClient()
	defer mcpClient.Close()
	env.ConnectClient(mcpClient)

	// Add upstream server to proxy using the same pattern as fixtures_test.go
	ctx := context.Background()
	addRequest := mcp.CallToolRequest{}
	addRequest.Params.Name = "upstream_servers"
	addRequest.Params.Arguments = map[string]interface{}{
		"operation": "add",
		"name":      "testserver",
		"url":       mockServer.addr,
		"protocol":  "streamable-http",
		"enabled":   true,
	}

	result, err := mcpClient.CallTool(ctx, addRequest)
	require.NoError(t, err)
	assert.False(t, result.IsError)

	// Unquarantine the server for testing (bypassing security restrictions)
	serverConfig, err := env.proxyServer.runtime.StorageManager().GetUpstreamServer("testserver")
	require.NoError(t, err)
	serverConfig.Quarantined = false
	err = env.proxyServer.runtime.StorageManager().SaveUpstreamServer(serverConfig)
	require.NoError(t, err)

	// Get all servers from storage and reload configuration
	// This properly triggers supervisor reconciliation and creates the client
	servers, err := env.proxyServer.runtime.StorageManager().ListUpstreamServers()
	require.NoError(t, err)

	// Update runtime config with the unquarantined server
	cfg := env.proxyServer.runtime.Config()
	cfg.Servers = servers
	err = env.proxyServer.runtime.LoadConfiguredServers(cfg)
	require.NoError(t, err)

	// Wait for supervisor to reconcile and client to connect
	time.Sleep(3 * time.Second)

	// Manually trigger tool discovery and indexing
	_ = env.proxyServer.runtime.DiscoverAndIndexTools(ctx)

	// Wait for tools to be discovered and indexed
	time.Sleep(3 * time.Second)

	// Use retrieve_tools to search for tools
	searchRequest := mcp.CallToolRequest{}
	searchRequest.Params.Name = "retrieve_tools"
	searchRequest.Params.Arguments = map[string]interface{}{
		"query": "test tool",
		"limit": 10,
	}

	searchResult, err := mcpClient.CallTool(ctx, searchRequest)
	require.NoError(t, err)
	assert.False(t, searchResult.IsError)

	// Parse and verify search results
	require.Greater(t, len(searchResult.Content), 0)
	// Content is an array of mcp.Content, get the text from the first one
	var contentText string
	if len(searchResult.Content) > 0 {
		contentBytes, err := json.Marshal(searchResult.Content[0])
		require.NoError(t, err)
		var contentMap map[string]interface{}
		err = json.Unmarshal(contentBytes, &contentMap)
		require.NoError(t, err)
		if text, ok := contentMap["text"].(string); ok {
			contentText = text
		}
	}

	var searchResponse map[string]interface{}
	err = json.Unmarshal([]byte(contentText), &searchResponse)
	require.NoError(t, err)

	tools, ok := searchResponse["tools"].([]interface{})
	require.True(t, ok)
	assert.GreaterOrEqual(t, len(tools), 2) // Should find both tools
}

// Test: Tool calling through proxy
func TestE2E_ToolCalling(t *testing.T) {
	env := NewTestEnvironment(t)
	defer env.Cleanup()

	// Create mock upstream server
	mockTools := []mcp.Tool{
		{
			Name:        "echo_tool",
			Description: "Echoes back the input",
			InputSchema: mcp.ToolInputSchema{
				Type: "object",
				Properties: map[string]interface{}{
					"message": map[string]interface{}{
						"type":        "string",
						"description": "Message to echo",
					},
				},
			},
		},
	}

	mockServer := env.CreateMockUpstreamServer("echoserver", mockTools)

	// Connect client and add upstream server
	mcpClient := env.CreateProxyClient()
	defer mcpClient.Close()
	env.ConnectClient(mcpClient)

	ctx := context.Background()

	// Add upstream server
	addRequest := mcp.CallToolRequest{}
	addRequest.Params.Name = "upstream_servers"
	addRequest.Params.Arguments = map[string]interface{}{
		"operation": "add",
		"name":      "echoserver",
		"url":       mockServer.addr,
		"protocol":  "streamable-http",
		"enabled":   true,
	}

	_, err := mcpClient.CallTool(ctx, addRequest)
	require.NoError(t, err)

	// Unquarantine the server for testing (bypassing security restrictions)
	serverConfig, err := env.proxyServer.runtime.StorageManager().GetUpstreamServer("echoserver")
	require.NoError(t, err)
	serverConfig.Quarantined = false
	err = env.proxyServer.runtime.StorageManager().SaveUpstreamServer(serverConfig)
	require.NoError(t, err)

	// Get all servers from storage and reload configuration
	// This properly triggers supervisor reconciliation and creates the client
	servers, err := env.proxyServer.runtime.StorageManager().ListUpstreamServers()
	require.NoError(t, err)

	// Update runtime config with the unquarantined server
	cfg := env.proxyServer.runtime.Config()
	cfg.Servers = servers
	err = env.proxyServer.runtime.LoadConfiguredServers(cfg)
	require.NoError(t, err)

	// Wait for supervisor to reconcile and client to connect
	time.Sleep(3 * time.Second)

	// Manually trigger tool discovery and indexing
	_ = env.proxyServer.runtime.DiscoverAndIndexTools(ctx)

	// Wait for tools to be discovered and indexed
	time.Sleep(3 * time.Second)

	// Call tool through proxy using call_tool_write with required intent
	callRequest := mcp.CallToolRequest{}
	callRequest.Params.Name = "call_tool_write"
	callRequest.Params.Arguments = map[string]interface{}{
		"name": "echoserver:echo_tool",
		"args": map[string]interface{}{
			"message": "Hello from e2e test!",
		},
		"intent": map[string]interface{}{
			"operation_type": "write",
		},
	}

	callResult, err := mcpClient.CallTool(ctx, callRequest)
	require.NoError(t, err)
	assert.False(t, callResult.IsError)

	// Verify result contains expected data
	require.Greater(t, len(callResult.Content), 0)
	// Extract text content
	var contentText string
	if len(callResult.Content) > 0 {
		contentBytes, err := json.Marshal(callResult.Content[0])
		require.NoError(t, err)
		var contentMap map[string]interface{}
		err = json.Unmarshal(contentBytes, &contentMap)
		require.NoError(t, err)
		if text, ok := contentMap["text"].(string); ok {
			contentText = text
		}
	}

	// After the fix for issue #368, the proxy forwards upstream content blocks
	// as-is. The mock server returned a single TextContent whose text is the
	// JSON-encoded echo payload, so contentText is that JSON directly (no more
	// double-wrapping in a {"content":[...]} envelope).
	var response map[string]interface{}
	err = json.Unmarshal([]byte(contentText), &response)
	require.NoError(t, err)

	assert.Equal(t, "echo_tool", response["tool"])
	assert.Equal(t, "echoserver", response["server"])
	assert.Equal(t, true, response["success"])
}

// Test: Server management operations
func TestE2E_ServerManagement(t *testing.T) {
	env := NewTestEnvironment(t)
	defer env.Cleanup()

	mcpClient := env.CreateProxyClient()
	defer mcpClient.Close()
	env.ConnectClient(mcpClient)

	ctx := context.Background()

	// Test list servers (should be empty initially)
	listRequest := mcp.CallToolRequest{}
	listRequest.Params.Name = "upstream_servers"
	listRequest.Params.Arguments = map[string]interface{}{
		"operation": "list",
	}

	listResult, err := mcpClient.CallTool(ctx, listRequest)
	require.NoError(t, err)
	assert.False(t, listResult.IsError)

	// Test add server
	mockServer := env.CreateMockUpstreamServer("testmgmt", []mcp.Tool{})

	addRequest := mcp.CallToolRequest{}
	addRequest.Params.Name = "upstream_servers"
	addRequest.Params.Arguments = map[string]interface{}{
		"operation": "add",
		"name":      "testmgmt",
		"url":       mockServer.addr,
		"protocol":  "streamable-http",
		"enabled":   true,
	}

	addResult, err := mcpClient.CallTool(ctx, addRequest)
	require.NoError(t, err)
	assert.False(t, addResult.IsError)

	// Test list servers again (should contain added server)
	listResult2, err := mcpClient.CallTool(ctx, listRequest)
	require.NoError(t, err)
	assert.False(t, listResult2.IsError)

	// Test remove server
	removeRequest := mcp.CallToolRequest{}
	removeRequest.Params.Name = "upstream_servers"
	removeRequest.Params.Arguments = map[string]interface{}{
		"operation": "remove",
		"name":      "testmgmt",
	}

	removeResult, err := mcpClient.CallTool(ctx, removeRequest)
	require.NoError(t, err)
	assert.False(t, removeResult.IsError)
}

// Test: Error handling and recovery
func TestE2E_ErrorHandling(t *testing.T) {
	env := NewTestEnvironment(t)
	defer env.Cleanup()

	mcpClient := env.CreateProxyClient()
	defer mcpClient.Close()
	env.ConnectClient(mcpClient)

	ctx := context.Background()

	// Test calling non-existent tool using call_tool_write with required intent
	callRequest := mcp.CallToolRequest{}
	callRequest.Params.Name = "call_tool_write"
	callRequest.Params.Arguments = map[string]interface{}{
		"name": "nonexistent:tool",
		"args": map[string]interface{}{},
		"intent": map[string]interface{}{
			"operation_type": "write",
		},
	}

	callResult, err := mcpClient.CallTool(ctx, callRequest)
	require.NoError(t, err)
	// Should return error but not crash
	assert.True(t, callResult.IsError || len(callResult.Content) > 0)

	// Test invalid server management operation
	invalidRequest := mcp.CallToolRequest{}
	invalidRequest.Params.Name = "upstream_servers"
	invalidRequest.Params.Arguments = map[string]interface{}{
		"operation": "invalid_operation",
	}

	invalidResult, err := mcpClient.CallTool(ctx, invalidRequest)
	require.NoError(t, err)
	assert.True(t, invalidResult.IsError)
}

// Test: Concurrent client operations
func TestE2E_ConcurrentOperations(t *testing.T) {
	env := NewTestEnvironment(t)
	defer env.Cleanup()

	// Create multiple clients
	clients := make([]*client.Client, 3)
	for i := range clients {
		clients[i] = env.CreateProxyClient()
		env.ConnectClient(clients[i])
	}

	// Defer close all clients
	defer func() {
		for _, client := range clients {
			client.Close()
		}
	}()

	// Create mock server
	mockTools := []mcp.Tool{
		{
			Name:        "concurrent_tool",
			Description: "Tool for concurrent testing",
			InputSchema: mcp.ToolInputSchema{Type: "object"},
		},
	}
	mockServer := env.CreateMockUpstreamServer("concurrent", mockTools)

	ctx := context.Background()

	// Add server from first client
	addRequest := mcp.CallToolRequest{}
	addRequest.Params.Name = "upstream_servers"
	addRequest.Params.Arguments = map[string]interface{}{
		"operation": "add",
		"name":      "concurrent",
		"url":       mockServer.addr,
		"protocol":  "streamable-http",
		"enabled":   true,
	}

	_, err := clients[0].CallTool(ctx, addRequest)
	require.NoError(t, err)

	time.Sleep(1 * time.Second)

	// Perform concurrent operations
	done := make(chan bool, len(clients))

	for i, mcpClient := range clients {
		go func(clientIdx int, c *client.Client) {
			defer func() { done <- true }()

			// Each client performs retrieve_tools
			searchRequest := mcp.CallToolRequest{}
			searchRequest.Params.Name = "retrieve_tools"
			searchRequest.Params.Arguments = map[string]interface{}{
				"query": "concurrent",
				"limit": 5,
			}

			result, err := c.CallTool(ctx, searchRequest)
			assert.NoError(t, err, "Client %d search failed", clientIdx)
			assert.False(t, result.IsError, "Client %d search returned error", clientIdx)
		}(i, mcpClient)
	}

	// Wait for all operations to complete
	for i := 0; i < len(clients); i++ {
		select {
		case <-done:
			// Success
		case <-time.After(10 * time.Second):
			t.Fatal("Timeout waiting for concurrent operations")
		}
	}
}

// Test: SSE Events endpoint functionality
func TestE2E_SSEEvents(t *testing.T) {
	env := NewTestEnvironment(t)
	defer env.Cleanup()

	// Test SSE connection with the initial API key from NewTestEnvironment
	testSSEConnection(t, env, "test-api-key-e2e")

	// Now test with different API key
	// Update config to include different API key
	cfg := env.proxyServer.runtime.Config()
	cfg.APIKey = "test-api-key-12345"

	// Test SSE with new correct API key
	testSSEConnection(t, env, "test-api-key-12345")

	// Test SSE with incorrect API key
	testSSEConnectionAuthFailure(t, env, "wrong-api-key")
}

// testSSEConnection tests SSE connection functionality
func testSSEConnection(t *testing.T, env *TestEnvironment, apiKey string) {
	listenAddr := env.proxyServer.GetListenAddress()
	if listenAddr == "" {
		listenAddr = ":8080" // fallback
	}

	// Parse the listen address to handle IPv6 format
	var sseURL string
	if strings.HasPrefix(listenAddr, "[::]:") {
		// IPv6 format [::]:port -> localhost:port
		port := strings.TrimPrefix(listenAddr, "[::]:")
		sseURL = fmt.Sprintf("http://localhost:%s/events", port)
	} else if strings.HasPrefix(listenAddr, ":") {
		// Port only format :port -> localhost:port
		port := strings.TrimPrefix(listenAddr, ":")
		sseURL = fmt.Sprintf("http://localhost:%s/events", port)
	} else {
		// Full address format
		sseURL = fmt.Sprintf("http://%s/events", listenAddr)
	}

	if apiKey != "" {
		sseURL += "?apikey=" + apiKey
	}

	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	// Create HTTP client with very short timeout to avoid hanging on SSE stream
	client := &http.Client{
		Timeout: 500 * time.Millisecond, // Very short timeout
	}

	// Test that SSE endpoint accepts GET connections
	// The connection will timeout quickly, but we can check the initial response
	req, err := http.NewRequestWithContext(ctx, "GET", sseURL, http.NoBody)
	require.NoError(t, err)
	req.Header.Set("Accept", "text/event-stream")

	resp, err := client.Do(req)

	// We expect either:
	// 1. A successful connection (200) that times out
	// 2. A timeout error (which indicates the connection was established)
	if err != nil && resp == nil {
		// Connection timeout is expected for SSE - this means the endpoint is working
		t.Logf("✅ SSE endpoint connection established (timed out as expected): %s", sseURL)
		return
	}

	if resp != nil {
		defer resp.Body.Close()
		// If we get a response, it should be 200 OK
		assert.Equal(t, 200, resp.StatusCode, "SSE endpoint should return 200 OK")
		t.Logf("✅ SSE endpoint accessible with status %d at %s", resp.StatusCode, sseURL)
	}
}

// testSSEConnectionAuthFailure tests SSE connection with invalid authentication
func testSSEConnectionAuthFailure(t *testing.T, env *TestEnvironment, wrongAPIKey string) {
	listenAddr := env.proxyServer.GetListenAddress()
	if listenAddr == "" {
		listenAddr = ":8080" // fallback
	}

	// Parse the listen address to handle IPv6 format
	var sseURL string
	if strings.HasPrefix(listenAddr, "[::]:") {
		// IPv6 format [::]:port -> localhost:port
		port := strings.TrimPrefix(listenAddr, "[::]:")
		sseURL = fmt.Sprintf("http://localhost:%s/events?apikey=%s", port, wrongAPIKey)
	} else if strings.HasPrefix(listenAddr, ":") {
		// Port only format :port -> localhost:port
		port := strings.TrimPrefix(listenAddr, ":")
		sseURL = fmt.Sprintf("http://localhost:%s/events?apikey=%s", port, wrongAPIKey)
	} else {
		// Full address format
		sseURL = fmt.Sprintf("http://%s/events?apikey=%s", listenAddr, wrongAPIKey)
	}

	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()

	client := &http.Client{
		Timeout: 2 * time.Second,
	}

	req, err := http.NewRequestWithContext(ctx, "GET", sseURL, http.NoBody)
	require.NoError(t, err)

	resp, err := client.Do(req)

	// For authentication failures, we should get an immediate 401 response
	if err != nil {
		t.Fatalf("Expected immediate auth failure response, got error: %v", err)
	}

	require.NotNil(t, resp, "Expected HTTP response for auth failure")
	defer resp.Body.Close()

	// Should receive 401 Unauthorized when API key is wrong
	assert.Equal(t, 401, resp.StatusCode, "SSE endpoint should return 401 for invalid API key")
}

// Test: Add single upstream server with command-based configuration
func TestE2E_AddUpstreamServerCommand(t *testing.T) {
	env := NewTestEnvironment(t)
	defer env.Cleanup()

	mcpClient := env.CreateProxyClient()
	defer mcpClient.Close()
	env.ConnectClient(mcpClient)

	ctx := context.Background()

	// Test adding a command-based server (using echo to avoid external dependencies).
	// Pass quarantined=true explicitly: the E2E test env disables QuarantineEnabled
	// globally to skip Spec 032 tool-level approval, but this test specifically
	// verifies the quarantine-by-default server-level behaviour.
	addRequest := mcp.CallToolRequest{}
	addRequest.Params.Name = "upstream_servers"
	addRequest.Params.Arguments = map[string]interface{}{
		"operation": "add",
		"name":      "test-command-server",
		"command":   "echo",
		"args": []interface{}{
			"test-mcp-server",
		},
		"env": map[string]interface{}{
			"TEST_KEY": "test_value_123",
		},
		"enabled":     false, // Disabled to prevent actual connection attempts
		"quarantined": true,
	}

	addResult, err := mcpClient.CallTool(ctx, addRequest)
	require.NoError(t, err)
	if addResult.IsError {
		t.Logf("Add operation failed with error: %v", addResult)
	}
	assert.False(t, addResult.IsError, "Add operation should succeed")

	// Parse the result
	require.Greater(t, len(addResult.Content), 0)
	t.Logf("Add result content: %+v", addResult.Content)
	var contentText string
	if len(addResult.Content) > 0 {
		contentBytes, err := json.Marshal(addResult.Content[0])
		require.NoError(t, err)
		t.Logf("Content bytes: %s", string(contentBytes))
		var contentMap map[string]interface{}
		err = json.Unmarshal(contentBytes, &contentMap)
		require.NoError(t, err)
		if text, ok := contentMap["text"].(string); ok {
			contentText = text
		}
		t.Logf("Content text: %s", contentText)
	}

	var addResponse map[string]interface{}
	err = json.Unmarshal([]byte(contentText), &addResponse)
	require.NoError(t, err)

	// Verify the operation was successful
	assert.Equal(t, "configured", addResponse["status"])
	assert.Equal(t, "disabled", addResponse["connection_status"]) // Server disabled, so connection is disabled
	assert.Contains(t, addResponse["message"], "test-command-server")
	assert.Equal(t, true, addResponse["quarantined"]) // Server should be quarantined by default
	assert.Equal(t, false, addResponse["enabled"])    // Server should be disabled as configured

	// Verify the server configuration by listing
	listRequest := mcp.CallToolRequest{}
	listRequest.Params.Name = "upstream_servers"
	listRequest.Params.Arguments = map[string]interface{}{
		"operation": "list",
	}

	listResult, err := mcpClient.CallTool(ctx, listRequest)
	require.NoError(t, err)
	assert.False(t, listResult.IsError)

	// Parse list result
	var listContentText string
	if len(listResult.Content) > 0 {
		contentBytes, err := json.Marshal(listResult.Content[0])
		require.NoError(t, err)
		var contentMap map[string]interface{}
		err = json.Unmarshal(contentBytes, &contentMap)
		require.NoError(t, err)
		if text, ok := contentMap["text"].(string); ok {
			listContentText = text
		}
	}

	var listResponse map[string]interface{}
	err = json.Unmarshal([]byte(listContentText), &listResponse)
	require.NoError(t, err)

	// Find and verify the server
	if servers, ok := listResponse["servers"].([]interface{}); ok {
		found := false
		for _, server := range servers {
			if serverMap, ok := server.(map[string]interface{}); ok {
				if name, ok := serverMap["name"].(string); ok && name == "test-command-server" {
					found = true
					// Verify key configuration properties, but not the command itself
					// as it's now wrapped in a shell.
					assert.Equal(t, "stdio", serverMap["protocol"])
					assert.Equal(t, false, serverMap["enabled"]) // Server should be disabled as configured

					// Verify environment variables
					if envVars, ok := serverMap["env"].(map[string]interface{}); ok {
						assert.Equal(t, "test_value_123", envVars["TEST_KEY"])
					}
					break
				}
			}
		}
		assert.True(t, found, "test-command-server should be found in the list")
	}

	// Test removal of the server
	removeRequest := mcp.CallToolRequest{}
	removeRequest.Params.Name = "upstream_servers"
	removeRequest.Params.Arguments = map[string]interface{}{
		"operation": "remove",
		"name":      "test-command-server",
	}

	removeResult, err := mcpClient.CallTool(ctx, removeRequest)
	require.NoError(t, err)
	assert.False(t, removeResult.IsError, "Remove operation should succeed")
}

// Test: Inspect quarantined server with temporary exemption
func TestE2E_InspectQuarantined(t *testing.T) {
	env := NewTestEnvironment(t)
	defer env.Cleanup()

	// Create MCP client
	mcpClient := env.CreateProxyClient()
	env.ConnectClient(mcpClient)
	defer mcpClient.Close()

	// Create mock server with some tools
	mockTools := []mcp.Tool{
		{
			Name:        "test_tool_1",
			Description: "First test tool",
			InputSchema: mcp.ToolInputSchema{Type: "object"},
		},
		{
			Name:        "test_tool_2",
			Description: "Second test tool",
			InputSchema: mcp.ToolInputSchema{Type: "object"},
		},
	}
	mockServer := env.CreateMockUpstreamServer("quarantined-server", mockTools)

	ctx := context.Background()

	// Add server quarantined. The E2E test env disables QuarantineEnabled
	// globally (to skip Spec 032 tool-level approval), so we must ask for
	// server-level quarantine explicitly here.
	addRequest := mcp.CallToolRequest{}
	addRequest.Params.Name = "upstream_servers"
	addRequest.Params.Arguments = map[string]interface{}{
		"operation":   "add",
		"name":        "quarantined-server",
		"url":         mockServer.addr,
		"protocol":    "streamable-http",
		"enabled":     true,
		"quarantined": true,
	}

	addResult, err := mcpClient.CallTool(ctx, addRequest)
	require.NoError(t, err)
	assert.False(t, addResult.IsError, "Add operation should succeed")

	// Wait for server to be added to storage (quarantined servers don't get clients created immediately)
	time.Sleep(500 * time.Millisecond)

	t.Log("🔍 Calling inspect_quarantined for quarantined-server...")

	// Call inspect_quarantined (use quarantine_security tool, not upstream_servers)
	inspectRequest := mcp.CallToolRequest{}
	inspectRequest.Params.Name = "quarantine_security"
	inspectRequest.Params.Arguments = map[string]interface{}{
		"operation": "inspect_quarantined",
		"name":      "quarantined-server",
	}

	inspectResult, err := mcpClient.CallTool(ctx, inspectRequest)
	require.NoError(t, err, "inspect_quarantined should not return error")

	// Debug: Print all content items with their types
	t.Logf("📋 Inspection result - IsError: %v, Content count: %d", inspectResult.IsError, len(inspectResult.Content))
	for i, content := range inspectResult.Content {
		t.Logf("Content[%d] type: %T", i, content)
		// Handle both pointer and value types
		if textContent, ok := content.(*mcp.TextContent); ok {
			t.Logf("Content[%d] text (pointer): %s", i, textContent.Text)
		} else if textContent, ok := content.(mcp.TextContent); ok {
			t.Logf("Content[%d] text (value): %s", i, textContent.Text)
		}
	}

	if inspectResult.IsError {
		// Print the error for debugging - handle both pointer and value types
		for _, content := range inspectResult.Content {
			if textContent, ok := content.(*mcp.TextContent); ok {
				t.Logf("❌ Error from inspect_quarantined (pointer): %s", textContent.Text)
			} else if textContent, ok := content.(mcp.TextContent); ok {
				t.Logf("❌ Error from inspect_quarantined (value): %s", textContent.Text)
			}
		}
		t.Fatal("inspect_quarantined returned an error - see logs above")
	}

	// Verify result contains tool data
	require.NotEmpty(t, inspectResult.Content, "Result should have content")

	// Verify the result contains information about the tools
	var resultText string
	for _, content := range inspectResult.Content {
		// Handle both pointer and value types
		if textContent, ok := content.(*mcp.TextContent); ok {
			resultText += textContent.Text
		} else if textContent, ok := content.(mcp.TextContent); ok {
			resultText += textContent.Text
		}
	}
	assert.Contains(t, resultText, "test_tool_1", "Result should mention test_tool_1")
	assert.Contains(t, resultText, "test_tool_2", "Result should mention test_tool_2")

	// After inspection, server should be disconnected again (exemption revoked)
	time.Sleep(1 * time.Second)

	// Now check if client exists and is disconnected
	upstreamManager := env.proxyServer.runtime.UpstreamManager()
	client, exists := upstreamManager.GetClient("quarantined-server")
	if exists {
		assert.False(t, client.IsConnected(), "Server should be disconnected after inspection")
	} else {
		t.Log("Client no longer exists after exemption revoked (acceptable)")
	}

	t.Log("✅ Test passed: Quarantine inspection with temporary exemption works correctly")
}

// TestE2E_UpdateServerEnvJson tests updating server env vars via env_json
func TestE2E_UpdateServerEnvJson(t *testing.T) {
	env := NewTestEnvironment(t)
	defer env.Cleanup()

	mcpClient := env.CreateProxyClient()
	defer mcpClient.Close()
	env.ConnectClient(mcpClient)

	ctx := context.Background()

	// Step 1: Add a server with initial env vars
	addRequest := mcp.CallToolRequest{}
	addRequest.Params.Name = "upstream_servers"
	addRequest.Params.Arguments = map[string]interface{}{
		"operation": "add",
		"name":      "env-test-server",
		"command":   "echo",
		"args_json": `["test"]`,
		"env_json":  `{"INITIAL_VAR": "initial_value", "SECOND_VAR": "second_value"}`,
		"enabled":   false,
	}

	addResult, err := mcpClient.CallTool(ctx, addRequest)
	require.NoError(t, err)
	assert.False(t, addResult.IsError, "Add operation should succeed")

	// Step 2: Update with new env_json (should do FULL REPLACEMENT)
	updateRequest := mcp.CallToolRequest{}
	updateRequest.Params.Name = "upstream_servers"
	updateRequest.Params.Arguments = map[string]interface{}{
		"operation": "update",
		"name":      "env-test-server",
		"env_json":  `{"NEW_VAR": "new_value"}`,
	}

	updateResult, err := mcpClient.CallTool(ctx, updateRequest)
	require.NoError(t, err)
	assert.False(t, updateResult.IsError, "Update operation should succeed")

	// Step 3: Verify via list that env was updated
	listRequest := mcp.CallToolRequest{}
	listRequest.Params.Name = "upstream_servers"
	listRequest.Params.Arguments = map[string]interface{}{
		"operation": "list",
	}

	listResult, err := mcpClient.CallTool(ctx, listRequest)
	require.NoError(t, err)
	assert.False(t, listResult.IsError)

	// Parse list result to verify env
	var listContentText string
	if len(listResult.Content) > 0 {
		contentBytes, _ := json.Marshal(listResult.Content[0])
		var contentMap map[string]interface{}
		json.Unmarshal(contentBytes, &contentMap)
		if text, ok := contentMap["text"].(string); ok {
			listContentText = text
		}
	}

	// The list should show env-test-server
	assert.Contains(t, listContentText, "env-test-server", "Server should be in list")

	// Step 4: Clean up - delete the test server
	deleteRequest := mcp.CallToolRequest{}
	deleteRequest.Params.Name = "upstream_servers"
	deleteRequest.Params.Arguments = map[string]interface{}{
		"operation": "remove",
		"name":      "env-test-server",
	}

	deleteResult, err := mcpClient.CallTool(ctx, deleteRequest)
	require.NoError(t, err)
	assert.False(t, deleteResult.IsError, "Delete operation should succeed")

	t.Log("✅ Test passed: Update server env_json works correctly")
}

// TestE2E_UpdateServerArgsJson tests updating server args via args_json
func TestE2E_UpdateServerArgsJson(t *testing.T) {
	env := NewTestEnvironment(t)
	defer env.Cleanup()

	mcpClient := env.CreateProxyClient()
	defer mcpClient.Close()
	env.ConnectClient(mcpClient)

	ctx := context.Background()

	// Step 1: Add a server with initial args
	addRequest := mcp.CallToolRequest{}
	addRequest.Params.Name = "upstream_servers"
	addRequest.Params.Arguments = map[string]interface{}{
		"operation": "add",
		"name":      "args-test-server",
		"command":   "echo",
		"args_json": `["initial-arg1", "initial-arg2"]`,
		"enabled":   false,
	}

	addResult, err := mcpClient.CallTool(ctx, addRequest)
	require.NoError(t, err)
	assert.False(t, addResult.IsError, "Add operation should succeed")

	// Step 2: Update with new args_json (should do FULL REPLACEMENT)
	updateRequest := mcp.CallToolRequest{}
	updateRequest.Params.Name = "upstream_servers"
	updateRequest.Params.Arguments = map[string]interface{}{
		"operation": "update",
		"name":      "args-test-server",
		"args_json": `["new-arg1", "new-arg2", "new-arg3"]`,
	}

	updateResult, err := mcpClient.CallTool(ctx, updateRequest)
	require.NoError(t, err)
	assert.False(t, updateResult.IsError, "Update operation should succeed")

	// Step 3: Clean up
	deleteRequest := mcp.CallToolRequest{}
	deleteRequest.Params.Name = "upstream_servers"
	deleteRequest.Params.Arguments = map[string]interface{}{
		"operation": "remove",
		"name":      "args-test-server",
	}

	deleteResult, err := mcpClient.CallTool(ctx, deleteRequest)
	require.NoError(t, err)
	assert.False(t, deleteResult.IsError, "Delete operation should succeed")

	t.Log("✅ Test passed: Update server args_json works correctly")
}

// TestE2E_UpdateServerHeadersJson tests updating server headers via headers_json
func TestE2E_UpdateServerHeadersJson(t *testing.T) {
	env := NewTestEnvironment(t)
	defer env.Cleanup()

	mcpClient := env.CreateProxyClient()
	defer mcpClient.Close()
	env.ConnectClient(mcpClient)

	ctx := context.Background()

	// Step 1: Add an HTTP server with initial headers
	addRequest := mcp.CallToolRequest{}
	addRequest.Params.Name = "upstream_servers"
	addRequest.Params.Arguments = map[string]interface{}{
		"operation":    "add",
		"name":         "headers-test-server",
		"url":          "http://localhost:9999/mcp",
		"protocol":     "http",
		"headers_json": `{"Authorization": "Bearer initial-token", "X-Initial": "value"}`,
		"enabled":      false,
	}

	addResult, err := mcpClient.CallTool(ctx, addRequest)
	require.NoError(t, err)
	assert.False(t, addResult.IsError, "Add operation should succeed")

	// Step 2: Update with new headers_json (should do FULL REPLACEMENT)
	updateRequest := mcp.CallToolRequest{}
	updateRequest.Params.Name = "upstream_servers"
	updateRequest.Params.Arguments = map[string]interface{}{
		"operation":    "update",
		"name":         "headers-test-server",
		"headers_json": `{"Authorization": "Bearer new-token", "X-New-Header": "new-value"}`,
	}

	updateResult, err := mcpClient.CallTool(ctx, updateRequest)
	require.NoError(t, err)
	assert.False(t, updateResult.IsError, "Update operation should succeed")

	// Step 3: Clean up
	deleteRequest := mcp.CallToolRequest{}
	deleteRequest.Params.Name = "upstream_servers"
	deleteRequest.Params.Arguments = map[string]interface{}{
		"operation": "remove",
		"name":      "headers-test-server",
	}

	deleteResult, err := mcpClient.CallTool(ctx, deleteRequest)
	require.NoError(t, err)
	assert.False(t, deleteResult.IsError, "Delete operation should succeed")

	t.Log("✅ Test passed: Update server headers_json works correctly")
}

// TestE2E_PatchServerEnvJson tests patch operation with env_json
func TestE2E_PatchServerEnvJson(t *testing.T) {
	env := NewTestEnvironment(t)
	defer env.Cleanup()

	mcpClient := env.CreateProxyClient()
	defer mcpClient.Close()
	env.ConnectClient(mcpClient)

	ctx := context.Background()

	// Step 1: Add a server with initial env vars
	addRequest := mcp.CallToolRequest{}
	addRequest.Params.Name = "upstream_servers"
	addRequest.Params.Arguments = map[string]interface{}{
		"operation": "add",
		"name":      "patch-env-test-server",
		"command":   "echo",
		"args_json": `["test"]`,
		"env_json":  `{"OLD_VAR": "old_value"}`,
		"enabled":   false,
	}

	addResult, err := mcpClient.CallTool(ctx, addRequest)
	require.NoError(t, err)
	assert.False(t, addResult.IsError, "Add operation should succeed")

	// Step 2: Patch with new env_json (should do FULL REPLACEMENT, same as update)
	patchRequest := mcp.CallToolRequest{}
	patchRequest.Params.Name = "upstream_servers"
	patchRequest.Params.Arguments = map[string]interface{}{
		"operation": "patch",
		"name":      "patch-env-test-server",
		"env_json":  `{"PATCHED_VAR": "patched_value"}`,
	}

	patchResult, err := mcpClient.CallTool(ctx, patchRequest)
	require.NoError(t, err)
	assert.False(t, patchResult.IsError, "Patch operation should succeed")

	// Step 3: Clean up
	deleteRequest := mcp.CallToolRequest{}
	deleteRequest.Params.Name = "upstream_servers"
	deleteRequest.Params.Arguments = map[string]interface{}{
		"operation": "remove",
		"name":      "patch-env-test-server",
	}

	deleteResult, err := mcpClient.CallTool(ctx, deleteRequest)
	require.NoError(t, err)
	assert.False(t, deleteResult.IsError, "Delete operation should succeed")

	t.Log("✅ Test passed: Patch server env_json works correctly")
}

// TestE2E_ClearEnvWithEmptyJson tests clearing env vars with empty JSON
func TestE2E_ClearEnvWithEmptyJson(t *testing.T) {
	env := NewTestEnvironment(t)
	defer env.Cleanup()

	mcpClient := env.CreateProxyClient()
	defer mcpClient.Close()
	env.ConnectClient(mcpClient)

	ctx := context.Background()

	// Step 1: Add a server with env vars
	addRequest := mcp.CallToolRequest{}
	addRequest.Params.Name = "upstream_servers"
	addRequest.Params.Arguments = map[string]interface{}{
		"operation": "add",
		"name":      "clear-env-test-server",
		"command":   "echo",
		"args_json": `["test"]`,
		"env_json":  `{"VAR1": "value1", "VAR2": "value2"}`,
		"enabled":   false,
	}

	addResult, err := mcpClient.CallTool(ctx, addRequest)
	require.NoError(t, err)
	assert.False(t, addResult.IsError, "Add operation should succeed")

	// Step 2: Clear env vars with empty JSON
	updateRequest := mcp.CallToolRequest{}
	updateRequest.Params.Name = "upstream_servers"
	updateRequest.Params.Arguments = map[string]interface{}{
		"operation": "update",
		"name":      "clear-env-test-server",
		"env_json":  `{}`,
	}

	updateResult, err := mcpClient.CallTool(ctx, updateRequest)
	require.NoError(t, err)
	assert.False(t, updateResult.IsError, "Update with empty env_json should succeed")

	// Step 3: Clean up
	deleteRequest := mcp.CallToolRequest{}
	deleteRequest.Params.Name = "upstream_servers"
	deleteRequest.Params.Arguments = map[string]interface{}{
		"operation": "remove",
		"name":      "clear-env-test-server",
	}

	deleteResult, err := mcpClient.CallTool(ctx, deleteRequest)
	require.NoError(t, err)
	assert.False(t, deleteResult.IsError, "Delete operation should succeed")

	t.Log("✅ Test passed: Clear env with empty JSON works correctly")
}

// Test: Intent Declaration Tool Variants (Spec 018)
// Tests that the three tool variants (call_tool_read, call_tool_write, call_tool_destructive) work correctly
func TestE2E_IntentDeclarationToolVariants(t *testing.T) {
	env := NewTestEnvironment(t)
	defer env.Cleanup()

	// Create mock upstream server
	mockTools := []mcp.Tool{
		{
			Name:        "read_data",
			Description: "Reads data without modification",
			InputSchema: mcp.ToolInputSchema{
				Type: "object",
				Properties: map[string]interface{}{
					"id": map[string]interface{}{
						"type":        "string",
						"description": "Data ID to read",
					},
				},
			},
		},
		{
			Name:        "write_data",
			Description: "Writes data to storage",
			InputSchema: mcp.ToolInputSchema{
				Type: "object",
				Properties: map[string]interface{}{
					"id": map[string]interface{}{
						"type":        "string",
						"description": "Data ID",
					},
					"value": map[string]interface{}{
						"type":        "string",
						"description": "Value to write",
					},
				},
			},
		},
		{
			Name:        "delete_data",
			Description: "Permanently deletes data",
			InputSchema: mcp.ToolInputSchema{
				Type: "object",
				Properties: map[string]interface{}{
					"id": map[string]interface{}{
						"type":        "string",
						"description": "Data ID to delete",
					},
				},
			},
		},
	}

	mockServer := env.CreateMockUpstreamServer("dataserver", mockTools)

	// Connect client and add upstream server
	mcpClient := env.CreateProxyClient()
	defer mcpClient.Close()
	env.ConnectClient(mcpClient)

	ctx := context.Background()

	// Add upstream server
	addRequest := mcp.CallToolRequest{}
	addRequest.Params.Name = "upstream_servers"
	addRequest.Params.Arguments = map[string]interface{}{
		"operation": "add",
		"name":      "dataserver",
		"url":       mockServer.addr,
		"protocol":  "streamable-http",
		"enabled":   true,
	}

	_, err := mcpClient.CallTool(ctx, addRequest)
	require.NoError(t, err)

	// Unquarantine the server for testing
	serverConfig, err := env.proxyServer.runtime.StorageManager().GetUpstreamServer("dataserver")
	require.NoError(t, err)
	serverConfig.Quarantined = false
	err = env.proxyServer.runtime.StorageManager().SaveUpstreamServer(serverConfig)
	require.NoError(t, err)

	// Reload configuration
	servers, err := env.proxyServer.runtime.StorageManager().ListUpstreamServers()
	require.NoError(t, err)
	cfg := env.proxyServer.runtime.Config()
	cfg.Servers = servers
	err = env.proxyServer.runtime.LoadConfiguredServers(cfg)
	require.NoError(t, err)

	// Wait for supervisor to reconcile and client to connect
	time.Sleep(3 * time.Second)

	// Trigger tool discovery and indexing
	_ = env.proxyServer.runtime.DiscoverAndIndexTools(ctx)
	time.Sleep(3 * time.Second)

	// Test 1: call_tool_read with matching intent
	t.Run("call_tool_read with matching intent succeeds", func(t *testing.T) {
		readRequest := mcp.CallToolRequest{}
		readRequest.Params.Name = contracts.ToolVariantRead
		readRequest.Params.Arguments = map[string]interface{}{
			"name": "dataserver:read_data",
			"args": map[string]interface{}{
				"id": "test-123",
			},
			"intent": map[string]interface{}{
				"operation_type": contracts.OperationTypeRead,
				"reason":         "Reading test data for verification",
			},
		}

		result, err := mcpClient.CallTool(ctx, readRequest)
		require.NoError(t, err)
		assert.False(t, result.IsError, "call_tool_read with matching intent should succeed")
		t.Log("✅ call_tool_read with matching intent succeeded")
	})

	// Test 2: call_tool_write with matching intent
	t.Run("call_tool_write with matching intent succeeds", func(t *testing.T) {
		writeRequest := mcp.CallToolRequest{}
		writeRequest.Params.Name = contracts.ToolVariantWrite
		writeRequest.Params.Arguments = map[string]interface{}{
			"name": "dataserver:write_data",
			"args": map[string]interface{}{
				"id":    "test-456",
				"value": "new value",
			},
			"intent": map[string]interface{}{
				"operation_type":   contracts.OperationTypeWrite,
				"data_sensitivity": contracts.DataSensitivityInternal,
				"reason":           "Updating test data",
			},
		}

		result, err := mcpClient.CallTool(ctx, writeRequest)
		require.NoError(t, err)
		assert.False(t, result.IsError, "call_tool_write with matching intent should succeed")
		t.Log("✅ call_tool_write with matching intent succeeded")
	})

	// Test 3: call_tool_destructive with matching intent
	t.Run("call_tool_destructive with matching intent succeeds", func(t *testing.T) {
		destructiveRequest := mcp.CallToolRequest{}
		destructiveRequest.Params.Name = contracts.ToolVariantDestructive
		destructiveRequest.Params.Arguments = map[string]interface{}{
			"name": "dataserver:delete_data",
			"args": map[string]interface{}{
				"id": "test-789",
			},
			"intent": map[string]interface{}{
				"operation_type":   contracts.OperationTypeDestructive,
				"data_sensitivity": contracts.DataSensitivityPrivate,
				"reason":           "User requested deletion",
			},
		}

		result, err := mcpClient.CallTool(ctx, destructiveRequest)
		require.NoError(t, err)
		assert.False(t, result.IsError, "call_tool_destructive with matching intent should succeed")
		t.Log("✅ call_tool_destructive with matching intent succeeded")
	})

	// Test 4: Intent operation_type is inferred from tool variant (any provided value is ignored)
	t.Run("call_tool_read infers operation_type from tool variant", func(t *testing.T) {
		// Even if operation_type is provided, it will be overwritten by the tool variant
		requestWithAnyIntent := mcp.CallToolRequest{}
		requestWithAnyIntent.Params.Name = contracts.ToolVariantRead
		requestWithAnyIntent.Params.Arguments = map[string]interface{}{
			"name": "dataserver:read_data",
			"args": map[string]interface{}{
				"id": "test-123",
			},
			"intent": map[string]interface{}{
				"operation_type": contracts.OperationTypeWrite, // Will be ignored, inferred as "read"
				"reason":         "Testing inference",
			},
		}

		result, err := mcpClient.CallTool(ctx, requestWithAnyIntent)
		require.NoError(t, err)
		assert.False(t, result.IsError, "call_tool_read should succeed - operation_type is inferred from tool variant")
		t.Log("✅ call_tool_read correctly infers operation_type from tool variant")
	})

	// Test 5: Missing intent should succeed (operation_type is inferred from tool variant)
	t.Run("call_tool_write without intent succeeds", func(t *testing.T) {
		noIntentRequest := mcp.CallToolRequest{}
		noIntentRequest.Params.Name = contracts.ToolVariantWrite
		noIntentRequest.Params.Arguments = map[string]interface{}{
			"name": "dataserver:write_data",
			"args": map[string]interface{}{
				"id":    "test-456",
				"value": "new value",
			},
			// No intent provided - operation_type will be inferred from tool variant
		}

		result, err := mcpClient.CallTool(ctx, noIntentRequest)
		require.NoError(t, err)
		assert.False(t, result.IsError, "call_tool_write without intent should succeed - operation_type is inferred")
		t.Log("✅ call_tool_write without intent succeeds with inferred operation_type")
	})

	t.Log("✅ All Intent Declaration tool variant tests passed")
}

// TestE2E_RequestID_InResponses tests that X-Request-Id header is present in all API responses
// and that error responses include request_id in the JSON body.
// Spec: 021-request-id-logging, User Story 1
func TestE2E_RequestID_InResponses(t *testing.T) {
	env := NewTestEnvironment(t)
	defer env.Cleanup()

	// Extract base URL from proxyAddr (which is "http://127.0.0.1:PORT/mcp")
	// We need just "http://127.0.0.1:PORT" for API endpoints
	baseURL := strings.TrimSuffix(env.proxyAddr, "/mcp")
	apiKey := "test-api-key-e2e"

	// Test 1: Success response includes X-Request-Id header
	t.Run("success response has X-Request-Id header", func(t *testing.T) {
		req, err := http.NewRequest("GET", baseURL+"/api/v1/status", nil)
		require.NoError(t, err)
		req.Header.Set("X-API-Key", apiKey)

		resp, err := http.DefaultClient.Do(req)
		require.NoError(t, err)
		defer resp.Body.Close()

		require.Equal(t, http.StatusOK, resp.StatusCode)

		// Verify X-Request-Id header is present
		requestID := resp.Header.Get("X-Request-Id")
		assert.NotEmpty(t, requestID, "X-Request-Id header should be present in success response")
		t.Logf("✅ Success response has X-Request-Id: %s", requestID)
	})

	// Test 2: Error response includes X-Request-Id header AND request_id in body
	t.Run("error response has X-Request-Id header and request_id in body", func(t *testing.T) {
		// Request a non-existent server to trigger a 404 error
		req, err := http.NewRequest("GET", baseURL+"/api/v1/servers/nonexistent-server-xyz/tools", nil)
		require.NoError(t, err)
		req.Header.Set("X-API-Key", apiKey)

		resp, err := http.DefaultClient.Do(req)
		require.NoError(t, err)
		defer resp.Body.Close()

		// Should be an error (404 or similar)
		require.GreaterOrEqual(t, resp.StatusCode, 400, "Should return an error status code")

		// Verify X-Request-Id header is present
		headerRequestID := resp.Header.Get("X-Request-Id")
		assert.NotEmpty(t, headerRequestID, "X-Request-Id header should be present in error response")

		// Verify request_id is in the JSON body
		var errorResp struct {
			Success   bool   `json:"success"`
			Error     string `json:"error"`
			RequestID string `json:"request_id"`
		}
		err = json.NewDecoder(resp.Body).Decode(&errorResp)
		require.NoError(t, err)

		assert.NotEmpty(t, errorResp.RequestID, "request_id should be present in error response body")
		assert.Equal(t, headerRequestID, errorResp.RequestID, "Header X-Request-Id and body request_id should match")
		t.Logf("✅ Error response has matching request_id: header=%s, body=%s", headerRequestID, errorResp.RequestID)
	})

	// Test 3: Client-provided X-Request-Id is echoed back
	t.Run("client-provided X-Request-Id is echoed back", func(t *testing.T) {
		clientRequestID := "my-custom-request-id-12345"

		req, err := http.NewRequest("GET", baseURL+"/api/v1/status", nil)
		require.NoError(t, err)
		req.Header.Set("X-API-Key", apiKey)
		req.Header.Set("X-Request-Id", clientRequestID)

		resp, err := http.DefaultClient.Do(req)
		require.NoError(t, err)
		defer resp.Body.Close()

		require.Equal(t, http.StatusOK, resp.StatusCode)

		// Verify the client-provided ID is echoed back
		responseRequestID := resp.Header.Get("X-Request-Id")
		assert.Equal(t, clientRequestID, responseRequestID, "Client-provided X-Request-Id should be echoed back")
		t.Logf("✅ Client-provided X-Request-Id echoed: sent=%s, received=%s", clientRequestID, responseRequestID)
	})

	// Test 4: Invalid X-Request-Id is replaced with generated UUID
	t.Run("invalid X-Request-Id is replaced with generated UUID", func(t *testing.T) {
		invalidRequestID := "invalid id with spaces and <special> chars!"

		req, err := http.NewRequest("GET", baseURL+"/api/v1/status", nil)
		require.NoError(t, err)
		req.Header.Set("X-API-Key", apiKey)
		req.Header.Set("X-Request-Id", invalidRequestID)

		resp, err := http.DefaultClient.Do(req)
		require.NoError(t, err)
		defer resp.Body.Close()

		require.Equal(t, http.StatusOK, resp.StatusCode)

		// Verify the invalid ID is NOT echoed back
		responseRequestID := resp.Header.Get("X-Request-Id")
		assert.NotEqual(t, invalidRequestID, responseRequestID, "Invalid X-Request-Id should not be echoed back")
		assert.NotEmpty(t, responseRequestID, "A generated X-Request-Id should be returned")

		// Should look like a UUID (contains dashes, proper length)
		assert.Contains(t, responseRequestID, "-", "Generated request ID should be a UUID with dashes")
		assert.Len(t, responseRequestID, 36, "Generated UUID should be 36 characters")
		t.Logf("✅ Invalid X-Request-Id replaced with UUID: sent=%q, received=%s", invalidRequestID, responseRequestID)
	})

	// Test 5: Very long X-Request-Id is replaced
	t.Run("too long X-Request-Id is replaced", func(t *testing.T) {
		longRequestID := strings.Repeat("a", 300) // 300 chars, exceeds 256 limit

		req, err := http.NewRequest("GET", baseURL+"/api/v1/status", nil)
		require.NoError(t, err)
		req.Header.Set("X-API-Key", apiKey)
		req.Header.Set("X-Request-Id", longRequestID)

		resp, err := http.DefaultClient.Do(req)
		require.NoError(t, err)
		defer resp.Body.Close()

		require.Equal(t, http.StatusOK, resp.StatusCode)

		// Verify the long ID is NOT echoed back
		responseRequestID := resp.Header.Get("X-Request-Id")
		assert.NotEqual(t, longRequestID, responseRequestID, "Too long X-Request-Id should not be echoed back")
		assert.LessOrEqual(t, len(responseRequestID), 256, "Response X-Request-Id should not exceed 256 chars")
		t.Logf("✅ Too long X-Request-Id replaced: sent length=%d, received=%s", len(longRequestID), responseRequestID)
	})

	t.Log("✅ All Request ID E2E tests passed")
}

// TestE2E_RequestID_ActivityFiltering tests that activities can be filtered by request_id
// via both API query parameter and CLI flag.
// Spec: 021-request-id-logging, User Story 4 (T030, T031)
func TestE2E_RequestID_ActivityFiltering(t *testing.T) {
	env := NewTestEnvironment(t)
	defer env.Cleanup()

	baseURL := strings.TrimSuffix(env.proxyAddr, "/mcp")
	apiKey := "test-api-key-e2e"

	// Step 1: Make a request with a known X-Request-Id
	knownRequestID := "test-request-id-for-activity-filtering-abc123"

	t.Run("API requests echo back X-Request-Id", func(t *testing.T) {
		// Make an API request with a known request ID to verify echo behavior
		req, err := http.NewRequest("GET", baseURL+"/api/v1/status", nil)
		require.NoError(t, err)
		req.Header.Set("X-API-Key", apiKey)
		req.Header.Set("X-Request-Id", knownRequestID)

		resp, err := http.DefaultClient.Do(req)
		require.NoError(t, err)
		defer resp.Body.Close()

		// Verify request was successful
		require.Equal(t, http.StatusOK, resp.StatusCode)

		// Verify X-Request-Id was echoed back
		responseRequestID := resp.Header.Get("X-Request-Id")
		assert.Equal(t, knownRequestID, responseRequestID)
		t.Logf("✅ API request echoed X-Request-Id: %s", knownRequestID)
	})

	// T031: Test API query param filtering
	t.Run("API request_id query param filters activities", func(t *testing.T) {
		// Query activities with the request_id filter
		req, err := http.NewRequest("GET", baseURL+"/api/v1/activity?request_id="+knownRequestID, nil)
		require.NoError(t, err)
		req.Header.Set("X-API-Key", apiKey)

		resp, err := http.DefaultClient.Do(req)
		require.NoError(t, err)
		defer resp.Body.Close()

		require.Equal(t, http.StatusOK, resp.StatusCode)

		var activityResp struct {
			Success bool `json:"success"`
			Data    struct {
				Activities []struct {
					ID        string `json:"id"`
					RequestID string `json:"request_id"`
					Type      string `json:"type"`
				} `json:"activities"`
				Total int `json:"total"`
			} `json:"data"`
		}
		err = json.NewDecoder(resp.Body).Decode(&activityResp)
		require.NoError(t, err)

		// Note: The activity might not exist yet if no tool was actually called
		// Just verify the API accepts the request_id parameter
		t.Logf("✅ API accepted request_id filter, returned %d activities", activityResp.Data.Total)

		// If activities exist, verify they all have the matching request_id
		for _, act := range activityResp.Data.Activities {
			assert.Equal(t, knownRequestID, act.RequestID, "All returned activities should have matching request_id")
		}
	})

	// T031: Test that non-matching request_id returns no results
	t.Run("API request_id filter returns empty for non-matching ID", func(t *testing.T) {
		nonExistentID := "non-existent-request-id-xyz789"

		req, err := http.NewRequest("GET", baseURL+"/api/v1/activity?request_id="+nonExistentID, nil)
		require.NoError(t, err)
		req.Header.Set("X-API-Key", apiKey)

		resp, err := http.DefaultClient.Do(req)
		require.NoError(t, err)
		defer resp.Body.Close()

		require.Equal(t, http.StatusOK, resp.StatusCode)

		var activityResp struct {
			Success bool `json:"success"`
			Data    struct {
				Activities []interface{} `json:"activities"`
				Total      int           `json:"total"`
			} `json:"data"`
		}
		err = json.NewDecoder(resp.Body).Decode(&activityResp)
		require.NoError(t, err)

		// Should return empty results for non-existent request_id
		assert.Equal(t, 0, activityResp.Data.Total, "Non-matching request_id should return no activities")
		t.Logf("✅ Non-matching request_id correctly returns 0 activities")
	})

	t.Log("✅ All Request ID Activity Filtering E2E tests passed")
}

// ============================================================================
// Activity Log Filtering E2E Tests (Spec 024)
// ============================================================================
// These tests verify that activity log filtering works correctly,
// including the exclusion of successful call_tool_* internal tool calls.

// TestE2E_Activity_ExcludeCallToolSuccess verifies that successful call_tool_*
// internal tool calls are excluded by default from activity listings.
// Spec: 024-expand-activity-log
func TestE2E_Activity_ExcludeCallToolSuccess(t *testing.T) {
	env := NewTestEnvironment(t)
	defer env.Cleanup()

	baseURL := strings.TrimSuffix(env.proxyAddr, "/mcp")
	apiKey := "test-api-key-e2e"

	mcpClient := env.CreateProxyClient()
	defer mcpClient.Close()
	env.ConnectClient(mcpClient)

	ctx := context.Background()

	// Step 1: Make a tool call to generate both tool_call and internal_tool_call records
	t.Run("Make tool call to generate activity", func(t *testing.T) {
		callRequest := mcp.CallToolRequest{}
		callRequest.Params.Name = "call_tool_read"
		callRequest.Params.Arguments = map[string]interface{}{
			"name":      "test-server-fixture:echo_tool",
			"args_json": `{"message": "test-activity-filtering"}`,
			"intent": map[string]interface{}{
				"operation_type": "read",
				"reason":         "E2E test for activity filtering",
			},
		}

		result, err := mcpClient.CallTool(ctx, callRequest)
		require.NoError(t, err)
		// Tool call may fail if test-server-fixture doesn't exist, but that's OK
		// The activity will still be logged
		t.Logf("Tool call result (may fail, activities still logged): isError=%v", result.IsError)
	})

	// Give some time for activities to be persisted
	time.Sleep(100 * time.Millisecond)

	// Step 2: Query activities without include_call_tool (default: exclude successful call_tool_*)
	t.Run("Default query excludes successful call_tool_*", func(t *testing.T) {
		req, err := http.NewRequest("GET", baseURL+"/api/v1/activity?limit=50", nil)
		require.NoError(t, err)
		req.Header.Set("X-API-Key", apiKey)

		resp, err := http.DefaultClient.Do(req)
		require.NoError(t, err)
		defer resp.Body.Close()

		require.Equal(t, http.StatusOK, resp.StatusCode)

		var activityResp struct {
			Success bool `json:"success"`
			Data    struct {
				Activities []struct {
					ID       string `json:"id"`
					Type     string `json:"type"`
					ToolName string `json:"tool_name"`
					Status   string `json:"status"`
				} `json:"activities"`
				Total int `json:"total"`
			} `json:"data"`
		}
		err = json.NewDecoder(resp.Body).Decode(&activityResp)
		require.NoError(t, err)

		// Check that no successful call_tool_* internal_tool_call records are in the response
		for _, act := range activityResp.Data.Activities {
			if act.Type == "internal_tool_call" && strings.HasPrefix(act.ToolName, "call_tool_") {
				// If we find a call_tool_* internal_tool_call, it must be a failure
				assert.NotEqual(t, "success", act.Status,
					"Successful call_tool_* internal_tool_call should be excluded by default")
			}
		}
		t.Logf("✅ Default query returned %d activities, no successful call_tool_* found", activityResp.Data.Total)
	})

	// Step 3: Query activities with include_call_tool=true (should include all)
	t.Run("include_call_tool=true shows all internal tool calls", func(t *testing.T) {
		req, err := http.NewRequest("GET", baseURL+"/api/v1/activity?limit=50&include_call_tool=true", nil)
		require.NoError(t, err)
		req.Header.Set("X-API-Key", apiKey)

		resp, err := http.DefaultClient.Do(req)
		require.NoError(t, err)
		defer resp.Body.Close()

		require.Equal(t, http.StatusOK, resp.StatusCode)

		var activityResp struct {
			Success bool `json:"success"`
			Data    struct {
				Activities []struct {
					ID       string `json:"id"`
					Type     string `json:"type"`
					ToolName string `json:"tool_name"`
					Status   string `json:"status"`
				} `json:"activities"`
				Total int `json:"total"`
			} `json:"data"`
		}
		err = json.NewDecoder(resp.Body).Decode(&activityResp)
		require.NoError(t, err)

		// Just verify the parameter is accepted - may or may not have call_tool_* entries
		t.Logf("✅ include_call_tool=true query returned %d activities", activityResp.Data.Total)
	})

	t.Log("✅ All Activity call_tool_* filtering E2E tests passed")
}

// TestE2E_Activity_MultiTypeFilter verifies that multi-type filtering works correctly.
// Spec: 024-expand-activity-log
func TestE2E_Activity_MultiTypeFilter(t *testing.T) {
	env := NewTestEnvironment(t)
	defer env.Cleanup()

	baseURL := strings.TrimSuffix(env.proxyAddr, "/mcp")
	apiKey := "test-api-key-e2e"

	// Step 1: Query with single type filter
	t.Run("Single type filter", func(t *testing.T) {
		req, err := http.NewRequest("GET", baseURL+"/api/v1/activity?type=system_start", nil)
		require.NoError(t, err)
		req.Header.Set("X-API-Key", apiKey)

		resp, err := http.DefaultClient.Do(req)
		require.NoError(t, err)
		defer resp.Body.Close()

		require.Equal(t, http.StatusOK, resp.StatusCode)

		var activityResp struct {
			Success bool `json:"success"`
			Data    struct {
				Activities []struct {
					ID   string `json:"id"`
					Type string `json:"type"`
				} `json:"activities"`
				Total int `json:"total"`
			} `json:"data"`
		}
		err = json.NewDecoder(resp.Body).Decode(&activityResp)
		require.NoError(t, err)

		// All returned activities should be system_start type
		for _, act := range activityResp.Data.Activities {
			assert.Equal(t, "system_start", act.Type, "Filtered activities should only be system_start type")
		}
		t.Logf("✅ Single type filter returned %d system_start activities", activityResp.Data.Total)
	})

	// Step 2: Query with multi-type filter (comma-separated)
	t.Run("Multi-type filter with comma separation", func(t *testing.T) {
		req, err := http.NewRequest("GET", baseURL+"/api/v1/activity?type=system_start,system_stop,config_change", nil)
		require.NoError(t, err)
		req.Header.Set("X-API-Key", apiKey)

		resp, err := http.DefaultClient.Do(req)
		require.NoError(t, err)
		defer resp.Body.Close()

		require.Equal(t, http.StatusOK, resp.StatusCode)

		var activityResp struct {
			Success bool `json:"success"`
			Data    struct {
				Activities []struct {
					ID   string `json:"id"`
					Type string `json:"type"`
				} `json:"activities"`
				Total int `json:"total"`
			} `json:"data"`
		}
		err = json.NewDecoder(resp.Body).Decode(&activityResp)
		require.NoError(t, err)

		// All returned activities should be one of the filtered types
		validTypes := map[string]bool{
			"system_start":  true,
			"system_stop":   true,
			"config_change": true,
		}
		for _, act := range activityResp.Data.Activities {
			assert.True(t, validTypes[act.Type], "Activity type %s should be in filter list", act.Type)
		}
		t.Logf("✅ Multi-type filter returned %d activities of types system_start/system_stop/config_change", activityResp.Data.Total)
	})

	t.Log("✅ All Activity multi-type filtering E2E tests passed")
}

// TestE2E_Activity_Summary verifies that activity summary endpoint works correctly.
// Spec: 024-expand-activity-log
func TestE2E_Activity_Summary(t *testing.T) {
	env := NewTestEnvironment(t)
	defer env.Cleanup()

	baseURL := strings.TrimSuffix(env.proxyAddr, "/mcp")
	apiKey := "test-api-key-e2e"

	t.Run("Get activity summary for 24h period", func(t *testing.T) {
		req, err := http.NewRequest("GET", baseURL+"/api/v1/activity/summary?period=24h", nil)
		require.NoError(t, err)
		req.Header.Set("X-API-Key", apiKey)

		resp, err := http.DefaultClient.Do(req)
		require.NoError(t, err)
		defer resp.Body.Close()

		require.Equal(t, http.StatusOK, resp.StatusCode)

		var summaryResp struct {
			Success bool `json:"success"`
			Data    struct {
				Period       string `json:"period"`
				TotalCount   int    `json:"total_count"`
				SuccessCount int    `json:"success_count"`
				ErrorCount   int    `json:"error_count"`
				BlockedCount int    `json:"blocked_count"`
			} `json:"data"`
		}
		err = json.NewDecoder(resp.Body).Decode(&summaryResp)
		require.NoError(t, err)

		assert.True(t, summaryResp.Success, "Summary request should succeed")
		assert.Equal(t, "24h", summaryResp.Data.Period, "Period should be 24h")
		// Total should be sum of success + error + blocked
		assert.GreaterOrEqual(t, summaryResp.Data.TotalCount,
			summaryResp.Data.SuccessCount+summaryResp.Data.ErrorCount+summaryResp.Data.BlockedCount,
			"Total should be >= sum of status counts")
		t.Logf("✅ Activity summary: total=%d, success=%d, error=%d, blocked=%d",
			summaryResp.Data.TotalCount, summaryResp.Data.SuccessCount,
			summaryResp.Data.ErrorCount, summaryResp.Data.BlockedCount)
	})

	t.Log("✅ All Activity summary E2E tests passed")
}

// ============================================================================
// Smart Config Patching E2E Tests (Spec 023, Issues #239, #240)
// ============================================================================
// These tests verify that config update operations preserve unrelated fields
// through the complete request flow.

// TestE2E_PatchPreservesIsolationConfig verifies that patching a server
// preserves the isolation configuration when only modifying other fields.
// This is the key E2E test for Issue #239 and #240.
func TestE2E_PatchPreservesIsolationConfig(t *testing.T) {
	env := NewTestEnvironment(t)
	defer env.Cleanup()

	mcpClient := env.CreateProxyClient()
	defer mcpClient.Close()
	env.ConnectClient(mcpClient)

	ctx := context.Background()

	// Step 1: Add a server with isolation config
	addRequest := mcp.CallToolRequest{}
	addRequest.Params.Name = "upstream_servers"
	addRequest.Params.Arguments = map[string]interface{}{
		"operation":      "add",
		"name":           "isolation-preserve-test",
		"command":        "echo",
		"args_json":      `["test"]`,
		"enabled":        false,
		"isolation_json": `{"enabled": true, "image": "python:3.11", "network_mode": "bridge"}`,
	}

	addResult, err := mcpClient.CallTool(ctx, addRequest)
	require.NoError(t, err)
	require.False(t, addResult.IsError, "Add operation should succeed: %v", getToolResultText(addResult))

	// Step 2: Patch server - only change enabled state (isolation should be preserved)
	patchRequest := mcp.CallToolRequest{}
	patchRequest.Params.Name = "upstream_servers"
	patchRequest.Params.Arguments = map[string]interface{}{
		"operation": "patch",
		"name":      "isolation-preserve-test",
		"enabled":   true, // Toggle enabled state
	}

	patchResult, err := mcpClient.CallTool(ctx, patchRequest)
	require.NoError(t, err)
	require.False(t, patchResult.IsError, "Patch operation should succeed: %v", getToolResultText(patchResult))

	// Step 3: List servers and verify isolation is preserved
	listRequest := mcp.CallToolRequest{}
	listRequest.Params.Name = "upstream_servers"
	listRequest.Params.Arguments = map[string]interface{}{
		"operation": "list",
	}

	listResult, err := mcpClient.CallTool(ctx, listRequest)
	require.NoError(t, err)
	require.False(t, listResult.IsError, "List operation should succeed")

	// Parse the response to verify isolation is preserved
	listText := getToolResultText(listResult)
	var listResponse map[string]interface{}
	err = json.Unmarshal([]byte(listText), &listResponse)
	require.NoError(t, err)

	servers, ok := listResponse["servers"].([]interface{})
	require.True(t, ok, "Response should contain servers array")

	var foundServer map[string]interface{}
	for _, s := range servers {
		server := s.(map[string]interface{})
		if server["name"] == "isolation-preserve-test" {
			foundServer = server
			break
		}
	}
	require.NotNil(t, foundServer, "Should find the test server")

	// CRITICAL: Verify isolation config is preserved
	// In the list response, isolation is under docker_isolation.server_isolation
	dockerIsolation, ok := foundServer["docker_isolation"].(map[string]interface{})
	require.True(t, ok, "docker_isolation should be present")
	isolation, ok := dockerIsolation["server_isolation"].(map[string]interface{})
	require.True(t, ok, "Isolation config must be preserved after patch (under docker_isolation.server_isolation)")
	assert.Equal(t, true, isolation["enabled"], "isolation.enabled must be preserved")
	assert.Equal(t, "python:3.11", isolation["image"], "isolation.image must be preserved")
	assert.Equal(t, "bridge", isolation["network_mode"], "isolation.network_mode must be preserved")

	// Verify enabled state was changed
	assert.Equal(t, true, foundServer["enabled"], "enabled state should be updated")

	// Step 4: Clean up
	deleteRequest := mcp.CallToolRequest{}
	deleteRequest.Params.Name = "upstream_servers"
	deleteRequest.Params.Arguments = map[string]interface{}{
		"operation": "remove",
		"name":      "isolation-preserve-test",
	}
	_, _ = mcpClient.CallTool(ctx, deleteRequest)

	t.Log("✅ Test passed: Patch operation preserves isolation config")
}

// TestE2E_PatchPreservesOAuthConfig verifies that patching a server
// preserves the OAuth configuration when only modifying other fields.
// Note: OAuth config is intentionally NOT exposed in list responses for security.
// We verify preservation by checking the patch response diff, which should NOT
// include oauth in the changes when only modifying other fields.
func TestE2E_PatchPreservesOAuthConfig(t *testing.T) {
	env := NewTestEnvironment(t)
	defer env.Cleanup()

	mcpClient := env.CreateProxyClient()
	defer mcpClient.Close()
	env.ConnectClient(mcpClient)

	ctx := context.Background()

	// Step 1: Add a server with OAuth config
	addRequest := mcp.CallToolRequest{}
	addRequest.Params.Name = "upstream_servers"
	addRequest.Params.Arguments = map[string]interface{}{
		"operation":  "add",
		"name":       "oauth-preserve-test",
		"url":        "https://example.com/mcp",
		"protocol":   "http",
		"enabled":    false,
		"oauth_json": `{"client_id": "my-client-id", "scopes": ["read", "write"], "pkce_enabled": true}`,
	}

	addResult, err := mcpClient.CallTool(ctx, addRequest)
	require.NoError(t, err)
	require.False(t, addResult.IsError, "Add operation should succeed: %v", getToolResultText(addResult))

	// Step 2: Patch server - only change URL (OAuth should be preserved)
	patchRequest := mcp.CallToolRequest{}
	patchRequest.Params.Name = "upstream_servers"
	patchRequest.Params.Arguments = map[string]interface{}{
		"operation": "patch",
		"name":      "oauth-preserve-test",
		"url":       "https://new-url.com/mcp",
	}

	patchResult, err := mcpClient.CallTool(ctx, patchRequest)
	require.NoError(t, err)
	require.False(t, patchResult.IsError, "Patch operation should succeed: %v", getToolResultText(patchResult))

	// Parse patch response to verify only URL was changed (OAuth not modified)
	patchText := getToolResultText(patchResult)
	var patchResponse map[string]interface{}
	err = json.Unmarshal([]byte(patchText), &patchResponse)
	require.NoError(t, err)

	// If there are changes, verify OAuth is NOT in the changes
	if changes, ok := patchResponse["changes"].(map[string]interface{}); ok {
		if modified, ok := changes["modified"].(map[string]interface{}); ok {
			// OAuth should not be in the modified list - only URL should be modified
			_, oauthModified := modified["oauth"]
			assert.False(t, oauthModified, "oauth should NOT be in modified list when only changing URL")
			// URL should be the only field modified
			_, urlModified := modified["url"]
			assert.True(t, urlModified, "url should be in modified list")
		}
	}

	// Step 3: List servers and verify URL was updated
	listRequest := mcp.CallToolRequest{}
	listRequest.Params.Name = "upstream_servers"
	listRequest.Params.Arguments = map[string]interface{}{
		"operation": "list",
	}

	listResult, err := mcpClient.CallTool(ctx, listRequest)
	require.NoError(t, err)
	require.False(t, listResult.IsError, "List operation should succeed")

	// Parse the response
	listText := getToolResultText(listResult)
	var listResponse map[string]interface{}
	err = json.Unmarshal([]byte(listText), &listResponse)
	require.NoError(t, err)

	servers, ok := listResponse["servers"].([]interface{})
	require.True(t, ok, "Response should contain servers array")

	var foundServer map[string]interface{}
	for _, s := range servers {
		server := s.(map[string]interface{})
		if server["name"] == "oauth-preserve-test" {
			foundServer = server
			break
		}
	}
	require.NotNil(t, foundServer, "Should find the test server")

	// Verify URL was updated
	assert.Equal(t, "https://new-url.com/mcp", foundServer["url"], "URL should be updated")

	// Note: OAuth config is intentionally NOT exposed in list responses for security
	// The fact that the patch succeeded and didn't error is indirect evidence OAuth is preserved
	// For full verification, use the /api/v1/servers/{name} endpoint which returns full config

	// Step 4: Clean up
	deleteRequest := mcp.CallToolRequest{}
	deleteRequest.Params.Name = "upstream_servers"
	deleteRequest.Params.Arguments = map[string]interface{}{
		"operation": "remove",
		"name":      "oauth-preserve-test",
	}
	_, _ = mcpClient.CallTool(ctx, deleteRequest)

	t.Log("✅ Test passed: Patch operation preserves OAuth config")
}

// TestE2E_PatchDeepMergesEnvAndHeaders verifies that patching env and headers
// does deep merge (adds to existing) rather than full replacement.
func TestE2E_PatchDeepMergesEnvAndHeaders(t *testing.T) {
	env := NewTestEnvironment(t)
	defer env.Cleanup()

	mcpClient := env.CreateProxyClient()
	defer mcpClient.Close()
	env.ConnectClient(mcpClient)

	ctx := context.Background()

	// Step 1: Add a server with env and headers
	addRequest := mcp.CallToolRequest{}
	addRequest.Params.Name = "upstream_servers"
	addRequest.Params.Arguments = map[string]interface{}{
		"operation":    "add",
		"name":         "deep-merge-test",
		"command":      "echo",
		"args_json":    `["test"]`,
		"enabled":      false,
		"env_json":     `{"EXISTING_VAR": "existing_value", "ANOTHER_VAR": "another_value"}`,
		"headers_json": `{"Authorization": "Bearer token", "X-Custom": "custom-value"}`,
	}

	addResult, err := mcpClient.CallTool(ctx, addRequest)
	require.NoError(t, err)
	require.False(t, addResult.IsError, "Add operation should succeed: %v", getToolResultText(addResult))

	// Step 2: Patch with additional env var and header (deep merge)
	patchRequest := mcp.CallToolRequest{}
	patchRequest.Params.Name = "upstream_servers"
	patchRequest.Params.Arguments = map[string]interface{}{
		"operation":    "patch",
		"name":         "deep-merge-test",
		"env_json":     `{"NEW_VAR": "new_value"}`,
		"headers_json": `{"X-New-Header": "new-header-value"}`,
	}

	patchResult, err := mcpClient.CallTool(ctx, patchRequest)
	require.NoError(t, err)
	require.False(t, patchResult.IsError, "Patch operation should succeed: %v", getToolResultText(patchResult))

	// Step 3: List servers and verify deep merge
	listRequest := mcp.CallToolRequest{}
	listRequest.Params.Name = "upstream_servers"
	listRequest.Params.Arguments = map[string]interface{}{
		"operation": "list",
	}

	listResult, err := mcpClient.CallTool(ctx, listRequest)
	require.NoError(t, err)
	require.False(t, listResult.IsError, "List operation should succeed")

	// Parse the response
	listText := getToolResultText(listResult)
	var listResponse map[string]interface{}
	err = json.Unmarshal([]byte(listText), &listResponse)
	require.NoError(t, err)

	servers, ok := listResponse["servers"].([]interface{})
	require.True(t, ok, "Response should contain servers array")

	var foundServer map[string]interface{}
	for _, s := range servers {
		server := s.(map[string]interface{})
		if server["name"] == "deep-merge-test" {
			foundServer = server
			break
		}
	}
	require.NotNil(t, foundServer, "Should find the test server")

	// CRITICAL: Verify existing env vars are preserved (deep merge)
	envMap, ok := foundServer["env"].(map[string]interface{})
	require.True(t, ok, "Env should be a map")
	assert.Equal(t, "existing_value", envMap["EXISTING_VAR"], "EXISTING_VAR must be preserved")
	assert.Equal(t, "another_value", envMap["ANOTHER_VAR"], "ANOTHER_VAR must be preserved")
	assert.Equal(t, "new_value", envMap["NEW_VAR"], "NEW_VAR must be added")

	// CRITICAL: Verify existing headers are preserved (deep merge).
	// The Authorization value is redacted in the API response by default
	// (see config.RevealSecretHeaders) — we assert the key is still
	// present (confirming the merge kept it) and that the value is the
	// masked-display format `••••<last2> (<N> chars)` rather than the
	// plaintext. Non-sensitive headers retain their actual values,
	// confirming both that merge worked AND that redaction is scoped to
	// sensitive header names only.
	headersMap, ok := foundServer["headers"].(map[string]interface{})
	require.True(t, ok, "Headers should be a map")
	authVal, _ := headersMap["Authorization"].(string)
	assert.Contains(t, authVal, "••••", "Authorization should be present and masked")
	assert.Contains(t, authVal, "chars)", "Authorization mask should include the length suffix")
	assert.NotContains(t, authVal, "Bearer", "Bearer plaintext must not survive the mask")
	assert.Equal(t, "custom-value", headersMap["X-Custom"], "X-Custom must be preserved")
	assert.Equal(t, "new-header-value", headersMap["X-New-Header"], "X-New-Header must be added")

	// Step 4: Clean up
	deleteRequest := mcp.CallToolRequest{}
	deleteRequest.Params.Name = "upstream_servers"
	deleteRequest.Params.Arguments = map[string]interface{}{
		"operation": "remove",
		"name":      "deep-merge-test",
	}
	_, _ = mcpClient.CallTool(ctx, deleteRequest)

	t.Log("✅ Test passed: Patch deep merges env and headers")
}

// TestE2E_MultipleEnableDisablePreservesConfig verifies that toggling a server's
// enabled state multiple times doesn't lose any configuration.
func TestE2E_MultipleEnableDisablePreservesConfig(t *testing.T) {
	env := NewTestEnvironment(t)
	defer env.Cleanup()

	mcpClient := env.CreateProxyClient()
	defer mcpClient.Close()
	env.ConnectClient(mcpClient)

	ctx := context.Background()

	// Step 1: Add a fully-configured server
	addRequest := mcp.CallToolRequest{}
	addRequest.Params.Name = "upstream_servers"
	addRequest.Params.Arguments = map[string]interface{}{
		"operation":      "add",
		"name":           "toggle-test-server",
		"command":        "npx",
		"args_json":      `["-y", "test-package"]`,
		"working_dir":    "/opt/test",
		"enabled":        false,
		"env_json":       `{"API_KEY": "secret", "DEBUG": "true"}`,
		"headers_json":   `{"Authorization": "Bearer token"}`,
		"isolation_json": `{"enabled": true, "image": "node:18"}`,
	}

	addResult, err := mcpClient.CallTool(ctx, addRequest)
	require.NoError(t, err)
	require.False(t, addResult.IsError, "Add operation should succeed: %v", getToolResultText(addResult))

	// Step 2: Toggle enabled state 5 times
	for i := 0; i < 5; i++ {
		enabled := i%2 == 0 // Alternates: true, false, true, false, true
		patchRequest := mcp.CallToolRequest{}
		patchRequest.Params.Name = "upstream_servers"
		patchRequest.Params.Arguments = map[string]interface{}{
			"operation": "patch",
			"name":      "toggle-test-server",
			"enabled":   enabled,
		}

		patchResult, err := mcpClient.CallTool(ctx, patchRequest)
		require.NoError(t, err)
		require.False(t, patchResult.IsError, "Patch #%d should succeed: %v", i+1, getToolResultText(patchResult))
	}

	// Step 3: Verify all config is still intact
	listRequest := mcp.CallToolRequest{}
	listRequest.Params.Name = "upstream_servers"
	listRequest.Params.Arguments = map[string]interface{}{
		"operation": "list",
	}

	listResult, err := mcpClient.CallTool(ctx, listRequest)
	require.NoError(t, err)
	require.False(t, listResult.IsError, "List operation should succeed")

	// Parse the response
	listText := getToolResultText(listResult)
	var listResponse map[string]interface{}
	err = json.Unmarshal([]byte(listText), &listResponse)
	require.NoError(t, err)

	servers, ok := listResponse["servers"].([]interface{})
	require.True(t, ok, "Response should contain servers array")

	var foundServer map[string]interface{}
	for _, s := range servers {
		server := s.(map[string]interface{})
		if server["name"] == "toggle-test-server" {
			foundServer = server
			break
		}
	}
	require.NotNil(t, foundServer, "Should find the test server")

	// Verify ALL fields are still intact after 5 toggles
	assert.Equal(t, "npx", foundServer["command"], "command must be preserved")
	// Note: working_dir is not exposed in top-level list response

	// Verify args
	args, ok := foundServer["args"].([]interface{})
	require.True(t, ok, "args should be an array")
	assert.Len(t, args, 2, "args should have 2 elements")

	// Verify env
	envMap, ok := foundServer["env"].(map[string]interface{})
	require.True(t, ok, "env should be a map")
	assert.Equal(t, "secret", envMap["API_KEY"], "API_KEY must be preserved")
	assert.Equal(t, "true", envMap["DEBUG"], "DEBUG must be preserved")

	// Verify headers — Authorization is masked in API output by default
	// (config.RevealSecretHeaders defaults to false). The key is still
	// present and the value is the masked-display format
	// `••••<last2> (<N> chars)`, which confirms the underlying merge
	// preserved the real secret across the 5 toggles.
	headersMap, ok := foundServer["headers"].(map[string]interface{})
	require.True(t, ok, "headers should be a map")
	authVal, _ := headersMap["Authorization"].(string)
	assert.Contains(t, authVal, "••••", "Authorization key must be preserved across toggles (value masked in API output)")
	assert.Contains(t, authVal, "chars)", "mask must include length suffix")

	// Verify isolation - in list response, isolation is under docker_isolation.server_isolation
	dockerIsolation, ok := foundServer["docker_isolation"].(map[string]interface{})
	require.True(t, ok, "docker_isolation should be present")
	serverIsolation, ok := dockerIsolation["server_isolation"].(map[string]interface{})
	require.True(t, ok, "server_isolation should be present after 5 toggles")
	assert.Equal(t, true, serverIsolation["enabled"], "isolation.enabled must be preserved")
	assert.Equal(t, "node:18", serverIsolation["image"], "isolation.image must be preserved")

	// Verify enabled state (should be true after 5 toggles: 0=true, 1=false, 2=true, 3=false, 4=true)
	assert.Equal(t, true, foundServer["enabled"], "enabled should be true after 5 toggles")

	// Step 4: Clean up
	deleteRequest := mcp.CallToolRequest{}
	deleteRequest.Params.Name = "upstream_servers"
	deleteRequest.Params.Arguments = map[string]interface{}{
		"operation": "remove",
		"name":      "toggle-test-server",
	}
	_, _ = mcpClient.CallTool(ctx, deleteRequest)

	t.Log("✅ Test passed: Multiple enable/disable cycles preserve config")
}

// Helper function to extract text from tool result
func getToolResultText(result *mcp.CallToolResult) string {
	if result == nil || len(result.Content) == 0 {
		return ""
	}
	contentBytes, err := json.Marshal(result.Content[0])
	if err != nil {
		return ""
	}
	var contentMap map[string]interface{}
	if err := json.Unmarshal(contentBytes, &contentMap); err != nil {
		return ""
	}
	if text, ok := contentMap["text"].(string); ok {
		return text
	}
	return ""
}

// TestE2E_DisableServerRemovesToolsFromSearch validates Issue #285 fix:
// When a server is disabled:
// 1. Tools from that server should be removed from search results
// 2. TotalTools stat should only count enabled servers' tools
// When re-enabled:
// 3. Tools should be discoverable again after re-enable
func TestE2E_DisableServerRemovesToolsFromSearch(t *testing.T) {
	env := NewTestEnvironment(t)
	defer env.Cleanup()

	ctx := context.Background()

	// Create mock server with distinctive tools
	uniqueTools := []mcp.Tool{
		{
			Name:        "issue285_alpha_tool",
			Description: "A unique tool for testing issue 285 disable scenario",
			InputSchema: mcp.ToolInputSchema{Type: "object"},
		},
		{
			Name:        "issue285_beta_tool",
			Description: "Another unique tool for testing issue 285 disable",
			InputSchema: mcp.ToolInputSchema{Type: "object"},
		},
	}

	const serverName = "issue285-test-server"
	mockServer := env.CreateMockUpstreamServer(serverName, uniqueTools)

	mcpClient := env.CreateProxyClient()
	defer mcpClient.Close()
	env.ConnectClient(mcpClient)

	// Add server via upstream_servers tool
	addRequest := mcp.CallToolRequest{}
	addRequest.Params.Name = "upstream_servers"
	addRequest.Params.Arguments = map[string]interface{}{
		"operation": "add",
		"name":      serverName,
		"url":       mockServer.addr,
		"protocol":  "streamable-http",
		"enabled":   true,
	}

	addResult, err := mcpClient.CallTool(ctx, addRequest)
	require.NoError(t, err)
	require.False(t, addResult.IsError, "Add server should succeed: %v", getToolResultText(addResult))

	// Unquarantine server
	serverConfig, err := env.proxyServer.runtime.StorageManager().GetUpstreamServer(serverName)
	require.NoError(t, err)
	serverConfig.Quarantined = false
	err = env.proxyServer.runtime.StorageManager().SaveUpstreamServer(serverConfig)
	require.NoError(t, err)

	// Reload configuration
	servers, err := env.proxyServer.runtime.StorageManager().ListUpstreamServers()
	require.NoError(t, err)
	cfg := env.proxyServer.runtime.Config()
	cfg.Servers = servers
	err = env.proxyServer.runtime.LoadConfiguredServers(cfg)
	require.NoError(t, err)

	// Wait for connection and discovery
	time.Sleep(4 * time.Second)
	_ = env.proxyServer.runtime.DiscoverAndIndexTools(ctx)
	time.Sleep(2 * time.Second)

	// Step 1: Verify tools are searchable when server is enabled
	searchRequest := mcp.CallToolRequest{}
	searchRequest.Params.Name = "retrieve_tools"
	searchRequest.Params.Arguments = map[string]interface{}{
		"query": "issue285_alpha",
	}

	searchResult, err := mcpClient.CallTool(ctx, searchRequest)
	require.NoError(t, err)
	require.False(t, searchResult.IsError, "Search should succeed")

	searchText := getToolResultText(searchResult)
	assert.Contains(t, searchText, "issue285_alpha_tool", "Tool should be searchable when server is enabled")
	t.Log("Step 1 PASSED: Tool is searchable when server is enabled")

	// Step 2: Disable the server
	disableRequest := mcp.CallToolRequest{}
	disableRequest.Params.Name = "upstream_servers"
	disableRequest.Params.Arguments = map[string]interface{}{
		"operation": "patch",
		"name":      "issue285-test-server",
		"enabled":   false,
	}

	disableResult, err := mcpClient.CallTool(ctx, disableRequest)
	require.NoError(t, err)
	require.False(t, disableResult.IsError, "Disable operation should succeed: %v", getToolResultText(disableResult))

	// Wait for async tool removal from index to complete
	time.Sleep(2 * time.Second)

	// Step 3: Verify tools are NOT searchable after disable
	searchAfterDisable, err := mcpClient.CallTool(ctx, searchRequest)
	require.NoError(t, err)
	require.False(t, searchAfterDisable.IsError, "Search should succeed (returning empty results)")

	searchAfterText := getToolResultText(searchAfterDisable)
	// The search should NOT contain the disabled server's tools
	assert.NotContains(t, searchAfterText, "issue285-test-server:issue285_alpha_tool",
		"Disabled server's tools should NOT appear in search results")
	t.Log("Step 3 PASSED: Tools are not searchable after server is disabled")

	// Step 4: Re-enable the server
	enableRequest := mcp.CallToolRequest{}
	enableRequest.Params.Name = "upstream_servers"
	enableRequest.Params.Arguments = map[string]interface{}{
		"operation": "patch",
		"name":      "issue285-test-server",
		"enabled":   true,
	}

	enableResult, err := mcpClient.CallTool(ctx, enableRequest)
	require.NoError(t, err)
	require.False(t, enableResult.IsError, "Enable operation should succeed: %v", getToolResultText(enableResult))

	// Wait for async tool discovery and indexing to complete
	time.Sleep(3 * time.Second)

	// Step 5: Verify tools are searchable again after re-enable
	searchAfterEnable, err := mcpClient.CallTool(ctx, searchRequest)
	require.NoError(t, err)
	require.False(t, searchAfterEnable.IsError, "Search should succeed")

	searchAfterEnableText := getToolResultText(searchAfterEnable)
	assert.Contains(t, searchAfterEnableText, "issue285_alpha_tool",
		"Tools should be searchable again after server is re-enabled")
	t.Log("Step 5 PASSED: Tools are searchable again after re-enable")

	// Cleanup
	deleteRequest := mcp.CallToolRequest{}
	deleteRequest.Params.Name = "upstream_servers"
	deleteRequest.Params.Arguments = map[string]interface{}{
		"operation": "remove",
		"name":      "issue285-test-server",
	}
	_, _ = mcpClient.CallTool(ctx, deleteRequest)

	t.Log("✅ Issue #285 fix verified: disable removes tools from search, enable restores them")
}

// TestE2E_ServerDeleteReaddDifferentTools validates the full lifecycle of stale index cleanup:
// 1. Add server with Tool Set A, verify tools are searchable
// 2. Remove server, verify Tool Set A is no longer searchable
// 3. Re-add server (same name) with Tool Set B, verify ONLY Tool Set B is searchable
// This tests that orphaned index entries are cleaned up when servers are removed.
func TestE2E_ServerDeleteReaddDifferentTools(t *testing.T) {
	env := NewTestEnvironment(t)
	defer env.Cleanup()

	ctx := context.Background()

	// Tool Set A: Initial tools
	toolSetA := []mcp.Tool{
		{
			Name:        "old_tool_alpha",
			Description: "Old tool alpha for initial setup",
			InputSchema: mcp.ToolInputSchema{
				Type: "object",
				Properties: map[string]interface{}{
					"param1": map[string]interface{}{
						"type":        "string",
						"description": "Alpha parameter",
					},
				},
			},
		},
		{
			Name:        "old_tool_beta",
			Description: "Old tool beta for initial setup",
			InputSchema: mcp.ToolInputSchema{
				Type: "object",
				Properties: map[string]interface{}{
					"param1": map[string]interface{}{
						"type":        "string",
						"description": "Beta parameter",
					},
				},
			},
		},
	}

	// Tool Set B: Replacement tools (different names)
	toolSetB := []mcp.Tool{
		{
			Name:        "new_tool_gamma",
			Description: "New tool gamma after server re-add",
			InputSchema: mcp.ToolInputSchema{
				Type: "object",
				Properties: map[string]interface{}{
					"value": map[string]interface{}{
						"type":        "string",
						"description": "Gamma value",
					},
				},
			},
		},
	}

	const serverName = "stale-test-server"

	// === Phase 1: Create mock server with Tool Set A ===
	t.Log("Phase 1: Creating mock server with Tool Set A (old_tool_alpha, old_tool_beta)")
	mockServerA := env.CreateMockUpstreamServer(serverName, toolSetA)

	// Create MCP client
	mcpClient := env.CreateProxyClient()
	defer mcpClient.Close()
	env.ConnectClient(mcpClient)

	// Add server via upstream_servers
	addRequestA := mcp.CallToolRequest{}
	addRequestA.Params.Name = "upstream_servers"
	addRequestA.Params.Arguments = map[string]interface{}{
		"operation": "add",
		"name":      serverName,
		"url":       mockServerA.addr,
		"protocol":  "streamable-http",
		"enabled":   true,
	}

	addResultA, err := mcpClient.CallTool(ctx, addRequestA)
	require.NoError(t, err)
	require.False(t, addResultA.IsError, "Add server should succeed")

	// Unquarantine server
	serverConfig, err := env.proxyServer.runtime.StorageManager().GetUpstreamServer(serverName)
	require.NoError(t, err)
	serverConfig.Quarantined = false
	err = env.proxyServer.runtime.StorageManager().SaveUpstreamServer(serverConfig)
	require.NoError(t, err)

	// Reload configuration
	servers, err := env.proxyServer.runtime.StorageManager().ListUpstreamServers()
	require.NoError(t, err)
	cfg := env.proxyServer.runtime.Config()
	cfg.Servers = servers
	err = env.proxyServer.runtime.LoadConfiguredServers(cfg)
	require.NoError(t, err)

	// Wait for connection and discovery
	time.Sleep(4 * time.Second)
	_ = env.proxyServer.runtime.DiscoverAndIndexTools(ctx)
	time.Sleep(2 * time.Second)

	// === Verify Tool Set A is searchable ===
	t.Log("Phase 1 Verify: Checking Tool Set A is searchable")
	searchAlpha := mcp.CallToolRequest{}
	searchAlpha.Params.Name = "retrieve_tools"
	searchAlpha.Params.Arguments = map[string]interface{}{
		"query": "old_tool_alpha",
		"limit": 10,
	}

	searchAlphaResult, err := mcpClient.CallTool(ctx, searchAlpha)
	require.NoError(t, err)
	require.False(t, searchAlphaResult.IsError)
	alphaText := getToolResultText(searchAlphaResult)
	assert.Contains(t, alphaText, "old_tool_alpha", "Tool Set A - old_tool_alpha should be searchable")

	searchBeta := mcp.CallToolRequest{}
	searchBeta.Params.Name = "retrieve_tools"
	searchBeta.Params.Arguments = map[string]interface{}{
		"query": "old_tool_beta",
		"limit": 10,
	}

	searchBetaResult, err := mcpClient.CallTool(ctx, searchBeta)
	require.NoError(t, err)
	require.False(t, searchBetaResult.IsError)
	betaText := getToolResultText(searchBetaResult)
	assert.Contains(t, betaText, "old_tool_beta", "Tool Set A - old_tool_beta should be searchable")

	t.Log("Phase 1 Complete: Tool Set A (old_tool_alpha, old_tool_beta) verified searchable")

	// === Phase 2: Stop and remove server ===
	t.Log("Phase 2: Stopping mock server and removing from config")

	// Stop mock server
	_ = mockServerA.stopFunc()
	delete(env.mockServers, serverName)

	// Remove server via upstream_servers
	removeRequest := mcp.CallToolRequest{}
	removeRequest.Params.Name = "upstream_servers"
	removeRequest.Params.Arguments = map[string]interface{}{
		"operation": "remove",
		"name":      serverName,
	}

	removeResult, err := mcpClient.CallTool(ctx, removeRequest)
	require.NoError(t, err)
	require.False(t, removeResult.IsError, "Remove server should succeed")

	// Wait for cleanup
	time.Sleep(3 * time.Second)

	// === Verify Tool Set A is no longer searchable ===
	t.Log("Phase 2 Verify: Checking Tool Set A is NOT searchable after removal")
	searchAlphaAfterRemove := mcp.CallToolRequest{}
	searchAlphaAfterRemove.Params.Name = "retrieve_tools"
	searchAlphaAfterRemove.Params.Arguments = map[string]interface{}{
		"query": "old_tool_alpha",
		"limit": 10,
	}

	searchAlphaAfterResult, err := mcpClient.CallTool(ctx, searchAlphaAfterRemove)
	require.NoError(t, err)
	alphaAfterText := getToolResultText(searchAlphaAfterResult)

	// Parse the response to check tools array
	var alphaAfterResponse map[string]interface{}
	if alphaAfterText != "" {
		_ = json.Unmarshal([]byte(alphaAfterText), &alphaAfterResponse)
	}

	// Check if tools array is empty or doesn't contain old_tool_alpha
	if tools, ok := alphaAfterResponse["tools"].([]interface{}); ok {
		for _, tool := range tools {
			if toolMap, ok := tool.(map[string]interface{}); ok {
				if name, ok := toolMap["name"].(string); ok {
					assert.NotContains(t, name, "old_tool_alpha",
						"old_tool_alpha should NOT be searchable after server removal")
				}
			}
		}
	}

	t.Log("Phase 2 Complete: Tool Set A verified NOT searchable after removal")

	// === Phase 3: Create NEW mock server with Tool Set B ===
	t.Log("Phase 3: Creating NEW mock server with Tool Set B (new_tool_gamma)")
	mockServerB := env.CreateMockUpstreamServer(serverName, toolSetB)

	// Re-add server via upstream_servers
	addRequestB := mcp.CallToolRequest{}
	addRequestB.Params.Name = "upstream_servers"
	addRequestB.Params.Arguments = map[string]interface{}{
		"operation": "add",
		"name":      serverName,
		"url":       mockServerB.addr,
		"protocol":  "streamable-http",
		"enabled":   true,
	}

	addResultB, err := mcpClient.CallTool(ctx, addRequestB)
	require.NoError(t, err)
	require.False(t, addResultB.IsError, "Re-add server should succeed")

	// Unquarantine server again
	serverConfigB, err := env.proxyServer.runtime.StorageManager().GetUpstreamServer(serverName)
	require.NoError(t, err)
	serverConfigB.Quarantined = false
	err = env.proxyServer.runtime.StorageManager().SaveUpstreamServer(serverConfigB)
	require.NoError(t, err)

	// Reload configuration
	serversB, err := env.proxyServer.runtime.StorageManager().ListUpstreamServers()
	require.NoError(t, err)
	cfgB := env.proxyServer.runtime.Config()
	cfgB.Servers = serversB
	err = env.proxyServer.runtime.LoadConfiguredServers(cfgB)
	require.NoError(t, err)

	// Wait for connection and discovery
	time.Sleep(4 * time.Second)
	_ = env.proxyServer.runtime.DiscoverAndIndexTools(ctx)
	time.Sleep(2 * time.Second)

	// === Verify ONLY Tool Set B is searchable ===
	t.Log("Phase 3 Verify: Checking ONLY Tool Set B is searchable")

	// Check new_tool_gamma IS found
	searchGamma := mcp.CallToolRequest{}
	searchGamma.Params.Name = "retrieve_tools"
	searchGamma.Params.Arguments = map[string]interface{}{
		"query": "new_tool_gamma",
		"limit": 10,
	}

	searchGammaResult, err := mcpClient.CallTool(ctx, searchGamma)
	require.NoError(t, err)
	require.False(t, searchGammaResult.IsError)
	gammaText := getToolResultText(searchGammaResult)
	assert.Contains(t, gammaText, "new_tool_gamma", "Tool Set B - new_tool_gamma SHOULD be searchable")

	// Check old_tool_alpha is NOT found
	searchOldAlpha := mcp.CallToolRequest{}
	searchOldAlpha.Params.Name = "retrieve_tools"
	searchOldAlpha.Params.Arguments = map[string]interface{}{
		"query": "old_tool_alpha",
		"limit": 10,
	}

	searchOldAlphaResult, err := mcpClient.CallTool(ctx, searchOldAlpha)
	require.NoError(t, err)
	oldAlphaText := getToolResultText(searchOldAlphaResult)

	// Parse and verify old_tool_alpha is not in results
	var oldAlphaResponse map[string]interface{}
	if oldAlphaText != "" {
		_ = json.Unmarshal([]byte(oldAlphaText), &oldAlphaResponse)
	}
	if tools, ok := oldAlphaResponse["tools"].([]interface{}); ok {
		for _, tool := range tools {
			if toolMap, ok := tool.(map[string]interface{}); ok {
				if name, ok := toolMap["name"].(string); ok {
					assert.NotContains(t, name, "old_tool_alpha",
						"FAIL: old_tool_alpha should NOT appear after re-add with different tools")
				}
			}
		}
	}

	// Check old_tool_beta is NOT found
	searchOldBeta := mcp.CallToolRequest{}
	searchOldBeta.Params.Name = "retrieve_tools"
	searchOldBeta.Params.Arguments = map[string]interface{}{
		"query": "old_tool_beta",
		"limit": 10,
	}

	searchOldBetaResult, err := mcpClient.CallTool(ctx, searchOldBeta)
	require.NoError(t, err)
	oldBetaText := getToolResultText(searchOldBetaResult)

	// Parse and verify old_tool_beta is not in results
	var oldBetaResponse map[string]interface{}
	if oldBetaText != "" {
		_ = json.Unmarshal([]byte(oldBetaText), &oldBetaResponse)
	}
	if tools, ok := oldBetaResponse["tools"].([]interface{}); ok {
		for _, tool := range tools {
			if toolMap, ok := tool.(map[string]interface{}); ok {
				if name, ok := toolMap["name"].(string); ok {
					assert.NotContains(t, name, "old_tool_beta",
						"FAIL: old_tool_beta should NOT appear after re-add with different tools")
				}
			}
		}
	}

	// === Phase 4: Verify new_tool_gamma is callable ===
	t.Log("Phase 4: Verifying new_tool_gamma is callable")
	callGamma := mcp.CallToolRequest{}
	callGamma.Params.Name = "call_tool_read"
	callGamma.Params.Arguments = map[string]interface{}{
		"name": serverName + ":new_tool_gamma",
		"args": map[string]interface{}{
			"value": "test-value",
		},
	}

	callGammaResult, err := mcpClient.CallTool(ctx, callGamma)
	require.NoError(t, err)
	assert.False(t, callGammaResult.IsError, "new_tool_gamma should be callable")

	t.Log("Phase 3 & 4 Complete: ONLY Tool Set B (new_tool_gamma) searchable and callable")
	t.Log("SUCCESS: Stale index entries cleaned up correctly on server re-add")
}

// Test: retrieve_tools returns correct annotations and call_with based on tool hints (Issue #306)
func TestE2E_RetrieveToolsAnnotationsAndCallWith(t *testing.T) {
	env := NewTestEnvironment(t)
	defer env.Cleanup()

	trueVal := true
	falseVal := false

	// Create mock upstream server with tools that have different annotation hints
	mockTools := []mcp.Tool{
		{
			Name:        "delete_records",
			Description: "Delete records from the database permanently",
			InputSchema: mcp.ToolInputSchema{
				Type:       "object",
				Properties: map[string]interface{}{},
			},
			Annotations: mcp.ToolAnnotation{
				DestructiveHint: &trueVal,
				ReadOnlyHint:    &falseVal,
			},
		},
		{
			Name:        "update_config",
			Description: "Update server configuration settings",
			InputSchema: mcp.ToolInputSchema{
				Type:       "object",
				Properties: map[string]interface{}{},
			},
			Annotations: mcp.ToolAnnotation{
				ReadOnlyHint: &falseVal,
			},
		},
		{
			Name:        "list_items",
			Description: "List all items in the inventory",
			InputSchema: mcp.ToolInputSchema{
				Type:       "object",
				Properties: map[string]interface{}{},
			},
			Annotations: mcp.ToolAnnotation{
				ReadOnlyHint: &trueVal,
			},
		},
	}

	mockServer := env.CreateMockUpstreamServer("annotated", mockTools)

	mcpClient := env.CreateProxyClient()
	defer mcpClient.Close()
	env.ConnectClient(mcpClient)

	ctx := context.Background()

	// Add and unquarantine the server
	addRequest := mcp.CallToolRequest{}
	addRequest.Params.Name = "upstream_servers"
	addRequest.Params.Arguments = map[string]interface{}{
		"operation": "add",
		"name":      "annotated",
		"url":       mockServer.addr,
		"protocol":  "streamable-http",
		"enabled":   true,
	}

	result, err := mcpClient.CallTool(ctx, addRequest)
	require.NoError(t, err)
	assert.False(t, result.IsError)

	serverConfig, err := env.proxyServer.runtime.StorageManager().GetUpstreamServer("annotated")
	require.NoError(t, err)
	serverConfig.Quarantined = false
	err = env.proxyServer.runtime.StorageManager().SaveUpstreamServer(serverConfig)
	require.NoError(t, err)

	servers, err := env.proxyServer.runtime.StorageManager().ListUpstreamServers()
	require.NoError(t, err)

	cfg := env.proxyServer.runtime.Config()
	cfg.Servers = servers
	err = env.proxyServer.runtime.LoadConfiguredServers(cfg)
	require.NoError(t, err)

	// Wait for connection and trigger tool discovery
	time.Sleep(4 * time.Second)
	err = env.proxyServer.runtime.DiscoverAndIndexTools(ctx)
	require.NoError(t, err)
	time.Sleep(2 * time.Second)

	// Do a broad search to find all annotated tools
	searchRequest := mcp.CallToolRequest{}
	searchRequest.Params.Name = "retrieve_tools"
	searchRequest.Params.Arguments = map[string]interface{}{
		"query": "delete update list records config items",
		"limit": 20,
	}
	searchResult, err := mcpClient.CallTool(ctx, searchRequest)
	require.NoError(t, err)
	assert.False(t, searchResult.IsError)

	require.Greater(t, len(searchResult.Content), 0)
	contentBytes, err := json.Marshal(searchResult.Content[0])
	require.NoError(t, err)
	var contentMap map[string]interface{}
	err = json.Unmarshal(contentBytes, &contentMap)
	require.NoError(t, err)
	contentText, ok := contentMap["text"].(string)
	require.True(t, ok)

	var searchResponse map[string]interface{}
	err = json.Unmarshal([]byte(contentText), &searchResponse)
	require.NoError(t, err)

	tools, ok := searchResponse["tools"].([]interface{})
	require.True(t, ok, "tools should be an array")
	t.Logf("Found %d tools in search", len(tools))
	for _, toolRaw := range tools {
		if tool, ok := toolRaw.(map[string]interface{}); ok {
			t.Logf("  Tool: name=%v, server=%v, call_with=%v, annotations=%v",
				tool["name"], tool["server"], tool["call_with"], tool["annotations"])
		}
	}
	require.GreaterOrEqual(t, len(tools), 3, "Should find all 3 annotated tools")

	// Verify call_with for each tool
	// Note: tool names may be bare ("delete_records") or prefixed ("annotated:delete_records")
	// depending on how they were indexed. Match by bare name + server field.
	expectedCallWith := map[string]string{
		"delete_records": "call_tool_destructive",
		"update_config":  "call_tool_write",
		"list_items":     "call_tool_read",
	}

	for _, toolRaw := range tools {
		tool, ok := toolRaw.(map[string]interface{})
		if !ok {
			continue
		}
		toolName, _ := tool["name"].(string)
		serverField, _ := tool["server"].(string)

		// Strip server prefix if present for matching
		bareName := toolName
		if parts := strings.SplitN(toolName, ":", 2); len(parts) == 2 {
			bareName = parts[1]
		}

		expected, isOurs := expectedCallWith[bareName]
		if !isOurs || serverField != "annotated" {
			continue
		}

		callWith, ok := tool["call_with"].(string)
		assert.True(t, ok, "call_with should be a string for %s", toolName)
		assert.Equal(t, expected, callWith,
			"call_with mismatch for %s: expected %s, got %s",
			toolName, expected, callWith)

		// Verify annotations are present for destructive/write tools
		if expected != "call_tool_read" {
			assert.NotNil(t, tool["annotations"],
				"annotations should be present for %s", toolName)
		}

		delete(expectedCallWith, bareName)
	}

	assert.Empty(t, expectedCallWith, "Not all expected tools were found: %v", expectedCallWith)
}
