package server

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"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/zzet/gortex/internal/graph"
)

// TestCallToolStrict_Success returns the text content with a nil error
// when the tool returns a normal text result.
func TestCallToolStrict_Success(t *testing.T) {
	h := newTestHandler(t)
	text, err := h.CallToolStrict(context.Background(), "echo", map[string]any{"message": "hello"})
	require.NoError(t, err)
	assert.Equal(t, "hello", text)
}

// TestCallToolStrict_MissingTool returns an error when the tool name is not
// registered, instead of silently returning an empty string.
func TestCallToolStrict_MissingTool(t *testing.T) {
	h := newTestHandler(t)
	_, err := h.CallToolStrict(context.Background(), "no-such-tool", nil)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "no-such-tool")
	assert.Contains(t, err.Error(), "not registered")
}

// TestCallToolStrict_ToolErrorResult promotes an MCP IsError=true result to
// a Go error. This is the contract that handleContracts depends on to surface
// 5xx instead of pretending the call succeeded with empty content.
func TestCallToolStrict_ToolErrorResult(t *testing.T) {
	g := graph.New()
	srv := mcpserver.NewMCPServer("gortex-test", "0.0.1-test",
		mcpserver.WithToolCapabilities(false),
	)
	srv.AddTool(
		mcp.NewTool("erroring_tool", mcp.WithDescription("returns IsError=true")),
		func(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
			return mcp.NewToolResultError("project not found: \"gortex\""), nil
		},
	)
	h := NewHandler(srv, g, "0.0.1-test", zap.NewNop())

	text, err := h.CallToolStrict(context.Background(), "erroring_tool", nil)
	require.Error(t, err, "IsError=true must be promoted to a Go error")
	assert.Contains(t, err.Error(), "project not found")
	// The text content is preserved so callers can put it in the response body.
	assert.Contains(t, text, "project not found")
}

// TestCallToolStrict_HandlerError surfaces a Go-level handler error.
func TestCallToolStrict_HandlerError(t *testing.T) {
	g := graph.New()
	srv := mcpserver.NewMCPServer("gortex-test", "0.0.1-test",
		mcpserver.WithToolCapabilities(false),
	)
	srv.AddTool(
		mcp.NewTool("failing_tool", mcp.WithDescription("returns Go-level error")),
		func(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
			return nil, errors.New("upstream blew up")
		},
	)
	h := NewHandler(srv, g, "0.0.1-test", zap.NewNop())

	_, err := h.CallToolStrict(context.Background(), "failing_tool", nil)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "upstream blew up")
}

// TestCallTool_BackwardsCompatible — the legacy best-effort wrapper still
// returns "" on error and never panics. Eval/http_handler.go and similar
// callers depend on this contract.
func TestCallTool_BackwardsCompatible(t *testing.T) {
	g := graph.New()
	srv := mcpserver.NewMCPServer("gortex-test", "0.0.1-test",
		mcpserver.WithToolCapabilities(false),
	)
	srv.AddTool(
		mcp.NewTool("erroring_tool", mcp.WithDescription("")),
		func(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
			return mcp.NewToolResultError("nope"), nil
		},
	)
	h := NewHandler(srv, g, "0.0.1-test", zap.NewNop())

	// IsError=true: legacy CallTool returns the text (best-effort) and
	// callers cannot tell it was an error. This is the contract callers
	// like eval/http_handler.go rely on; the ones that need to know now
	// use CallToolStrict.
	got := h.CallTool(context.Background(), "erroring_tool", nil)
	assert.Equal(t, "nope", got)

	// Missing tool: still returns "".
	got = h.CallTool(context.Background(), "no-such-tool", nil)
	assert.Empty(t, got)
}

// TestHandleContracts_ToolError_500 verifies the user-visible bug fix: when
// the underlying contracts tool returns an error result (e.g. project not
// found), /v1/contracts must return HTTP 500 with the error in the body, not
// a fake empty 200.
func TestHandleContracts_ToolError_500(t *testing.T) {
	g := graph.New()
	srv := mcpserver.NewMCPServer("gortex-test", "0.0.1-test",
		mcpserver.WithToolCapabilities(false),
	)
	srv.AddTool(
		mcp.NewTool("contracts", mcp.WithDescription("contracts stub")),
		func(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
			return mcp.NewToolResultError(`project not found: "gortex" (available: )`), nil
		},
	)
	h := NewHandler(srv, g, "0.0.1-test", zap.NewNop())

	req := httptest.NewRequest(http.MethodGet, "/v1/contracts", nil)
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)

	assert.Equal(t, http.StatusInternalServerError, rec.Code,
		"tool error must surface as 5xx, not 200 with empty body")

	var body map[string]any
	require.NoError(t, json.NewDecoder(rec.Body).Decode(&body))
	msg, _ := body["message"].(string)
	assert.True(t, strings.Contains(msg, "project not found"),
		"error body must include the underlying tool message; got: %q", msg)
}

// TestHandleContracts_Success_200 verifies the happy path still works:
// contracts tool returns a valid JSON payload → handler 200s with the
// flattened list.
func TestHandleContracts_Success_200(t *testing.T) {
	g := graph.New()
	srv := mcpserver.NewMCPServer("gortex-test", "0.0.1-test",
		mcpserver.WithToolCapabilities(false),
	)
	srv.AddTool(
		mcp.NewTool("contracts", mcp.WithDescription("contracts stub")),
		func(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
			payload := `{"by_repo":{"alpha":{"contracts":{"http":[{"id":"GET /foo","type":"http","role":"provider","symbol_id":"alpha/x.go::H","file_path":"alpha/x.go","line":10,"repo_prefix":"alpha"}]},"total":1}}}`
			return mcp.NewToolResultText(payload), nil
		},
	)
	h := NewHandler(srv, g, "0.0.1-test", zap.NewNop())

	req := httptest.NewRequest(http.MethodGet, "/v1/contracts", nil)
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)

	assert.Equal(t, http.StatusOK, rec.Code)

	var body struct {
		Contracts []map[string]any `json:"contracts"`
	}
	require.NoError(t, json.NewDecoder(rec.Body).Decode(&body))
	require.Len(t, body.Contracts, 1)
	assert.Equal(t, "GET /foo", body.Contracts[0]["id"])
}
