//go:build e2e

package api

import (
	"bufio"
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"testing"

	"github.com/gin-gonic/gin"
	"github.com/pingcap/failpoint"
	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/desktop/internal/api/dashboard"
	"github.com/tidbcloud/AgentTeam/desktop/internal/api/dto"
)

func setupMCPServersAPIRouter(t *testing.T) *gin.Engine {
	t.Helper()
	cfgDir := t.TempDir()
	require.NoError(t, failpoint.Enable("agentteam-config-dir", fmt.Sprintf(`return("%s")`, cfgDir)))
	t.Cleanup(func() { require.NoError(t, failpoint.Disable("agentteam-config-dir")) })

	gin.SetMode(gin.TestMode)
	router := gin.New()
	group := router.Group("/api/internal")
	group.GET("/mcp-servers", dashboard.NewListMCPServersHandler())
	group.POST("/mcp-servers", dashboard.NewCreateMCPServerHandler())
	group.POST("/mcp-servers/test", dashboard.NewTestUnsavedMCPServerHandler())
	group.GET("/mcp-servers/:id", dashboard.NewGetMCPServerHandler())
	group.PUT("/mcp-servers/:id", dashboard.NewUpdateMCPServerHandler())
	group.DELETE("/mcp-servers/:id", dashboard.NewDeleteMCPServerHandler())
	group.POST("/mcp-servers/:id/test", dashboard.NewTestMCPServerHandler())
	return router
}

func doMCPServersJSON[T any](t *testing.T, router *gin.Engine, method, path string, wantStatus int, body any) T {
	t.Helper()
	var reader io.Reader
	if body != nil {
		payload, err := json.Marshal(body)
		require.NoError(t, err)
		reader = bytes.NewReader(payload)
	}
	req := httptest.NewRequest(method, path, reader)
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)
	require.Equal(t, wantStatus, rec.Code, rec.Body.String())
	var decoded T
	if rec.Body.Len() > 0 {
		require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &decoded))
	}
	return decoded
}

func doMCPServersNoBody(t *testing.T, router *gin.Engine, method, path string, wantStatus int) {
	t.Helper()
	req := httptest.NewRequest(method, path, nil)
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)
	require.Equal(t, wantStatus, rec.Code, rec.Body.String())
}

func TestMCPServersAPI_CRUDAndHTTPCheck(t *testing.T) {
	router := setupMCPServersAPIRouter(t)
	initialized := false
	httpFixture := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, "Bearer e2e-token", r.Header.Get("Authorization"))
		var req struct {
			ID     json.RawMessage `json:"id"`
			Method string          `json:"method"`
		}
		require.NoError(t, json.NewDecoder(r.Body).Decode(&req))
		switch req.Method {
		case "initialize":
			w.Header().Set("Mcp-Session-Id", "e2e-session")
			_, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{"tools":{}},"serverInfo":{"name":"http-fixture","version":"1"}}}`))
		case "notifications/initialized":
			require.Equal(t, "e2e-session", r.Header.Get("Mcp-Session-Id"))
			initialized = true
			w.WriteHeader(http.StatusAccepted)
		case "tools/list":
			require.True(t, initialized, "tools/list must follow notifications/initialized")
			require.Equal(t, "e2e-session", r.Header.Get("Mcp-Session-Id"))
			_, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"http_fixture_tool","description":"HTTP fixture tool"}]}}`))
		default:
			w.WriteHeader(http.StatusBadRequest)
		}
	}))
	t.Cleanup(httpFixture.Close)

	enabled := true
	created := doMCPServersJSON[dto.MCPServerResponse](t, router, http.MethodPost, "/api/internal/mcp-servers", http.StatusCreated, dto.MCPServerRequest{
		ID:          "http_fixture",
		DisplayName: "HTTP Fixture",
		Description: "HTTP fixture server",
		Enabled:     &enabled,
		Transport:   "http",
		HTTP: &dto.MCPServerHTTPConfig{
			URL:     httpFixture.URL,
			Headers: []string{"Authorization: Bearer e2e-token"},
		},
	})
	require.Equal(t, "http_fixture", created.ID)
	require.Empty(t, created.HTTP.Headers)

	list := doMCPServersJSON[[]dto.MCPServerResponse](t, router, http.MethodGet, "/api/internal/mcp-servers", http.StatusOK, nil)
	require.Len(t, list, 1)
	require.Equal(t, "http_fixture", list[0].ID)
	require.Empty(t, list[0].HTTP.Headers)

	updated := doMCPServersJSON[dto.MCPServerResponse](t, router, http.MethodPut, "/api/internal/mcp-servers/http_fixture", http.StatusOK, dto.MCPServerRequest{
		DisplayName: "HTTP Fixture Updated",
		Enabled:     &enabled,
		Transport:   "http",
		HTTP: &dto.MCPServerHTTPConfig{
			URL:     httpFixture.URL,
			Headers: []string{"Authorization: Bearer e2e-token"},
		},
	})
	require.Equal(t, "HTTP Fixture Updated", updated.DisplayName)
	require.Empty(t, updated.HTTP.Headers)

	check := doMCPServersJSON[dto.MCPServerCheckResponse](t, router, http.MethodPost, "/api/internal/mcp-servers/http_fixture/test", http.StatusOK, nil)
	require.True(t, check.OK, check.Message)
	require.Equal(t, "tools", check.Stage)
	require.Len(t, check.Tools, 1)
	require.Equal(t, "http_fixture_tool", check.Tools[0].Name)

	doMCPServersNoBody(t, router, http.MethodDelete, "/api/internal/mcp-servers/http_fixture", http.StatusNoContent)
	doMCPServersNoBody(t, router, http.MethodGet, "/api/internal/mcp-servers/http_fixture", http.StatusNotFound)
}

func TestMCPServersAPI_StdioFixtureCheck(t *testing.T) {
	router := setupMCPServersAPIRouter(t)
	fixtureBinary, err := os.Executable()
	require.NoError(t, err)
	enabled := true

	created := doMCPServersJSON[dto.MCPServerResponse](t, router, http.MethodPost, "/api/internal/mcp-servers", http.StatusCreated, dto.MCPServerRequest{
		ID:          "stdio_fixture",
		DisplayName: "Stdio Fixture",
		Enabled:     &enabled,
		Transport:   "stdio",
		Stdio: &dto.MCPServerStdioConfig{
			Command: fixtureBinary,
			Args:    []string{"-test.run=TestMCPStdioFixtureProcess", "--"},
			CWD:     t.TempDir(),
			Env: []string{
				"AGENTTEAM_MCP_STDIO_FIXTURE=1",
				"AGENTTEAM_MCP_STDIO_TOKEN=stdio-secret",
			},
		},
	})
	require.Equal(t, "stdio_fixture", created.ID)
	require.Empty(t, created.Stdio.Env)

	check := doMCPServersJSON[dto.MCPServerCheckResponse](t, router, http.MethodPost, "/api/internal/mcp-servers/stdio_fixture/test", http.StatusOK, nil)
	require.True(t, check.OK, check.Message)
	require.Equal(t, "tools", check.Stage)
	require.Len(t, check.Tools, 1)
	require.Equal(t, "stdio_fixture_tool", check.Tools[0].Name)
}

func TestMCPStdioFixtureProcess(t *testing.T) {
	if os.Getenv("AGENTTEAM_MCP_STDIO_FIXTURE") != "1" {
		t.Skip("helper process only")
	}
	if os.Getenv("AGENTTEAM_MCP_STDIO_TOKEN") != "stdio-secret" {
		_, _ = fmt.Fprintln(os.Stderr, "missing fixture token")
		os.Exit(2)
	}
	scanner := bufio.NewScanner(os.Stdin)
	encoder := json.NewEncoder(os.Stdout)
	for scanner.Scan() {
		var req struct {
			JSONRPC string          `json:"jsonrpc"`
			ID      json.RawMessage `json:"id,omitempty"`
			Method  string          `json:"method"`
		}
		if err := json.Unmarshal(scanner.Bytes(), &req); err != nil {
			_, _ = fmt.Fprintf(os.Stderr, "decode request: %v\n", err)
			os.Exit(3)
		}
		switch req.Method {
		case "initialize":
			_ = encoder.Encode(map[string]any{
				"jsonrpc": "2.0",
				"id":      json.RawMessage(req.ID),
				"result": map[string]any{
					"protocolVersion": "2024-11-05",
					"capabilities":    map[string]any{"tools": map[string]any{}},
					"serverInfo":      map[string]string{"name": "stdio-fixture", "version": "1"},
				},
			})
		case "notifications/initialized":
			continue
		case "tools/list":
			_ = encoder.Encode(map[string]any{
				"jsonrpc": "2.0",
				"id":      json.RawMessage(req.ID),
				"result": map[string]any{
					"tools": []map[string]string{{
						"name":        "stdio_fixture_tool",
						"description": "Stdio fixture tool",
					}},
				},
			})
			os.Exit(0)
		default:
			_ = encoder.Encode(map[string]any{
				"jsonrpc": "2.0",
				"id":      json.RawMessage(req.ID),
				"error":   map[string]any{"code": -32601, "message": "method not found"},
			})
		}
	}
	if err := scanner.Err(); err != nil {
		_, _ = fmt.Fprintf(os.Stderr, "read stdin: %v\n", err)
		os.Exit(4)
	}
	os.Exit(0)
}
