package webagent_test

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/gorilla/websocket"
	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/api/daemon"
	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	. "github.com/tidbcloud/AgentTeam/internal/api/testkit"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
)

func TestAgentsUpdateSupportsAvatarURLAndNullClearing(t *testing.T) {
	t.Parallel()

	runtime := StartRuntime(t)

	router := gin.New()
	runtime.RegisterRoutes(router)
	server := httptest.NewServer(router)
	defer server.Close()

	client := server.Client()

	var auth dto.AuthTokensResponse
	response := DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/auth/register", map[string]any{
		"email":    "admin@example.com",
		"password": TestUserPassword,
		"name":     "admin",
	}, nil, &auth)
	require.Equal(t, http.StatusOK, response.StatusCode)

	var workspaces []dto.WorkspaceResponse
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces", nil, map[string]string{
		"Authorization": fmt.Sprintf("Bearer %s", auth.AccessToken),
	}, &workspaces)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, workspaces)
	workspaceID := workspaces[0].ID

	var machineResponse dto.CreateMachineResponse
	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/machines", map[string]any{
		"name": "dev-machine",
	}, map[string]string{
		"Authorization": fmt.Sprintf("Bearer %s", auth.AccessToken),
	}, &machineResponse)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, machineResponse.Machine.ID)

	var createdAgent agent.AppAgent
	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/agents", map[string]any{
		"name":        "test-agent",
		"description": "Initial role",
		"runtime":     "codex",
		"model":       "gpt-5.4",
		"machineId":   machineResponse.Machine.ID,
	}, map[string]string{
		"Authorization":  fmt.Sprintf("Bearer %s", auth.AccessToken),
		"X-Workspace-Id": workspaceID,
	}, &createdAgent)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, createdAgent.ID)
	require.Equal(t, "Initial role", createdAgent.Description)
	require.Equal(t, "test-agent", createdAgent.Name)
	require.Equal(t, "codex", createdAgent.Runtime)
	require.Equal(t, "gpt-5.4", createdAgent.Model)

	_, err := runtime.Pool.Exec(runtime.Ctx, `UPDATE agents SET session_id = 'sess_keep' WHERE id = $1`, createdAgent.ID)
	require.NoError(t, err)

	var updatedAgent agent.AppAgent
	response = DoJSON(t, client, http.MethodPatch, server.URL+"/api/v1/workspaces/"+workspaceID+"/agents/"+createdAgent.ID, map[string]any{
		"avatarUrl":   "pixel:robot",
		"description": "Updated role",
		"displayName": "Test Agent",
	}, map[string]string{
		"Authorization":  fmt.Sprintf("Bearer %s", auth.AccessToken),
		"X-Workspace-Id": workspaceID,
	}, &updatedAgent)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Equal(t, "pixel:robot", updatedAgent.AvatarURL)
	require.Equal(t, "Updated role", updatedAgent.Description)
	require.Equal(t, "Test Agent", updatedAgent.DisplayName)

	var sessionID string
	require.NoError(t, runtime.Pool.QueryRow(runtime.Ctx, `SELECT session_id FROM agents WHERE id = $1`, createdAgent.ID).Scan(&sessionID))
	require.Equal(t, "sess_keep", sessionID)

	_, err = runtime.Pool.Exec(runtime.Ctx, `UPDATE agents SET session_id = 'sess_restart' WHERE id = $1`, createdAgent.ID)
	require.NoError(t, err)

	response = DoJSON(t, client, http.MethodPatch, server.URL+"/api/v1/workspaces/"+workspaceID+"/agents/"+createdAgent.ID, map[string]any{
		"reasoningEffort": "medium",
	}, map[string]string{
		"Authorization":  fmt.Sprintf("Bearer %s", auth.AccessToken),
		"X-Workspace-Id": workspaceID,
	}, &updatedAgent)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Equal(t, "medium", updatedAgent.ReasoningEffort)

	require.NoError(t, runtime.Pool.QueryRow(runtime.Ctx, `SELECT session_id FROM agents WHERE id = $1`, createdAgent.ID).Scan(&sessionID))
	require.Empty(t, sessionID)

	var clearedAgent agent.AppAgent
	response = DoJSON(t, client, http.MethodPatch, server.URL+"/api/v1/workspaces/"+workspaceID+"/agents/"+createdAgent.ID, map[string]any{
		"avatarUrl":   nil,
		"description": nil,
	}, map[string]string{
		"Authorization":  fmt.Sprintf("Bearer %s", auth.AccessToken),
		"X-Workspace-Id": workspaceID,
	}, &clearedAgent)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Empty(t, clearedAgent.AvatarURL)
	require.Empty(t, clearedAgent.Description)
}

func TestStartWorkspaceWriteAgentRequiresPermissionModeCapability(t *testing.T) {
	runtime := StartRuntime(t)
	hub := daemon.New(runtime.Logger, daemon.NewStore(runtime.AgentRuntime.Agents(), runtime.AgentRuntime.Recovery(), runtime.AgentRuntime.PrepareAgentStart), "http://localhost", nil)
	runtime.Hub = hub

	router := gin.New()
	runtime.RegisterRoutes(router)
	daemon.RegisterRoutes(router, hub, runtime.AgentRuntime.Agents(), nil, daemon.RegisterRoutesOptions{})
	server := httptest.NewServer(router)
	defer server.Close()

	client := server.Client()

	var auth dto.AuthTokensResponse
	response := DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/auth/register", map[string]any{
		"email":    "old-daemon-start@example.com",
		"password": TestUserPassword,
		"name":     "old-daemon-start",
	}, nil, &auth)
	require.Equal(t, http.StatusOK, response.StatusCode)

	var workspaces []dto.WorkspaceResponse
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces", nil, map[string]string{
		"Authorization": fmt.Sprintf("Bearer %s", auth.AccessToken),
	}, &workspaces)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, workspaces)
	workspaceID := workspaces[0].ID

	var machineResponse dto.CreateMachineResponse
	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/machines", map[string]any{
		"name": "old-daemon-machine",
	}, WorkspaceHeaders(auth.AccessToken, workspaceID), &machineResponse)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, machineResponse.APIKey)

	var createdAgent agent.AppAgent
	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/agents", map[string]any{
		"name":      "old-daemon-codex-agent",
		"runtime":   "codex",
		"model":     "gpt-5-codex",
		"machineId": machineResponse.Machine.ID,
	}, WorkspaceHeaders(auth.AccessToken, workspaceID), &createdAgent)
	require.Equal(t, http.StatusOK, response.StatusCode)

	wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/api/v1/workspaces/" + workspaceID + "/daemon/connect?key=" + machineResponse.APIKey
	conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
	require.NoError(t, err)
	defer conn.Close()
	requireDaemonServerCapabilities(t, conn)

	require.NoError(t, conn.WriteJSON(map[string]any{
		"type":          "ready",
		"capabilities":  []string{"workspace"},
		"runtimes":      []string{"codex"},
		"runningAgents": []string{},
		"hostname":      "test-host",
		"os":            "linux",
		"daemonVersion": "0.9.0",
	}))

	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/agents/"+createdAgent.ID+"/start", map[string]any{}, WorkspaceHeaders(auth.AccessToken, workspaceID), nil)
	require.Equal(t, http.StatusOK, response.StatusCode)

	require.NoError(t, conn.SetReadDeadline(time.Now().Add(2*time.Second)))
	_, payload, err := conn.ReadMessage()
	require.NoError(t, err)
	var legacyStart map[string]any
	require.NoError(t, json.Unmarshal(payload, &legacyStart))
	require.Equal(t, "agent:start", legacyStart["type"])
	require.Equal(t, createdAgent.ID, legacyStart["agentId"])

	_ = conn.Close()
	conn, _, err = websocket.DefaultDialer.Dial(wsURL, nil)
	require.NoError(t, err)
	defer conn.Close()
	requireDaemonServerCapabilities(t, conn)

	require.NoError(t, conn.WriteJSON(map[string]any{
		"type":          "ready",
		"capabilities":  []string{"workspace", "agent:permission-mode"},
		"runtimes":      []string{"codex"},
		"runningAgents": []string{},
		"hostname":      "test-host",
		"os":            "linux",
		"daemonVersion": "0.9.1",
	}))
	require.Eventually(t, func() bool {
		var version string
		if err := runtime.Pool.QueryRow(runtime.Ctx, `SELECT daemon_version FROM machines WHERE id = $1`, machineResponse.Machine.ID).Scan(&version); err != nil {
			return false
		}
		return version == "0.9.1"
	}, 2*time.Second, 10*time.Millisecond)

	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/agents/"+createdAgent.ID+"/start", map[string]any{}, WorkspaceHeaders(auth.AccessToken, workspaceID), nil)
	require.Equal(t, http.StatusOK, response.StatusCode)

	require.NoError(t, conn.SetReadDeadline(time.Now().Add(2*time.Second)))
	_, payload, err = conn.ReadMessage()
	require.NoError(t, err)
	var start map[string]any
	require.NoError(t, json.Unmarshal(payload, &start))
	require.Equal(t, "agent:start", start["type"])
	require.Equal(t, createdAgent.ID, start["agentId"])
}

func requireDaemonServerCapabilities(t *testing.T, conn *websocket.Conn) {
	t.Helper()

	require.NoError(t, conn.SetReadDeadline(time.Now().Add(2*time.Second)))
	_, payload, err := conn.ReadMessage()
	require.NoError(t, err)
	var capabilities struct {
		Type         string   `json:"type"`
		Capabilities []string `json:"capabilities"`
	}
	require.NoError(t, json.Unmarshal(payload, &capabilities))
	require.Equal(t, "server:capabilities", capabilities.Type)
	require.Contains(t, capabilities.Capabilities, "transient_legacy_wake_status")
}

func TestAgentsListIncludesLatestDurableRuntimeFailureSummaryWhenRequested(t *testing.T) {
	t.Parallel()

	runtime := StartRuntime(t)

	router := gin.New()
	runtime.RegisterRoutes(router)
	server := httptest.NewServer(router)
	defer server.Close()

	client := server.Client()

	var auth dto.AuthTokensResponse
	response := DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/auth/register", map[string]any{
		"email":    "admin@example.com",
		"password": TestUserPassword,
		"name":     "admin",
	}, nil, &auth)
	require.Equal(t, http.StatusOK, response.StatusCode)

	var workspaces []dto.WorkspaceResponse
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces", nil, map[string]string{
		"Authorization": fmt.Sprintf("Bearer %s", auth.AccessToken),
	}, &workspaces)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, workspaces)
	workspaceID := workspaces[0].ID

	var machineResponse dto.CreateMachineResponse
	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/machines", map[string]any{
		"name": "dev-machine",
	}, map[string]string{
		"Authorization": fmt.Sprintf("Bearer %s", auth.AccessToken),
	}, &machineResponse)
	require.Equal(t, http.StatusOK, response.StatusCode)

	var createdAgent agent.AppAgent
	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/agents", map[string]any{
		"name":      "test-agent",
		"runtime":   "codex",
		"machineId": machineResponse.Machine.ID,
	}, map[string]string{
		"Authorization":  fmt.Sprintf("Bearer %s", auth.AccessToken),
		"X-Workspace-Id": workspaceID,
	}, &createdAgent)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, createdAgent.ID)

	now := time.Date(2026, time.April, 2, 10, 0, 0, 0, time.UTC)
	latestFailureAt := now.Add(2 * time.Minute)
	err := runtime.Store.AgentOps().RecordAgentRuntimeEvent(runtime.Ctx, agent.AgentRuntimeEvent{
		WorkspaceID: workspaceID,
		AgentID:     createdAgent.ID,
		MachineID:   machineResponse.Machine.ID,
		MessageID:   "msg-start-fail",
		EventClass:  agent.AgentRuntimeEventClassRuntimeStartFailed,
		Detail:      "boot timeout",
		OccurredAt:  now.Add(time.Minute),
	})
	require.NoError(t, err)
	err = runtime.Store.AgentOps().RecordAgentRuntimeEvent(runtime.Ctx, agent.AgentRuntimeEvent{
		WorkspaceID: workspaceID,
		AgentID:     createdAgent.ID,
		MachineID:   machineResponse.Machine.ID,
		MessageID:   "msg-wake-fail",
		EventClass:  agent.AgentRuntimeEventClassWakeFailed,
		Target:      "#all",
		Detail:      "deferred continuation crashed",
		OccurredAt:  latestFailureAt,
	})
	require.NoError(t, err)
	err = runtime.Store.AgentOps().RecordAgentRuntimeEvent(runtime.Ctx, agent.AgentRuntimeEvent{
		WorkspaceID:    workspaceID,
		AgentID:        createdAgent.ID,
		MachineID:      machineResponse.Machine.ID,
		MessageID:      "msg-success",
		WakeToken:      "wake-success",
		EventClass:     agent.AgentRuntimeEventClassVisibleReplySent,
		Target:         "#all",
		ReplyMessageID: "msg-reply-1",
		OccurredAt:     latestFailureAt.Add(time.Minute),
	})
	require.NoError(t, err)

	var agents []map[string]any
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces/"+workspaceID+"/agents?includeRuntimeFailures=true", nil, map[string]string{
		"Authorization":  fmt.Sprintf("Bearer %s", auth.AccessToken),
		"X-Workspace-Id": workspaceID,
	}, &agents)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Len(t, agents, 1)

	failure, ok := agents[0]["latestRuntimeFailure"].(map[string]any)
	require.True(t, ok, "expected latestRuntimeFailure in %#v", agents[0])
	require.Equal(t, "wake_failed", failure["eventClass"])
	require.Equal(t, "msg-wake-fail", failure["messageId"])
	require.Equal(t, "Wake handling failed after the daemon accepted the message.", failure["summary"])
	require.Equal(t, "deferred continuation crashed", failure["detail"])
	occurredAt, ok := failure["occurredAt"].(string)
	require.True(t, ok)
	parsedOccurredAt, err := time.Parse(time.RFC3339Nano, occurredAt)
	require.NoError(t, err)
	require.True(t, parsedOccurredAt.UTC().Equal(latestFailureAt.UTC()))
}

func TestAgentsResetClearsSessionWithoutChangingAutoWakeAndFullResetRequiresMachineOnline(t *testing.T) {
	t.Parallel()

	runtime := StartRuntime(t)

	router := gin.New()
	runtime.RegisterRoutes(router)
	server := httptest.NewServer(router)
	defer server.Close()

	client := server.Client()

	var auth dto.AuthTokensResponse
	response := DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/auth/register", map[string]any{
		"email":    "admin@example.com",
		"password": TestUserPassword,
		"name":     "admin",
	}, nil, &auth)
	require.Equal(t, http.StatusOK, response.StatusCode)

	var workspaces []dto.WorkspaceResponse
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces", nil, map[string]string{
		"Authorization": fmt.Sprintf("Bearer %s", auth.AccessToken),
	}, &workspaces)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, workspaces)
	workspaceID := workspaces[0].ID

	var machineResponse dto.CreateMachineResponse
	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/machines", map[string]any{
		"name": "dev-machine",
	}, map[string]string{
		"Authorization": fmt.Sprintf("Bearer %s", auth.AccessToken),
	}, &machineResponse)
	require.Equal(t, http.StatusOK, response.StatusCode)

	var createdAgent agent.AppAgent
	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/agents", map[string]any{
		"name":      "test-agent",
		"runtime":   "claude",
		"model":     "sonnet",
		"machineId": machineResponse.Machine.ID,
	}, map[string]string{
		"Authorization":  fmt.Sprintf("Bearer %s", auth.AccessToken),
		"X-Workspace-Id": workspaceID,
	}, &createdAgent)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, createdAgent.ID)

	_, err := runtime.Pool.Exec(runtime.Ctx, `UPDATE agents SET session_id = 'sess_test' WHERE id = $1`, createdAgent.ID)
	require.NoError(t, err)

	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/agents/"+createdAgent.ID+"/reset", map[string]any{
		"clearWorkspace": false,
	}, map[string]string{
		"Authorization":  fmt.Sprintf("Bearer %s", auth.AccessToken),
		"X-Workspace-Id": workspaceID,
	}, nil)
	require.Equal(t, http.StatusOK, response.StatusCode)

	var sessionID string
	var autoWakeEnabled bool
	require.NoError(t, runtime.Pool.QueryRow(runtime.Ctx, `SELECT session_id, auto_wake_enabled FROM agents WHERE id = $1`, createdAgent.ID).Scan(&sessionID, &autoWakeEnabled))
	require.Empty(t, sessionID)
	require.True(t, autoWakeEnabled)

	_, err = runtime.Pool.Exec(runtime.Ctx, `UPDATE agents SET session_id = 'sess_test_2', auto_wake_enabled = FALSE WHERE id = $1`, createdAgent.ID)
	require.NoError(t, err)

	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/agents/"+createdAgent.ID+"/reset", map[string]any{
		"clearWorkspace": false,
	}, map[string]string{
		"Authorization":  fmt.Sprintf("Bearer %s", auth.AccessToken),
		"X-Workspace-Id": workspaceID,
	}, nil)
	require.Equal(t, http.StatusOK, response.StatusCode)

	require.NoError(t, runtime.Pool.QueryRow(runtime.Ctx, `SELECT session_id, auto_wake_enabled FROM agents WHERE id = $1`, createdAgent.ID).Scan(&sessionID, &autoWakeEnabled))
	require.Empty(t, sessionID)
	require.False(t, autoWakeEnabled)

	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/agents/"+createdAgent.ID+"/reset", map[string]any{
		"clearWorkspace": true,
	}, map[string]string{
		"Authorization":  fmt.Sprintf("Bearer %s", auth.AccessToken),
		"X-Workspace-Id": workspaceID,
	}, nil)
	require.Equal(t, http.StatusServiceUnavailable, response.StatusCode)

	require.NoError(t, runtime.Pool.QueryRow(runtime.Ctx, `SELECT session_id, auto_wake_enabled FROM agents WHERE id = $1`, createdAgent.ID).Scan(&sessionID, &autoWakeEnabled))
	require.Empty(t, sessionID)
	require.False(t, autoWakeEnabled)
}
