package workbench

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"sync/atomic"
	"testing"
	"time"
)

// minimalMCP is a scriptable JSON-RPC MCP server for exercising the client.
type minimalMCP struct {
	t         *testing.T
	server    *httptest.Server
	tools     []MCPTool
	initCount int32
	listCount int32
	callCount int32
	lastArgs  map[string]any
	lastTool  string
	versioned bool
}

func newMinimalMCP(t *testing.T) *minimalMCP {
	m := &minimalMCP{t: t, tools: []MCPTool{
		{Name: "lookup", Description: "look something up", InputSchema: json.RawMessage(`{"type":"object","properties":{"q":{"type":"string"}},"required":["q"]}`)},
		{Name: "add", Description: "add two numbers", InputSchema: json.RawMessage(`{"type":"object","properties":{"a":{"type":"number"},"b":{"type":"number"}},"required":["a","b"]}`)},
	}}
	m.server = httptest.NewServer(http.HandlerFunc(m.serve))
	t.Cleanup(m.server.Close)
	return m
}

func (m *minimalMCP) endpoint() string { return m.server.URL + "/mcp" }

func (m *minimalMCP) serve(w http.ResponseWriter, r *http.Request) {
	var req mcpRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "bad json", http.StatusBadRequest)
		return
	}
	switch req.Method {
	case "initialize":
		atomic.AddInt32(&m.initCount, 1)
		params, _ := req.Params.(map[string]any)
		clientInfo, _ := params["clientInfo"].(map[string]any)
		if clientInfo["name"] == nil || clientInfo["name"] == "" {
			m.t.Error("initialize missing clientInfo.name")
		}
		result := map[string]any{
			"protocolVersion": "2025-06-18",
			"capabilities":    map[string]any{"tools": map[string]any{}},
			"serverInfo":      map[string]any{"name": "test-mcp", "version": "1.0.0"},
		}
		m.write(w, req.ID, result, false)
	case "notifications/initialized":
		w.WriteHeader(http.StatusAccepted)
	case "tools/list":
		atomic.AddInt32(&m.listCount, 1)
		m.write(w, req.ID, map[string]any{"tools": m.tools}, false)
	case "tools/call":
		atomic.AddInt32(&m.callCount, 1)
		params, _ := req.Params.(map[string]any)
		name, _ := params["name"].(string)
		args, _ := params["arguments"].(map[string]any)
		m.lastArgs = args
		m.lastTool = name
		if name == "boom" {
			m.write(w, req.ID, map[string]any{"content": []map[string]any{{"type": "text", "text": "failed intentionally"}}, "isError": true}, false)
			return
		}
		if name == "nope" {
			m.write(w, req.ID, nil, true)
			return
		}
		m.write(w, req.ID, map[string]any{"content": []map[string]any{{"type": "text", "text": "result for " + name}}, "isError": false}, false)
	case "tools/fail":
		m.write(w, req.ID, nil, true)
	default:
		http.Error(w, "unknown method", http.StatusBadRequest)
	}
}

func (m *minimalMCP) write(w http.ResponseWriter, id any, result any, isError bool) {
	w.Header().Set("Content-Type", "application/json")
	if isError {
		json.NewEncoder(w).Encode(map[string]any{"jsonrpc": "2.0", "id": id, "error": map[string]any{"code": -32601, "message": "method not found"}})
		return
	}
	json.NewEncoder(w).Encode(map[string]any{"jsonrpc": "2.0", "id": id, "result": result})
}

func testServer(code string) MCPServer {
	return MCPServer{ID: "00000000-0000-0000-0000-000000000001", Code: code}
}

func TestMCPDiscoverTools(t *testing.T) {
	mcp := newMinimalMCP(t)
	client := NewMCPClient(true, time.Minute)
	server := testServer("demo")
	server.EndpointURL = mcp.endpoint()

	tools, err := client.DiscoverTools(context.Background(), server, nil)
	if err != nil {
		t.Fatalf("discover failed: %v", err)
	}
	if len(tools) != 2 || tools[0].Name != "lookup" {
		t.Fatalf("expected 2 tools starting with lookup, got %+v", tools)
	}
	if atomic.LoadInt32(&mcp.initCount) != 1 {
		t.Fatalf("expected exactly one initialize handshake, got %d", mcp.initCount)
	}
	// Second call hits the tool cache, no new tools/list.
	if _, err = client.DiscoverTools(context.Background(), server, nil); err != nil {
		t.Fatalf("cached discover failed: %v", err)
	}
	if atomic.LoadInt32(&mcp.listCount) != 1 {
		t.Fatalf("expected tools/list to run once, got %d", mcp.listCount)
	}
}

func TestMCPCacheExpiryRediscover(t *testing.T) {
	mcp := newMinimalMCP(t)
	client := NewMCPClient(true, 50*time.Millisecond)
	server := testServer("expire")
	server.EndpointURL = mcp.endpoint()

	if _, err := client.DiscoverTools(context.Background(), server, nil); err != nil {
		t.Fatalf("discover failed: %v", err)
	}
	time.Sleep(80 * time.Millisecond)
	if _, err := client.DiscoverTools(context.Background(), server, nil); err != nil {
		t.Fatalf("rediscover failed: %v", err)
	}
	if atomic.LoadInt32(&mcp.listCount) != 2 {
		t.Fatalf("expected tools/list to run twice after expiry, got %d", mcp.listCount)
	}
}

func TestMCPCallTool(t *testing.T) {
	mcp := newMinimalMCP(t)
	client := NewMCPClient(true, time.Minute)
	server := testServer("call")
	server.EndpointURL = mcp.endpoint()

	result, err := client.CallTool(context.Background(), server, nil, "lookup", map[string]any{"q": "hello"})
	if err != nil {
		t.Fatalf("call failed: %v", err)
	}
	if result.Content != "result for lookup" {
		t.Fatalf("unexpected content %q", result.Content)
	}
	if mcp.lastArgs["q"] != "hello" {
		t.Fatalf("arguments not forwarded: %v", mcp.lastArgs)
	}
	// The runtime exposes tools under the mcp__{code}__{tool} prefix; CallTool
	// must strip it so the remote server sees the real tool name.
	if mcp.lastTool != "lookup" {
		t.Fatalf("tool name not sent as-is: %q", mcp.lastTool)
	}
	if _, err := client.CallTool(context.Background(), server, nil, "mcp__call__lookup", map[string]any{"q": "x"}); err != nil {
		t.Fatalf("prefixed call failed: %v", err)
	}
	if mcp.lastTool != "lookup" {
		t.Fatalf("prefixed tool name was not stripped: %q", mcp.lastTool)
	}
}

func TestMCPCallToolIsError(t *testing.T) {
	mcp := newMinimalMCP(t)
	client := NewMCPClient(true, time.Minute)
	server := testServer("err")
	server.EndpointURL = mcp.endpoint()

	if _, err := client.CallTool(context.Background(), server, nil, "boom", nil); err == nil {
		t.Fatal("expected error for isError tool result")
	}
}

func TestMCPJSONRPCError(t *testing.T) {
	mcp := newMinimalMCP(t)
	client := NewMCPClient(true, time.Minute)
	server := testServer("rpc")
	server.EndpointURL = mcp.endpoint()

	if _, err := client.CallTool(context.Background(), server, nil, "nope", nil); err == nil || !strings.Contains(err.Error(), "method not found") {
		t.Fatalf("expected JSON-RPC error surfaced, got %v", err)
	}
}

func TestMCPToolNamePrefixRoundTrip(t *testing.T) {
	prefixed := mcpToolName("github", "create-issue")
	if prefixed != "mcp__github__create-issue" {
		t.Fatalf("unexpected prefix %q", prefixed)
	}
	code, tool, ok := resolveMCPTool(prefixed)
	if !ok || code != "github" || tool != "create-issue" {
		t.Fatalf("resolve failed: %q %q %v", code, tool, ok)
	}
	if _, _, ok := resolveMCPTool("plain-name"); ok {
		t.Fatal("non-prefixed name should not resolve as MCP tool")
	}
}

func TestMCPSSESingleFrame(t *testing.T) {
	handler := func(w http.ResponseWriter, r *http.Request) {
		var req mcpRequest
		_ = json.NewDecoder(r.Body).Decode(&req)
		w.Header().Set("Content-Type", "text/event-stream")
		switch req.Method {
		case "initialize":
			payload, _ := json.Marshal(map[string]any{"jsonrpc": "2.0", "id": req.ID, "result": map[string]any{"protocolVersion": "2025-06-18", "capabilities": map[string]any{}, "serverInfo": map[string]any{"name": "sse", "version": "1"}}})
			w.Write([]byte("event: message\ndata: " + string(payload) + "\n\n"))
		case "notifications/initialized":
			w.WriteHeader(http.StatusAccepted)
		case "tools/list":
			payload, _ := json.Marshal(map[string]any{"jsonrpc": "2.0", "id": req.ID, "result": map[string]any{"tools": []MCPTool{{Name: "sse-tool", Description: "", InputSchema: json.RawMessage(`{}`)}}}})
			w.Write([]byte("event: message\ndata: " + string(payload) + "\n\n"))
		}
	}
	server := httptest.NewServer(http.HandlerFunc(handler))
	defer server.Close()

	client := NewMCPClient(true, time.Minute)
	svr := testServer("sse")
	svr.EndpointURL = server.URL
	tools, err := client.DiscoverTools(context.Background(), svr, nil)
	if err != nil {
		t.Fatalf("SSE discover failed: %v", err)
	}
	if len(tools) != 1 || tools[0].Name != "sse-tool" {
		t.Fatalf("unexpected SSE tools: %+v", tools)
	}
}
