//go:build e2e

package e2e

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"

	"github.com/gin-gonic/gin"
	"github.com/pingcap/failpoint"

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

// ---------------------------------------------------------------------------

func newFullRouter(t *testing.T, servers *profile.ServerManager) *gin.Engine {
	t.Helper()
	gin.SetMode(gin.TestMode)
	r := gin.New()
	r.Use(gin.Recovery())

	api := r.Group("/api/internal")
	api.GET("", dashboard.NewOverviewHandler())
	api.GET("/", dashboard.NewOverviewHandler())
	api.GET("/overview", dashboard.NewOverviewHandler())

	api.GET("/agents", dashboard.NewListAgentsHandler())
	api.GET("/agents/:id", dashboard.NewGetAgentHandler())

	registerE2EServerConfigRoutes(api, "/servers", servers)
	registerE2EServerConfigRoutes(api, "/channels", servers)

	api.GET("/skills", dashboard.NewListSkillsHandler())
	api.POST("/skills/import", dashboard.NewImportSkillHandler())
	api.GET("/skills/:id", dashboard.NewGetSkillHandler())
	api.PUT("/skills/:id", dashboard.NewUpdateSkillHandler())
	api.DELETE("/skills/:id", dashboard.NewDeleteSkillHandler())

	api.GET("/help-docs", dashboard.NewHelpDocsHandler())

	api.GET("/settings", dashboard.NewGetSettingsHandler())
	api.PUT("/settings", dashboard.NewUpdateSettingsHandler())

	return r
}

func registerE2EServerConfigRoutes(api *gin.RouterGroup, base string, servers *profile.ServerManager) {
	api.GET(base, dashboard.NewListServersHandler())
	api.POST(base, dashboard.NewCreateServerHandler(servers))
	api.GET(base+"/:id", dashboard.NewGetServerHandler())
	api.PUT(base+"/:id", dashboard.NewUpdateServerHandler(servers))
	api.DELETE(base+"/:id", dashboard.NewDeleteServerHandler(servers))
}

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

func doJSON(t *testing.T, r *gin.Engine, method, path string, body interface{}) (int, map[string]interface{}) {
	t.Helper()
	var reqBody io.Reader
	if body != nil {
		b, _ := json.Marshal(body)
		reqBody = bytes.NewReader(b)
	}
	req, _ := http.NewRequest(method, path, reqBody)
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)
	var resp map[string]interface{}
	json.Unmarshal(w.Body.Bytes(), &resp)
	return w.Code, resp
}

func doJSONArray(t *testing.T, r *gin.Engine, method, path string) (int, []map[string]interface{}) {
	t.Helper()
	req, _ := http.NewRequest(method, path, nil)
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)
	var raw interface{}
	json.Unmarshal(w.Body.Bytes(), &raw)
	switch v := raw.(type) {
	case []interface{}:
		result := make([]map[string]interface{}, 0, len(v))
		for _, item := range v {
			if m, ok := item.(map[string]interface{}); ok {
				result = append(result, m)
			}
		}
		return w.Code, result
	case map[string]interface{}:
		if data, ok := v["data"].([]interface{}); ok {
			result := make([]map[string]interface{}, 0, len(data))
			for _, item := range data {
				if m, ok := item.(map[string]interface{}); ok {
					result = append(result, m)
				}
			}
			return w.Code, result
		}
	}
	return w.Code, nil
}

func extractQuotedID(output string) string {
	parts := strings.Split(output, "\"")
	if len(parts) >= 2 {
		return parts[1]
	}
	return ""
}

func seedSyncedAgent(t *testing.T, id, name string) {
	t.Helper()
	mgr := profile.Default()
	channel := profile.AgentTeamServer{
		ID:      "agentteam-channel-" + id,
		Enabled: true,
		Name:    "AgentTeam Server",
	}
	if err := mgr.Servers().Add(&channel); err != nil {
		t.Fatalf("seed channel: %v", err)
	}
	if err := mgr.Agents().Add(agent.Config{
		ID:      id,
		Name:    name,
		Runtime: "opencode",
		Source:  agent.AgentSource{Type: "agentteam", ID: channel.ID},
	}); err != nil {
		t.Fatalf("seed agent: %v", err)
	}
}

func seedSyncedAgentFiles(t *testing.T, cfgDir, id, name string) {
	t.Helper()
	channelID := "agentteam-channel-" + id
	channelsYAML := fmt.Sprintf(`channels:
  - id: %s
    enabled: true
    name: AgentTeam Server
`, channelID)
	agentsYAML := fmt.Sprintf(`agents:
  - id: %s
    name: %s
    runtime: opencode
    source:
      type: agentteam
      id: %s
`, id, name, channelID)
	if err := os.MkdirAll(cfgDir, 0o755); err != nil {
		t.Fatalf("create config dir: %v", err)
	}
	if err := os.WriteFile(filepath.Join(cfgDir, "channels.yaml"), []byte(channelsYAML), 0o644); err != nil {
		t.Fatalf("write channels.yaml: %v", err)
	}
	if err := os.WriteFile(filepath.Join(cfgDir, "agents.yaml"), []byte(agentsYAML), 0o644); err != nil {
		t.Fatalf("write agents.yaml: %v", err)
	}
}

// ---------------------------------------------------------------------------
// Agent API e2e
// ---------------------------------------------------------------------------

func TestAPI_AgentReadOnly(t *testing.T) {
	router := setupAPI(t)

	code, listResp := doJSONArray(t, router, "GET", "/api/internal/agents")
	if code != 200 {
		t.Fatalf("list empty: expected 200, got %d", code)
	}
	if len(listResp) != 0 {
		t.Fatalf("list empty: expected 0 agents, got %d", len(listResp))
	}

	seedSyncedAgent(t, "test-agent", "Test Agent")

	code, resp := doJSON(t, router, "GET", "/api/internal/agents/test-agent", nil)
	if code != 200 {
		t.Fatalf("get: expected 200, got %d", code)
	}
	if resp["name"] != "Test Agent" {
		t.Fatalf("expected name 'Test Agent', got %v", resp["name"])
	}
	if resp["sourceType"] != "agentteam" {
		t.Fatalf("expected sourceType 'agentteam', got %v", resp["sourceType"])
	}

	for _, ep := range []struct {
		method string
		path   string
	}{
		{"POST", "/api/internal/agents"},
		{"PUT", "/api/internal/agents/test-agent"},
		{"DELETE", "/api/internal/agents/test-agent"},
	} {
		code, _ = doJSON(t, router, ep.method, ep.path, map[string]interface{}{})
		if code != 404 {
			t.Fatalf("%s %s: expected 404, got %d", ep.method, ep.path, code)
		}
	}
}

func TestAPI_AgentNotFound(t *testing.T) {
	router := setupAPI(t)

	code, _ := doJSON(t, router, "GET", "/api/internal/agents/nonexistent", nil)
	if code != 404 {
		t.Fatalf("get nonexistent: expected 404, got %d", code)
	}
}

// ---------------------------------------------------------------------------
// Server API e2e
// ---------------------------------------------------------------------------

func TestAPI_ServerCRUD(t *testing.T) {
	router := setupAPI(t)

	code, _ := doJSON(t, router, "GET", "/api/internal/servers", nil)
	if code != 200 {
		t.Fatalf("list: expected 200, got %d", code)
	}

	chBody := map[string]interface{}{
		"type": "agentteam",
		"name": "Test AgentTeam",
		"config": map[string]interface{}{
			"server_url": "http://localhost:8080",
			"api_key":    "test-key",
		},
	}
	code, resp := doJSON(t, router, "POST", "/api/internal/servers", chBody)
	if code != 201 {
		t.Fatalf("create: expected 201, got %d: %v", code, resp)
	}
	if resp["name"] != "Test AgentTeam" {
		t.Fatalf("expected name 'Test AgentTeam', got %v", resp["name"])
	}

	chID, _ := resp["id"].(string)

	code, _ = doJSON(t, router, "GET", "/api/internal/servers/"+chID, nil)
	if code != 200 {
		t.Fatalf("get: expected 200, got %d", code)
	}

	code, resp = doJSON(t, router, "DELETE", "/api/internal/servers/"+chID, nil)
	if code != 200 {
		t.Fatalf("delete: expected 200, got %d: %v", code, resp)
	}

	code, _ = doJSON(t, router, "GET", "/api/internal/servers/"+chID, nil)
	if code != 404 {
		t.Fatalf("get after delete: expected 404, got %d", code)
	}
}

// ---------------------------------------------------------------------------
// Settings API e2e
// ---------------------------------------------------------------------------

func TestAPI_SettingsGet(t *testing.T) {
	router := setupAPI(t)

	code, resp := doJSON(t, router, "GET", "/api/internal/settings", nil)
	if code != 200 {
		t.Fatalf("get settings: expected 200, got %d: %v", code, resp)
	}
	if resp["host"] == nil {
		t.Fatal("expected host field in settings")
	}
	if resp["port"] == nil {
		t.Fatal("expected port field in settings")
	}
}

func TestAPI_SettingsUpdate(t *testing.T) {
	router := setupAPI(t)

	code, resp := doJSON(t, router, "PUT", "/api/internal/settings", map[string]interface{}{
		"language": "en",
		"port":     9090,
	})
	if code != 200 {
		t.Fatalf("update: expected 200, got %d: %v", code, resp)
	}
	if resp["language"] != "en" {
		t.Fatalf("expected language 'en', got %v", resp["language"])
	}

	code, resp = doJSON(t, router, "GET", "/api/internal/settings", nil)
	if code != 200 {
		t.Fatalf("get after update: expected 200, got %d", code)
	}
	if resp["language"] != "en" {
		t.Fatalf("expected language 'en' after reload, got %v", resp["language"])
	}
	if resp["port"].(float64) != 9090 {
		t.Fatalf("expected port 9090, got %v", resp["port"])
	}
}

// ---------------------------------------------------------------------------
// CLI e2e: Agent commands
// ---------------------------------------------------------------------------

func TestCLI_AgentReadOnly(t *testing.T) {
	bin := buildAgentTeam(t)
	cfgDir := t.TempDir()
	env := failpointEnv(cfgDir)

	run := func(args ...string) (string, int) {
		cmd := exec.Command(bin, args...)
		cmd.Env = env
		out, err := cmd.CombinedOutput()
		return string(out), func() int {
			if err != nil {
				return 1
			}
			return 0
		}()
	}

	out, rc := run("agent", "list")
	if rc != 0 {
		t.Fatalf("agent list: %s", out)
	}

	out, rc = run("agent", "get", "missing-agent")
	if rc == 0 {
		t.Fatalf("agent get missing should fail: %s", out)
	}

	for _, args := range [][]string{
		{"agent", "add", "test-agent", "--runtime", "opencode"},
		{"agent", "update", "test-agent", "--name", "Updated"},
		{"agent", "delete", "test-agent"},
		{"agent", "rm", "test-agent"},
	} {
		out, rc = run(args...)
		if rc == 0 {
			t.Fatalf("agentteam %v: expected read-only command failure, got success: %s", args, out)
		}
	}

	seedSyncedAgentFiles(t, cfgDir, "test-agent", "Test Agent")

	out, rc = run("agent", "get", "test-agent")
	if rc != 0 {
		t.Fatalf("agent get seeded: %s", out)
	}
	if !strings.Contains(out, "Test Agent") {
		t.Fatalf("expected 'Test Agent' in output: %s", out)
	}

	out, rc = run("agent", "list")
	if rc != 0 {
		t.Fatalf("agent list seeded: %s", out)
	}
	if !strings.Contains(out, "test-agent") {
		t.Fatalf("list should contain 'test-agent': %s", out)
	}
}

// ---------------------------------------------------------------------------
// CLI e2e: Channel commands
// ---------------------------------------------------------------------------

func TestCLI_ChannelCRUD(t *testing.T) {
	bin := buildAgentTeam(t)
	cfgDir := t.TempDir()
	env := failpointEnv(cfgDir)

	run := func(args ...string) (string, int) {
		cmd := exec.Command(bin, args...)
		cmd.Env = env
		out, err := cmd.CombinedOutput()
		return string(out), func() int {
			if err != nil {
				return 1
			}
			return 0
		}()
	}

	out, rc := run("channel", "list")
	if rc != 0 {
		t.Fatalf("channel list: %s", out)
	}

	out, rc = run("channel", "add", "--agentteam.server-url", "http://localhost:8080", "--agentteam.api-key", "key123")
	if rc != 0 {
		t.Fatalf("channel add: %s", out)
	}
	firstChannelID := extractQuotedID(out)
	if firstChannelID == "" {
		t.Fatalf("expected channel ID in add output: %s", out)
	}

	out, rc = run("channel", "list")
	if rc != 0 {
		t.Fatalf("channel list after add: %s", out)
	}
	if !strings.Contains(out, firstChannelID) {
		t.Fatalf("list should contain %q: %s", firstChannelID, out)
	}

	out, rc = run("channel", "add", "--agentteam.server-url", "http://localhost:8080", "--agentteam.api-key", "key456")
	if rc != 0 {
		t.Fatalf("second agentteam channel add: %s", out)
	}
	secondChannelID := extractQuotedID(out)
	if secondChannelID == "" {
		t.Fatalf("expected second channel ID in add output: %s", out)
	}
	if secondChannelID == firstChannelID {
		t.Fatalf("expected unique agentteam channel IDs, got %q", secondChannelID)
	}

	out, rc = run("channel", "list")
	if rc != 0 {
		t.Fatalf("channel list after second add: %s", out)
	}
	if !strings.Contains(out, firstChannelID) || !strings.Contains(out, secondChannelID) {
		t.Fatalf("list should contain both agentteam channel IDs: %s", out)
	}

	out, rc = run("channel", "delete", firstChannelID)
	if rc != 0 {
		t.Fatalf("channel delete: %s", out)
	}

	out, rc = run("channel", "delete", secondChannelID)
	if rc != 0 {
		t.Fatalf("second channel delete: %s", out)
	}
}

// ---------------------------------------------------------------------------
// CLI e2e: JSON output formats
// ---------------------------------------------------------------------------

func TestCLI_OutputFormats(t *testing.T) {
	bin := buildAgentTeam(t)
	cfgDir := t.TempDir()
	env := failpointEnv(cfgDir)

	run := func(args ...string) (string, int) {
		cmd := exec.Command(bin, args...)
		cmd.Env = env
		out, err := cmd.CombinedOutput()
		return string(out), func() int {
			if err != nil {
				return 1
			}
			return 0
		}()
	}

	seedSyncedAgentFiles(t, cfgDir, "fmt-agent", "Fmt")

	formats := []string{"json", "jsonc", "yaml", "toml"}
	for _, f := range formats {
		t.Run(fmt.Sprintf("agent list -f %s", f), func(t *testing.T) {
			out, rc := run("agent", "list", "-f", f)
			if rc != 0 {
				t.Fatalf("agent list -f %s: rc=%d: %s", f, rc, out)
			}
			if strings.TrimSpace(out) == "" {
				t.Fatalf("empty output for format %s", f)
			}
		})
	}
}

// ---------------------------------------------------------------------------
// Profile persistence e2e
// ---------------------------------------------------------------------------

func TestAPI_AgentPersistence(t *testing.T) {
	router := setupAPI(t)

	seedSyncedAgent(t, "persist-agent", "Persist Agent")

	code, resp := doJSON(t, router, "GET", "/api/internal/agents/persist-agent", nil)
	if code != 200 {
		t.Fatalf("get: expected 200, got %d", code)
	}
	if resp["name"] != "Persist Agent" {
		t.Fatalf("expected name 'Persist Agent', got %v", resp["name"])
	}
}

// ---------------------------------------------------------------------------
// Cross-entity e2e: Delete nonexistent
// ---------------------------------------------------------------------------

func TestAPI_DeleteNonexistent(t *testing.T) {
	router := setupAPI(t)

	entities := []string{"channels", "skills"}
	for _, e := range entities {
		t.Run(e, func(t *testing.T) {
			code, _ := doJSON(t, router, "DELETE", "/api/internal/"+e+"/nonexistent", nil)
			if code != 404 {
				t.Fatalf("DELETE %s/nonexistent: expected 404, got %d", e, code)
			}
		})
	}
}

func TestAPI_GetNonexistent(t *testing.T) {
	router := setupAPI(t)

	entities := []string{"agents", "servers", "skills"}
	for _, e := range entities {
		t.Run(e, func(t *testing.T) {
			code, _ := doJSON(t, router, "GET", "/api/internal/"+e+"/nonexistent", nil)
			if code != 404 {
				t.Fatalf("GET %s/nonexistent: expected 404, got %d", e, code)
			}
		})
	}
}

// ---------------------------------------------------------------------------
// List empty collections
// ---------------------------------------------------------------------------

func TestAPI_EmptyLists(t *testing.T) {
	router := setupAPI(t)

	endpoints := []string{
		"/api/internal/agents",
		"/api/internal/servers",
		"/api/internal/skills",
	}
	for _, ep := range endpoints {
		t.Run(ep, func(t *testing.T) {
			code, resp := doJSON(t, router, "GET", ep, nil)
			if code != 200 {
				t.Fatalf("GET %s: expected 200, got %d", ep, code)
			}
			arr, ok := resp["data"].([]interface{})
			if ok {
				if len(arr) != 0 {
					t.Fatalf("GET %s: expected empty array, got %d items", ep, len(arr))
				}
			}
		})
	}
}

// ---------------------------------------------------------------------------
// Invalid request body
// ---------------------------------------------------------------------------

func TestAPI_InvalidBody(t *testing.T) {
	router := setupAPI(t)

	endpoints := []struct {
		method string
		path   string
		want   int
	}{
		{"POST", "/api/internal/servers", 400},
		{"PUT", "/api/internal/servers/test", 400},
	}
	for _, ep := range endpoints {
		t.Run(ep.method+" "+ep.path, func(t *testing.T) {
			req, _ := http.NewRequest(ep.method, ep.path, strings.NewReader("invalid json{{{"))
			req.Header.Set("Content-Type", "application/json")
			w := httptest.NewRecorder()
			router.ServeHTTP(w, req)
			if w.Code != ep.want {
				t.Fatalf("%s %s: expected %d, got %d", ep.method, ep.path, ep.want, w.Code)
			}
		})
	}
}
