package dashboard

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"sync"
	"testing"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/require"
	"go.yaml.in/yaml/v3"

	"github.com/tidbcloud/AgentTeam/desktop/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/desktop/internal/profile"
	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
	wiremanager "github.com/tidbcloud/AgentTeam/desktop/internal/wire/manager"
)

type dashboardTestServer struct {
	id        string
	enabled   bool
	started   bool
	signature string
	identity  profile.AgentTeamServer
	recorder  *dashboardTestServerRecorder
	startErr  error
}

func (c *dashboardTestServer) ID() string { return c.id }

func (c *dashboardTestServer) Enabled() bool { return c.enabled }

func (c *dashboardTestServer) Start(context.Context) error {
	if !c.enabled {
		return nil
	}
	c.started = true
	if c.recorder != nil {
		c.recorder.recordStart(c.signature)
	}
	return c.startErr
}

func (c *dashboardTestServer) Stop(context.Context) error {
	if c.recorder != nil {
		c.recorder.recordStop()
	}
	return nil
}

func (c *dashboardTestServer) Send(context.Context, types.Metadata, types.OutboundMessage) error {
	return nil
}

func (c *dashboardTestServer) ServerIdentitySnapshot() profile.AgentTeamServer {
	return c.identity
}

type dashboardTestServerRecorder struct {
	mu         sync.Mutex
	starts     int
	stops      int
	signatures []string
}

var dashboardTestServerRecorders sync.Map

func (r *dashboardTestServerRecorder) recordStart(signature string) {
	r.mu.Lock()
	defer r.mu.Unlock()
	r.starts++
	r.signatures = append(r.signatures, signature)
}

func (r *dashboardTestServerRecorder) recordStop() {
	r.mu.Lock()
	defer r.mu.Unlock()
	r.stops++
}

func (r *dashboardTestServerRecorder) snapshot() (starts int, stops int, signatures []string) {
	r.mu.Lock()
	defer r.mu.Unlock()
	return r.starts, r.stops, append([]string(nil), r.signatures...)
}

func dashboardTestRecorderFor(id string) *dashboardTestServerRecorder {
	recorder, _ := dashboardTestServerRecorders.LoadOrStore(id, &dashboardTestServerRecorder{})
	return recorder.(*dashboardTestServerRecorder)
}

func newDashboardTestServer(serverCfg profile.AgentTeamServer, _ types.ServerConnectionContext) (types.ServerConnection, error) {
	return &dashboardTestServer{
		id:        serverCfg.ID,
		enabled:   serverCfg.Enabled,
		signature: dashboardTestSignature(serverCfg),
		identity:  serverCfg,
		recorder:  dashboardTestRecorderFor(serverCfg.ID),
	}, nil
}

func dashboardTestSignature(server profile.AgentTeamServer) string {
	return server.AgentID + "|" + string(server.Config)
}

func seedDashboardTestServers(t testing.TB, dir string, servers ...profile.AgentTeamServer) {
	t.Helper()

	data, err := yaml.Marshal(struct {
		Servers []profile.AgentTeamServer `yaml:"servers"`
	}{Servers: servers})
	require.NoError(t, err)
	require.NoError(t, os.WriteFile(filepath.Join(dir, "servers.yaml"), data, 0644))
}

func TestServerRequestToProfile(t *testing.T) {
	setupDashboardTestProfile(t)

	t.Run("basic conversion", func(t *testing.T) {
		req := dto.ServerRequest{
			ID:      "ch-123",
			Enabled: true,
			Name:    "My AgentTeam",
			AgentID: "agent-1",
			Config:  map[string]interface{}{"server_url": "https://agentteam.example.com", "api_key": "secret"},
		}
		ch := serverRequestToProfile(req)

		if ch.ID != "ch-123" {
			t.Errorf("ID = %q, want %q", ch.ID, "ch-123")
		}
		if !ch.Enabled {
			t.Error("Enabled should be true")
		}
		if ch.Name != "My AgentTeam" {
			t.Errorf("Name = %q, want %q", ch.Name, "My AgentTeam")
		}
		if ch.AgentID != "agent-1" {
			t.Errorf("AgentID = %q, want %q", ch.AgentID, "agent-1")
		}
		if len(ch.Config) == 0 {
			t.Fatal("Config should not be empty")
		}
	})

	t.Run("empty config produces nil config", func(t *testing.T) {
		req := dto.ServerRequest{ID: "ch-456"}
		ch := serverRequestToProfile(req)
		if len(ch.Config) != 0 {
			t.Errorf("Config should be empty, got %q", string(ch.Config))
		}
	})

	t.Run("agent_id round-trips through response", func(t *testing.T) {
		req := dto.ServerRequest{
			ID:      "ch-789",
			Enabled: true,
			AgentID: "dev-agent",
		}
		ch := serverRequestToProfile(req)
		resp := toServerResponse(ch)
		if resp.AgentID != "dev-agent" {
			t.Errorf("AgentID = %q, want %q", resp.AgentID, "dev-agent")
		}
	})
}

func TestUpdateServerHandler_EnablingServerStartsUpdatedConfig(t *testing.T) {
	dir := t.TempDir()
	serverConfig := profile.AgentTeamServer{
		ID:        "chn-enable-test",
		Enabled:   false,
		ServerURL: "https://enable.example.test",
		APIKey:    "key-enable",
	}
	seedDashboardTestServers(t, dir, serverConfig)

	t.Setenv(profile.ConfigDirEnvKey, dir)
	gin.SetMode(gin.TestMode)

	router := gin.New()
	chMgr := wiremanager.New(nil, newDashboardTestServer)
	activateDashboardTestRuntime(profile.Default().Servers(), chMgr)
	router.PUT("/servers/:id", NewUpdateServerHandler(profile.Default().Servers()))

	body, err := json.Marshal(dto.ServerRequest{Enabled: true})
	require.NoError(t, err)

	req := httptest.NewRequest(http.MethodPut, "/servers/"+serverConfig.ID, bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	recorder := httptest.NewRecorder()
	router.ServeHTTP(recorder, req)

	require.Equal(t, http.StatusOK, recorder.Code, recorder.Body.String())

	updated, ok := profile.Default().Servers().Get(serverConfig.ID)
	require.True(t, ok)
	require.True(t, updated.Enabled)

	running, ok := chMgr.ServerConnection(serverConfig.ID)
	require.True(t, ok)

	testServer, ok := running.(*dashboardTestServer)
	require.True(t, ok)
	require.True(t, testServer.Enabled())
	require.True(t, testServer.started)
}

func TestUpdateServerHandler_ConfigOverridesPersistedServerAuthFields(t *testing.T) {
	dir := t.TempDir()
	serverConfig := profile.AgentTeamServer{
		ID:          "chn-auth-update-test",
		Enabled:     true,
		ServerURL:   "https://old.example.test",
		WorkspaceID: "wsp_old",
		MachineID:   "mac_old",
		APIKey:      "key-old",
	}
	seedDashboardTestServers(t, dir, serverConfig)

	t.Setenv(profile.ConfigDirEnvKey, dir)
	gin.SetMode(gin.TestMode)

	router := gin.New()
	chMgr := wiremanager.New(nil, newDashboardTestServer)
	activateDashboardTestRuntime(profile.Default().Servers(), chMgr)
	router.PUT("/servers/:id", NewUpdateServerHandler(profile.Default().Servers()))

	body, err := json.Marshal(dto.ServerRequest{
		Enabled: true,
		Config: map[string]interface{}{
			"server_url":   "https://new.example.test",
			"workspace_id": "wsp_new",
			"machine_id":   "mac_new",
			"api_key":      "key-new",
		},
	})
	require.NoError(t, err)

	req := httptest.NewRequest(http.MethodPut, "/servers/"+serverConfig.ID, bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	recorder := httptest.NewRecorder()
	router.ServeHTTP(recorder, req)

	require.Equal(t, http.StatusOK, recorder.Code, recorder.Body.String())

	updated, ok := profile.Default().Servers().Get(serverConfig.ID)
	require.True(t, ok)
	require.Equal(t, "https://new.example.test", updated.ServerURL)
	require.Equal(t, "wsp_new", updated.WorkspaceID)
	require.Equal(t, "mac_new", updated.MachineID)
	require.Equal(t, "key-new", updated.APIKey)

	running, ok := chMgr.ServerConnection(serverConfig.ID)
	require.True(t, ok)
	testServer, ok := running.(*dashboardTestServer)
	require.True(t, ok)
	require.Contains(t, testServer.signature, "https://new.example.test")
	require.Contains(t, testServer.signature, "wsp_new")
	require.Contains(t, testServer.signature, "mac_new")
	require.Contains(t, testServer.signature, "key-new")
	require.NotContains(t, testServer.signature, "https://old.example.test")
	require.NotContains(t, testServer.signature, "key-old")
}

func TestUpdateServerHandler_ClearsDescription(t *testing.T) {
	dir := t.TempDir()
	seed := profile.New(dir)
	serverConfig := profile.AgentTeamServer{
		ID:          "chn-desc-test",
		Enabled:     true,
		Name:        "AgentTeam",
		Description: "old description",
		Config:      []byte("server_url: https://agentteam.example.test\n"),
	}
	require.NoError(t, seed.Servers().Add(&serverConfig))

	t.Setenv(profile.ConfigDirEnvKey, dir)
	gin.SetMode(gin.TestMode)

	router := gin.New()
	chMgr := wiremanager.New(nil, newDashboardTestServer)
	activateDashboardTestRuntime(profile.Default().Servers(), chMgr)
	router.PUT("/servers/:id", NewUpdateServerHandler(profile.Default().Servers()))

	empty := ""
	body, err := json.Marshal(dto.ServerRequest{Description: &empty, Enabled: true})
	require.NoError(t, err)

	req := httptest.NewRequest(http.MethodPut, "/servers/"+serverConfig.ID, bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	recorder := httptest.NewRecorder()
	router.ServeHTTP(recorder, req)

	require.Equal(t, http.StatusOK, recorder.Code, recorder.Body.String())
	updated, ok := profile.Default().Servers().Get(serverConfig.ID)
	require.True(t, ok)
	require.Empty(t, updated.Description)
}

func TestUpdateServerHandler_RuntimeChangeRestartsRunningServer(t *testing.T) {
	dir := t.TempDir()
	serverConfig := profile.AgentTeamServer{
		ID:        "chn-restart-test",
		Enabled:   true,
		AgentID:   "agent-a",
		ServerURL: "https://alpha.example.test",
		APIKey:    "key-alpha",
	}
	seedDashboardTestServers(t, dir, serverConfig)

	t.Setenv(profile.ConfigDirEnvKey, dir)
	gin.SetMode(gin.TestMode)

	router := gin.New()
	chMgr := wiremanager.New(nil, newDashboardTestServer)
	activateDashboardTestRuntime(profile.Default().Servers(), chMgr)
	require.NoError(t, chMgr.Enable(serverConfig.ID))
	initialConfig, ok := profile.Default().Servers().Get(serverConfig.ID)
	require.True(t, ok)
	router.PUT("/servers/:id", NewUpdateServerHandler(profile.Default().Servers()))

	body, err := json.Marshal(dto.ServerRequest{
		Enabled: true,
		Config: map[string]interface{}{
			"api_key":    "key-alpha",
			"server_url": "https://beta.example.test",
		},
	})
	require.NoError(t, err)

	req := httptest.NewRequest(http.MethodPut, "/servers/"+serverConfig.ID, bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	recorder := httptest.NewRecorder()
	router.ServeHTTP(recorder, req)

	require.Equal(t, http.StatusOK, recorder.Code, recorder.Body.String())

	updatedConfig, ok := profile.Default().Servers().Get(serverConfig.ID)
	require.True(t, ok)
	runtimeRecorder := dashboardTestRecorderFor(serverConfig.ID)
	starts, stops, signatures := runtimeRecorder.snapshot()
	require.Equal(t, 2, starts)
	require.Equal(t, 1, stops)
	require.Equal(t, []string{
		dashboardTestSignature(initialConfig),
		dashboardTestSignature(updatedConfig),
	}, signatures)
}

func TestServerGatewayIdentityChanged(t *testing.T) {
	base := profile.AgentTeamServer{
		ID:          "server-a",
		Enabled:     true,
		AgentID:     "agent-a",
		ServerURL:   "https://agentteam.example.test",
		WorkspaceID: "wsp-a",
		MachineID:   "mac-a",
		APIKey:      "machine-key",
		AuthToken:   "user-token",
	}
	tests := []struct {
		name    string
		mutate  func(*profile.AgentTeamServer)
		changed bool
	}{
		{name: "unchanged", mutate: func(*profile.AgentTeamServer) {}},
		{name: "display name only", mutate: func(server *profile.AgentTeamServer) { server.Name = "Renamed" }},
		{name: "description only", mutate: func(server *profile.AgentTeamServer) { server.Description = "Updated" }},
		{name: "enabled only", mutate: func(server *profile.AgentTeamServer) { server.Enabled = false }},
		{name: "server URL", mutate: func(server *profile.AgentTeamServer) { server.ServerURL = "https://new.example.test" }, changed: true},
		{name: "workspace", mutate: func(server *profile.AgentTeamServer) { server.WorkspaceID = "wsp-b" }, changed: true},
		{name: "machine", mutate: func(server *profile.AgentTeamServer) { server.MachineID = "mac-b" }, changed: true},
		{name: "machine API key", mutate: func(server *profile.AgentTeamServer) { server.APIKey = "new-key" }, changed: true},
		{name: "user token", mutate: func(server *profile.AgentTeamServer) { server.AuthToken = "new-token" }, changed: true},
		{name: "agent", mutate: func(server *profile.AgentTeamServer) { server.AgentID = "agent-b" }, changed: true},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			current := base
			test.mutate(&current)
			require.Equal(t, test.changed, profile.ServerGatewayIdentityChanged(base, current))
		})
	}
}

func TestDeleteServerHandler_StopsRunningServerBeforeRemovingConfig(t *testing.T) {
	dir := t.TempDir()
	serverConfig := profile.AgentTeamServer{
		ID:        "chn-delete-test",
		Enabled:   true,
		AgentID:   "agent-a",
		ServerURL: "https://delete.example.test",
		APIKey:    "key-delete",
	}
	seedDashboardTestServers(t, dir, serverConfig)

	t.Setenv(profile.ConfigDirEnvKey, dir)
	gin.SetMode(gin.TestMode)

	router := gin.New()
	chMgr := wiremanager.New(nil, newDashboardTestServer)
	activateDashboardTestRuntime(profile.Default().Servers(), chMgr)
	require.NoError(t, chMgr.Enable(serverConfig.ID))
	initialConfig, ok := profile.Default().Servers().Get(serverConfig.ID)
	require.True(t, ok)
	router.DELETE("/servers/:id", NewDeleteServerHandler(profile.Default().Servers()))

	req := httptest.NewRequest(http.MethodDelete, "/servers/"+serverConfig.ID, nil)
	recorder := httptest.NewRecorder()
	router.ServeHTTP(recorder, req)

	require.Equal(t, http.StatusOK, recorder.Code, recorder.Body.String())

	_, ok = profile.Default().Servers().Get(serverConfig.ID)
	require.False(t, ok)
	_, ok = chMgr.ServerConnection(serverConfig.ID)
	require.False(t, ok)

	runtimeRecorder := dashboardTestRecorderFor(serverConfig.ID)
	starts, stops, signatures := runtimeRecorder.snapshot()
	require.Equal(t, 1, starts)
	require.Equal(t, 1, stops)
	require.Equal(t, []string{dashboardTestSignature(initialConfig)}, signatures)
}

func TestRepairServerHandlerUsesExplicitRuntimeRepair(t *testing.T) {
	setupDashboardTestProfile(t)
	server := profile.AgentTeamServer{
		ID:          "agentteam-repair",
		Enabled:     true,
		ServerURL:   "https://agentteam.example.test",
		APIKey:      "machine-key",
		WorkspaceID: "wsp-repair",
	}
	manager := profile.Default()
	require.NoError(t, manager.Servers().Add(&server))
	starts := 0
	connections := wiremanager.New(nil, func(serverCfg profile.AgentTeamServer, _ types.ServerConnectionContext) (types.ServerConnection, error) {
		starts++
		runtime, err := newDashboardTestServer(serverCfg, nil)
		if starts <= 4 {
			runtime.(*dashboardTestServer).startErr = errors.New("injected start failure")
		}
		return runtime, err
	})
	activateDashboardTestRuntime(manager.Servers(), connections)

	config := types.RawMessage("server_url: https://changed.example.test\napi_key: machine-key\nworkspace_id: wsp-repair\n")
	_, err := manager.Servers().Patch(server.ID, profile.ServerUpdate{Config: &config})
	require.ErrorContains(t, err, "runtime rollback failed")
	require.True(t, manager.Servers().ServerRuntimeHalted(server.ID))

	gin.SetMode(gin.TestMode)
	router := gin.New()
	router.POST("/servers/:id/repair", NewRepairServerHandler(manager.Servers()))
	req := httptest.NewRequest(http.MethodPost, "/servers/"+server.ID+"/repair", nil)
	recorder := httptest.NewRecorder()
	router.ServeHTTP(recorder, req)

	require.Equal(t, http.StatusOK, recorder.Code, recorder.Body.String())
	require.False(t, manager.Servers().ServerRuntimeHalted(server.ID))
	_, ok := connections.ServerConnection(server.ID)
	require.True(t, ok)
}

func TestToServerResponse(t *testing.T) {
	setupDashboardTestProfile(t)

	t.Run("nil config produces nil config in response", func(t *testing.T) {
		ch := profile.AgentTeamServer{ID: "ch-1", Enabled: true}
		resp := toServerResponse(ch)
		if resp.Config != nil {
			t.Errorf("Config should be nil, got %v", resp.Config)
		}
	})

	t.Run("config is unmarshaled from YAML", func(t *testing.T) {
		ch := profile.AgentTeamServer{
			ID:      "ch-2",
			Enabled: true,
			Config:  []byte("server_url: https://agentteam.example.com\napi_key: secret\n"),
		}
		resp := toServerResponse(ch)
		if resp.Config == nil {
			t.Fatal("Config should not be nil")
		}
		if resp.Config["server_url"] != "https://agentteam.example.com" {
			t.Errorf("server_url = %v, want https://agentteam.example.com", resp.Config["server_url"])
		}
	})

	t.Run("agent_id is included in response", func(t *testing.T) {
		ch := profile.AgentTeamServer{
			ID:      "ch-3",
			Enabled: true,
			AgentID: "bound-agent",
		}
		resp := toServerResponse(ch)
		if resp.AgentID != "bound-agent" {
			t.Errorf("AgentID = %q, want %q", resp.AgentID, "bound-agent")
		}
	})

	t.Run("empty agent_id is omitted from response", func(t *testing.T) {
		ch := profile.AgentTeamServer{
			ID:      "ch-4",
			Enabled: true,
			AgentID: "",
		}
		resp := toServerResponse(ch)
		if resp.AgentID != "" {
			t.Errorf("AgentID = %q, want empty", resp.AgentID)
		}
	})
}
