package server

import (
	"context"
	"encoding/json"
	"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"
	"github.com/zzet/gortex/internal/graph"
	"go.uber.org/zap"
)

func newTestHandler(t *testing.T) *Handler {
	t.Helper()

	g := graph.New()
	g.AddNode(&graph.Node{
		ID: "test.go::Foo", Kind: graph.KindFunction,
		Name: "Foo", FilePath: "test.go", Language: "go",
	})
	g.AddNode(&graph.Node{
		ID: "test.go::Bar", Kind: graph.KindFunction,
		Name: "Bar", FilePath: "test.go", Language: "go",
	})

	srv := mcpserver.NewMCPServer("gortex-test", "0.0.1-test",
		mcpserver.WithToolCapabilities(false),
		mcpserver.WithRecovery(),
	)
	srv.AddTool(
		mcp.NewTool("echo",
			mcp.WithDescription("Echo tool for testing"),
			mcp.WithString("message", mcp.Description("Message to echo")),
		),
		func(_ context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
			args := req.GetArguments()
			msg, _ := args["message"].(string)
			if msg == "" {
				msg = "no message"
			}
			return mcp.NewToolResultText(msg), nil
		},
	)

	return NewHandler(srv, g, "0.0.1-test", zap.NewNop())
}

func TestHealthEndpoint(t *testing.T) {
	h := newTestHandler(t)
	req := httptest.NewRequest(http.MethodGet, "/v1/health", nil)
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)

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

	var resp HealthResponse
	require.NoError(t, json.NewDecoder(rec.Body).Decode(&resp))
	assert.Equal(t, "ok", resp.Status)
	assert.True(t, resp.Indexed)
	assert.Equal(t, 2, resp.Nodes)
	assert.Equal(t, "0.0.1-test", resp.Version)
	assert.Greater(t, resp.UptimeSeconds, float64(0))
}

func TestListToolsEndpoint(t *testing.T) {
	h := newTestHandler(t)
	req := httptest.NewRequest(http.MethodGet, "/v1/tools", nil)
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)

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

	var tools []toolInfo
	require.NoError(t, json.NewDecoder(rec.Body).Decode(&tools))
	require.Len(t, tools, 1)
	assert.Equal(t, "echo", tools[0].Name)
	assert.Equal(t, "Echo tool for testing", tools[0].Description)
}

func TestStatsEndpoint(t *testing.T) {
	h := newTestHandler(t)
	req := httptest.NewRequest(http.MethodGet, "/v1/stats", nil)
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)

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

	var resp StatsResponse
	require.NoError(t, json.NewDecoder(rec.Body).Decode(&resp))
	assert.Equal(t, 2, resp.TotalNodes)
	assert.Equal(t, 2, resp.ByKind["function"])
	assert.False(t, resp.StartedAt.IsZero(), "StartedAt should be populated")
	assert.Empty(t, resp.ServerID, "ServerID empty until SetServerID is called")
}

func TestStatsEndpoint_WithServerID(t *testing.T) {
	h := newTestHandler(t)
	h.SetServerID("11111111-2222-3333-4444-555555555555")

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

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

	var resp StatsResponse
	require.NoError(t, json.NewDecoder(rec.Body).Decode(&resp))
	assert.Equal(t, "11111111-2222-3333-4444-555555555555", resp.ServerID)
}

func TestToolCallValid(t *testing.T) {
	h := newTestHandler(t)
	body := `{"arguments":{"message":"hello world"}}`
	req := httptest.NewRequest(http.MethodPost, "/v1/tools/echo", strings.NewReader(body))
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)

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

	var resp ToolResponse
	require.NoError(t, json.NewDecoder(rec.Body).Decode(&resp))
	assert.False(t, resp.IsError)
	require.Len(t, resp.Content, 1)
	assert.Equal(t, "hello world", resp.Content[0].Text)
}

func TestToolCallUnknownTool(t *testing.T) {
	h := newTestHandler(t)
	req := httptest.NewRequest(http.MethodPost, "/v1/tools/nonexistent", strings.NewReader("{}"))
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)

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

	var resp map[string]any
	require.NoError(t, json.NewDecoder(rec.Body).Decode(&resp))
	assert.Equal(t, "tool_not_found", resp["error"])
	available, ok := resp["available_tools"].([]any)
	require.True(t, ok)
	assert.Contains(t, available, "echo")
}

func TestToolCallMalformedJSON(t *testing.T) {
	h := newTestHandler(t)
	req := httptest.NewRequest(http.MethodPost, "/v1/tools/echo", strings.NewReader("{bad"))
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)

	assert.Equal(t, http.StatusBadRequest, rec.Code)
}

func TestToolCallEmptyName(t *testing.T) {
	h := newTestHandler(t)
	req := httptest.NewRequest(http.MethodPost, "/v1/tools/", strings.NewReader("{}"))
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)

	assert.Equal(t, http.StatusBadRequest, rec.Code)
}

func TestToolCallAcceptsQueryFormat(t *testing.T) {
	h := newTestHandler(t)
	// echo returns its "message" argument; here we smuggle the format
	// through "message" to assert the merge happened. Use a tool that
	// echoes the argument map — simplest path is to register a fresh
	// tool that stringifies args.
	body := `{"arguments":{"message":"fmt"}}`
	req := httptest.NewRequest(http.MethodPost, "/v1/tools/echo?format=gcx", strings.NewReader(body))
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)

	// The echo tool ignores "format", so the ok status is the
	// only observable contract here — the point is that the merge
	// didn't corrupt the request. Dedicated merge logic is covered by
	// TestToolCall_MergesQueryFormat.
	assert.Equal(t, http.StatusOK, rec.Code)
}

func TestToolCall_MergesQueryFormat(t *testing.T) {
	g := graph.New()
	srv := mcpserver.NewMCPServer("gortex-test", "0.0.1-test",
		mcpserver.WithToolCapabilities(false),
	)
	srv.AddTool(
		mcp.NewTool("spy",
			mcp.WithDescription("returns its format arg"),
			mcp.WithString("format"),
		),
		func(_ context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
			args := req.GetArguments()
			fmtArg, _ := args["format"].(string)
			return mcp.NewToolResultText(fmtArg), nil
		},
	)
	h := NewHandler(srv, g, "0.0.1-test", zap.NewNop())

	// Query param.
	req := httptest.NewRequest(http.MethodPost, "/v1/tools/spy?format=gcx", strings.NewReader("{}"))
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)
	require.Equal(t, http.StatusOK, rec.Code)
	var resp ToolResponse
	require.NoError(t, json.NewDecoder(rec.Body).Decode(&resp))
	require.Len(t, resp.Content, 1)
	assert.Equal(t, "gcx", resp.Content[0].Text)

	// Body-level format field.
	req = httptest.NewRequest(http.MethodPost, "/v1/tools/spy",
		strings.NewReader(`{"format":"toon"}`))
	rec = httptest.NewRecorder()
	h.ServeHTTP(rec, req)
	require.Equal(t, http.StatusOK, rec.Code)
	require.NoError(t, json.NewDecoder(rec.Body).Decode(&resp))
	require.Len(t, resp.Content, 1)
	assert.Equal(t, "toon", resp.Content[0].Text)

	// Explicit arguments.format wins over query.
	req = httptest.NewRequest(http.MethodPost, "/v1/tools/spy?format=gcx",
		strings.NewReader(`{"arguments":{"format":"json"}}`))
	rec = httptest.NewRecorder()
	h.ServeHTTP(rec, req)
	require.Equal(t, http.StatusOK, rec.Code)
	require.NoError(t, json.NewDecoder(rec.Body).Decode(&resp))
	require.Len(t, resp.Content, 1)
	assert.Equal(t, "json", resp.Content[0].Text)
}

func TestPanicRecovery(t *testing.T) {
	g := graph.New()
	srv := mcpserver.NewMCPServer("gortex-test", "0.0.1-test",
		mcpserver.WithToolCapabilities(false),
	)
	srv.AddTool(
		mcp.NewTool("panic_tool", mcp.WithDescription("panics")),
		func(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
			panic("test panic")
		},
	)
	h := NewHandler(srv, g, "0.0.1-test", zap.NewNop())

	req := httptest.NewRequest(http.MethodPost, "/v1/tools/panic_tool", strings.NewReader("{}"))
	rec := httptest.NewRecorder()
	assert.NotPanics(t, func() { h.ServeHTTP(rec, req) })
	assert.Equal(t, http.StatusInternalServerError, rec.Code)
}

func TestCallToolHelper(t *testing.T) {
	h := newTestHandler(t)
	result := h.CallTool(context.Background(), "echo", map[string]any{"message": "test"})
	assert.Equal(t, "test", result)

	result = h.CallTool(context.Background(), "nonexistent", nil)
	assert.Empty(t, result)
}

func TestV1GraphEndpoint_FullDump(t *testing.T) {
	h := newTestHandler(t)
	req := httptest.NewRequest(http.MethodGet, "/v1/graph", nil)
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)

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

	var resp GraphResponse
	require.NoError(t, json.NewDecoder(rec.Body).Decode(&resp))
	assert.Len(t, resp.Nodes, 2)
	assert.Equal(t, 2, resp.Stats.TotalNodes)
	// Heavy fields stripped.
	for _, n := range resp.Nodes {
		assert.Empty(t, n.QualName)
		assert.Zero(t, n.EndLine)
	}
}

func TestV1GraphEndpoint_RepoFilter(t *testing.T) {
	g := graph.New()
	g.AddNode(&graph.Node{
		ID: "frontend::App", Kind: graph.KindFunction, Name: "App",
		FilePath: "src/app.ts", Language: "typescript", RepoPrefix: "frontend",
	})
	g.AddNode(&graph.Node{
		ID: "backend::Server", Kind: graph.KindFunction, Name: "Server",
		FilePath: "cmd/main.go", Language: "go", RepoPrefix: "backend",
	})
	srv := mcpserver.NewMCPServer("gortex-test", "0.0.1-test",
		mcpserver.WithToolCapabilities(false),
	)
	h := NewHandler(srv, g, "0.0.1-test", zap.NewNop())

	req := httptest.NewRequest(http.MethodGet, "/v1/graph?repo=frontend", nil)
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)

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

	var resp GraphResponse
	require.NoError(t, json.NewDecoder(rec.Body).Decode(&resp))
	require.Len(t, resp.Nodes, 1)
	assert.Equal(t, "frontend::App", resp.Nodes[0].ID)
	// Filtered dump: stats are zeroed; counts come from the nodes array.
	assert.Equal(t, 0, resp.Stats.TotalNodes)
}

func TestV1GraphEndpoint_ProjectWithoutConfigManager(t *testing.T) {
	h := newTestHandler(t)
	req := httptest.NewRequest(http.MethodGet, "/v1/graph?project=my-saas", nil)
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)

	assert.Equal(t, http.StatusBadRequest, rec.Code)
	var body map[string]string
	require.NoError(t, json.NewDecoder(rec.Body).Decode(&body))
	assert.Contains(t, body["message"], "multi-repo config")
}

func TestV1EventsEndpoint_NoHub(t *testing.T) {
	h := newTestHandler(t)
	req := httptest.NewRequest(http.MethodGet, "/v1/events", nil)
	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, req)

	assert.Equal(t, http.StatusOK, rec.Code)
	assert.Equal(t, "text/event-stream", rec.Header().Get("Content-Type"))
	assert.Contains(t, rec.Body.String(), "watch mode not active")
}
