package localruntime

import (
	"bytes"
	"context"
	"encoding/json"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"sync"
	"testing"

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

	"github.com/tidbcloud/AgentTeam/desktop/internal/api/auth"
	"github.com/tidbcloud/AgentTeam/desktop/internal/profile"
	"github.com/tidbcloud/AgentTeam/desktop/internal/token"
	"github.com/tidbcloud/AgentTeam/desktop/internal/uisession"
)

type testServerRegistry struct {
	mu      sync.RWMutex
	servers map[string]profile.AgentTeamServer
	blocked map[string]bool
}

type testActiveTargetHistory struct {
	targetID string
	err      error
}

type testFirstMachineBindingEnsurer struct {
	binding   profile.MachineBinding
	calledFor string
	status    int
	err       error
}

func (ensurer *testFirstMachineBindingEnsurer) EnsureFirstBinding(_ context.Context, targetID string) (profile.MachineBinding, int, error) {
	ensurer.calledFor = targetID
	return ensurer.binding, ensurer.status, ensurer.err
}

func (history *testActiveTargetHistory) LastActiveTarget() (string, bool) {
	return history.targetID, history.targetID != ""
}

func (history *testActiveTargetHistory) SetLastActiveTarget(targetID string) error {
	if history.err != nil {
		return history.err
	}
	history.targetID = targetID
	return nil
}

func (history *testActiveTargetHistory) ClearLastActiveTarget(targetID string) error {
	if history.err != nil {
		return history.err
	}
	if targetID == "" || history.targetID == targetID {
		history.targetID = ""
	}
	return nil
}

func newTestServerRegistry(servers ...profile.AgentTeamServer) *testServerRegistry {
	registry := &testServerRegistry{
		servers: make(map[string]profile.AgentTeamServer, len(servers)),
		blocked: make(map[string]bool),
	}
	for _, server := range servers {
		registry.servers[server.ID] = server
	}
	return registry
}

func (r *testServerRegistry) ServerAvailableForSelection(id string) bool {
	r.mu.RLock()
	defer r.mu.RUnlock()
	return !r.blocked[id]
}

func (r *testServerRegistry) Get(id string) (profile.AgentTeamServer, bool) {
	r.mu.RLock()
	defer r.mu.RUnlock()
	server, ok := r.servers[id]
	return server, ok
}

func (r *testServerRegistry) All() []profile.AgentTeamServer {
	r.mu.RLock()
	defer r.mu.RUnlock()
	servers := make([]profile.AgentTeamServer, 0, len(r.servers))
	for _, server := range r.servers {
		servers = append(servers, server)
	}
	return servers
}

func (r *testServerRegistry) Delete(id string) {
	r.mu.Lock()
	delete(r.servers, id)
	r.mu.Unlock()
}

func newTestRouter(store *uisession.Store, servers *testServerRegistry) *gin.Engine {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	internal := router.Group("/api/internal", auth.Middleware())
	RegisterRoutes(internal, Dependencies{
		NativeWindow:     func() bool { return true },
		Platform:         "test-platform",
		Servers:          servers,
		ServerCandidates: servers,
		Sessions:         store,
	})
	return router
}

func TestListAvailableServersReturnsOnlySelectablePublicFields(t *testing.T) {
	servers := newTestServerRegistry(
		profile.AgentTeamServer{
			ID:          "server-b",
			Enabled:     true,
			Name:        "Staging",
			Description: "Secondary workspace",
			ServerURL:   "https://secret.example.test",
			WorkspaceID: "wsp-b",
			MachineID:   "mac-b",
			APIKey:      "machine-secret",
			AuthToken:   "user-secret",
			Config:      []byte(`apiKey: legacy-secret`),
		},
		profile.AgentTeamServer{
			ID:          "server-a",
			Enabled:     true,
			Name:        "Production",
			WorkspaceID: "wsp-a",
		},
		profile.AgentTeamServer{ID: "disabled", Enabled: false, Name: "Disabled"},
		profile.AgentTeamServer{ID: "blocked", Enabled: true, Name: "Blocked"},
	)
	servers.blocked["blocked"] = true
	router := newTestRouter(uisession.NewStore(uisession.Options{}), servers)

	recorder := performRequest(router, token.CreateSession(), http.MethodGet, "/api/internal/servers/available", nil)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.JSONEq(t, `{
		"servers": [
			{"id":"server-a","name":"Production","workspaceId":"wsp-a"},
			{"id":"server-b","name":"Staging","description":"Secondary workspace","workspaceId":"wsp-b"}
		]
	}`, recorder.Body.String())
	require.NotContains(t, recorder.Body.String(), "secret")
	require.NotContains(t, recorder.Body.String(), "machineId")
}

func performRequest(
	router http.Handler,
	sessionToken string,
	method string,
	path string,
	body any,
) *httptest.ResponseRecorder {
	var encoded []byte
	if body != nil {
		encoded, _ = json.Marshal(body)
	}
	req := httptest.NewRequest(method, path, bytes.NewReader(encoded))
	if sessionToken != "" {
		req.Header.Set("Authorization", "Bearer "+sessionToken)
	}
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	recorder := httptest.NewRecorder()
	router.ServeHTTP(recorder, req)
	return recorder
}

func TestClientObservationHandlerLogsFrontendReloadReason(t *testing.T) {
	var logBuffer bytes.Buffer
	router := gin.New()
	internal := router.Group("/api/internal", auth.Middleware())
	RegisterRoutes(internal, Dependencies{
		Logger: slog.New(slog.NewTextHandler(&logBuffer, nil)),
	})

	recorder := performRequest(router, token.CreateSession(), http.MethodPost, "/api/internal/client-observations", map[string]any{
		"event":         "frontend_reload",
		"reloadId":      "reload-1",
		"reload_reason": "lazy_chunk_auto_reload",
		"previousPath":  "/w/wsp_1/channels/ch_1",
		"currentPath":   "/w/wsp_1/channels/ch_1",
		"frontendBuild": "dev",
		"navType":       "reload",
		"ageMs":         12,
		"detail": map[string]string{
			"errorName": "ChunkLoadError",
		},
	})

	require.Equal(t, http.StatusNoContent, recorder.Code)
	logs := logBuffer.String()
	require.Contains(t, logs, "desktop frontend reload")
	require.Contains(t, logs, "reload_reason=lazy_chunk_auto_reload")
	require.Contains(t, logs, "reload_id=reload-1")
	require.Contains(t, logs, "previous_path=/w/wsp_1/channels/ch_1")
}

func TestClientObservationHandlerRequiresReloadReason(t *testing.T) {
	router := gin.New()
	internal := router.Group("/api/internal", auth.Middleware())
	RegisterRoutes(internal, Dependencies{})

	recorder := performRequest(router, token.CreateSession(), http.MethodPost, "/api/internal/client-observations", map[string]any{
		"event": "frontend_reload",
	})

	require.Equal(t, http.StatusBadRequest, recorder.Code)
	require.Contains(t, recorder.Body.String(), `"code":"INVALID_REQUEST"`)
}

func TestRuntimeHandlerReturnsDesktopHostFacts(t *testing.T) {
	router := newTestRouter(uisession.NewStore(uisession.Options{}), newTestServerRegistry())
	sessionToken := token.CreateSession()

	recorder := performRequest(router, sessionToken, http.MethodGet, "/api/internal/runtime", nil)

	require.Equal(t, http.StatusOK, recorder.Code)
	require.JSONEq(t, `{
		"kind": "desktop",
		"platform": "test-platform",
		"capabilities": {
			"localDaemon": true,
			"nativeWindow": true
		},
		"buildInfo": {
			"version": "unknown",
			"commit": "none",
			"buildAt": "unknown"
		}
	}`, recorder.Body.String())
}

func TestActiveServerHandlersKeepSelectionSessionLocalAndSanitizeResponse(t *testing.T) {
	store := uisession.NewStore(uisession.Options{})
	servers := newTestServerRegistry(
		profile.AgentTeamServer{
			ID:          "server-a",
			Enabled:     true,
			Name:        "Production",
			Description: "Primary server",
			ServerURL:   "https://secret.example.test",
			APIKey:      "machine-secret",
			AuthToken:   "user-secret",
			WorkspaceID: "wsp-a",
			MachineID:   "mac-a",
		},
		profile.AgentTeamServer{
			ID:          "server-b",
			Enabled:     true,
			Name:        "Staging",
			WorkspaceID: "wsp-b",
			MachineID:   "mac-b",
		},
	)
	router := newTestRouter(store, servers)
	windowA := token.CreateSession()
	windowB := token.CreateSession()

	missing := performRequest(router, windowA, http.MethodGet, "/api/internal/servers/active", nil)
	require.Equal(t, http.StatusConflict, missing.Code)
	require.JSONEq(t, `{"code":"ACTIVE_SERVER_REQUIRED","error":"Select a AgentTeam server to continue."}`, missing.Body.String())
	optionalMissing := performRequest(router, windowA, http.MethodGet, "/api/internal/servers/active?optional=true", nil)
	require.Equal(t, http.StatusOK, optionalMissing.Code)
	require.JSONEq(t, `null`, optionalMissing.Body.String())

	selectedA := performRequest(router, windowA, http.MethodPut, "/api/internal/servers/active", map[string]any{
		"serverId": "server-a",
	})
	require.Equal(t, http.StatusOK, selectedA.Code)
	require.JSONEq(t, `{
		"server": {
			"id": "server-a",
			"name": "Production",
			"description": "Primary server"
		},
		"binding": {
			"workspaceId": "wsp-a",
			"machineId": "mac-a"
		},
		"generation": 1
	}`, selectedA.Body.String())
	require.NotContains(t, selectedA.Body.String(), "machine-secret")
	require.NotContains(t, selectedA.Body.String(), "user-secret")
	require.NotContains(t, selectedA.Body.String(), "secret.example.test")

	selectedB := performRequest(router, windowB, http.MethodPut, "/api/internal/servers/active", map[string]any{
		"serverId": "server-b",
	})
	require.Equal(t, http.StatusOK, selectedB.Code)
	require.Contains(t, selectedB.Body.String(), `"generation":1`)

	switchedA := performRequest(router, windowA, http.MethodPut, "/api/internal/servers/active", map[string]any{
		"serverId": "server-b",
	})
	require.Equal(t, http.StatusOK, switchedA.Code)
	require.Contains(t, switchedA.Body.String(), `"generation":2`)

	gotB := performRequest(router, windowB, http.MethodGet, "/api/internal/servers/active", nil)
	require.Equal(t, http.StatusOK, gotB.Code)
	require.Contains(t, gotB.Body.String(), `"id":"server-b"`)
	require.Contains(t, gotB.Body.String(), `"generation":1`)

	deleted := performRequest(router, windowA, http.MethodDelete, "/api/internal/servers/active", nil)
	require.Equal(t, http.StatusNoContent, deleted.Code)

	cleared := performRequest(router, windowA, http.MethodGet, "/api/internal/servers/active", nil)
	require.Equal(t, http.StatusConflict, cleared.Code)

	reselected := performRequest(router, windowA, http.MethodPut, "/api/internal/servers/active", map[string]any{
		"serverId": "server-a",
	})
	require.Equal(t, http.StatusOK, reselected.Code)
	require.Contains(t, reselected.Body.String(), `"generation":4`)
}

func TestPutActiveServerRejectsInvalidOrUnavailableServer(t *testing.T) {
	store := uisession.NewStore(uisession.Options{})
	servers := newTestServerRegistry(profile.AgentTeamServer{
		ID:      "disabled",
		Enabled: false,
		Name:    "Disabled",
	})
	servers.servers["halted"] = profile.AgentTeamServer{
		ID:      "halted",
		Enabled: true,
		Name:    "Halted",
	}
	servers.blocked["halted"] = true
	router := newTestRouter(store, servers)
	sessionToken := token.CreateSession()

	tests := []struct {
		name     string
		body     any
		wantCode int
		wantBody string
	}{
		{
			name:     "invalid request",
			body:     map[string]any{},
			wantCode: http.StatusBadRequest,
			wantBody: `"code":"INVALID_REQUEST"`,
		},
		{
			name:     "unknown server",
			body:     map[string]any{"serverId": "missing"},
			wantCode: http.StatusNotFound,
			wantBody: `"code":"SERVER_NOT_FOUND"`,
		},
		{
			name:     "disabled server",
			body:     map[string]any{"serverId": "disabled"},
			wantCode: http.StatusConflict,
			wantBody: `"code":"SERVER_UNAVAILABLE"`,
		},
		{
			name:     "halted server",
			body:     map[string]any{"serverId": "halted"},
			wantCode: http.StatusConflict,
			wantBody: `"code":"SERVER_UNAVAILABLE"`,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			recorder := performRequest(router, sessionToken, http.MethodPut, "/api/internal/servers/active", tt.body)
			require.Equal(t, tt.wantCode, recorder.Code)
			require.Contains(t, recorder.Body.String(), tt.wantBody)
		})
	}

	_, ok := store.Get(sessionToken)
	require.False(t, ok)
}

func TestGetActiveServerClearsStaleSelectionWithoutClobberingGeneration(t *testing.T) {
	store := uisession.NewStore(uisession.Options{})
	servers := newTestServerRegistry(profile.AgentTeamServer{
		ID:          "server-a",
		Enabled:     true,
		Name:        "Production",
		WorkspaceID: "wsp-a",
		MachineID:   "mac-a",
	})
	router := newTestRouter(store, servers)
	sessionToken := token.CreateSession()

	selected := performRequest(router, sessionToken, http.MethodPut, "/api/internal/servers/active", map[string]any{
		"serverId": "server-a",
	})
	require.Equal(t, http.StatusOK, selected.Code)
	servers.Delete("server-a")

	stale := performRequest(router, sessionToken, http.MethodGet, "/api/internal/servers/active", nil)
	require.Equal(t, http.StatusConflict, stale.Code)

	snapshot, ok := store.Get(sessionToken)
	require.True(t, ok)
	require.Empty(t, snapshot.ActiveServerID)
	require.Equal(t, uint64(2), snapshot.Generation)
}

func TestGetActiveServerRestoresLastTargetIntoNewUISession(t *testing.T) {
	store := uisession.NewStore(uisession.Options{})
	servers := newTestServerRegistry(profile.AgentTeamServer{
		ID:          "desktop-user:uas_1:d3NwX29uZQ",
		Enabled:     true,
		Name:        "AgentTeam",
		WorkspaceID: "wsp_one",
	})
	history := &testActiveTargetHistory{targetID: "desktop-user:uas_1:d3NwX29uZQ"}
	gateway := gin.New()
	internal := gateway.Group("/api/internal", auth.Middleware())
	RegisterRoutes(internal, Dependencies{
		Servers:       servers,
		Sessions:      store,
		TargetHistory: history,
	})
	sessionToken := token.CreateSession()

	restored := performRequest(gateway, sessionToken, http.MethodGet, "/api/internal/servers/active?optional=true", nil)

	require.Equal(t, http.StatusOK, restored.Code)
	require.Contains(t, restored.Body.String(), `"workspaceId":"wsp_one"`)
	snapshot, ok := store.Get(sessionToken)
	require.True(t, ok)
	require.Equal(t, history.targetID, snapshot.ActiveServerID)
	require.Equal(t, uint64(1), snapshot.Generation)
}

func TestGetActiveServerEnsuresFirstMachineWhenRestoringLastTarget(t *testing.T) {
	store := uisession.NewStore(uisession.Options{})
	targetID := "desktop-user:uas_1:d3NwX29uZQ"
	servers := newTestServerRegistry(profile.AgentTeamServer{
		ID:          targetID,
		Enabled:     true,
		Name:        "AgentTeam",
		WorkspaceID: "wsp_one",
	})
	history := &testActiveTargetHistory{targetID: targetID}
	machineSetup := &testFirstMachineBindingEnsurer{
		binding: profile.MachineBinding{MachineID: "mch_local"},
		status:  http.StatusOK,
	}
	gateway := gin.New()
	internal := gateway.Group("/api/internal", auth.Middleware())
	RegisterRoutes(internal, Dependencies{
		Servers:       servers,
		Sessions:      store,
		TargetHistory: history,
		MachineSetup:  machineSetup,
	})

	restored := performRequest(gateway, token.CreateSession(), http.MethodGet, "/api/internal/servers/active?optional=true", nil)

	require.Equal(t, http.StatusOK, restored.Code)
	require.Equal(t, targetID, machineSetup.calledFor)
	require.Contains(t, restored.Body.String(), `"machineId":"mch_local"`)
}

func TestGetActiveServerDropsUnavailableLastTarget(t *testing.T) {
	store := uisession.NewStore(uisession.Options{})
	history := &testActiveTargetHistory{targetID: "missing"}
	gateway := gin.New()
	internal := gateway.Group("/api/internal", auth.Middleware())
	RegisterRoutes(internal, Dependencies{
		Servers:       newTestServerRegistry(),
		Sessions:      store,
		TargetHistory: history,
	})

	response := performRequest(gateway, token.CreateSession(), http.MethodGet, "/api/internal/servers/active?optional=true", nil)

	require.Equal(t, http.StatusOK, response.Code)
	require.JSONEq(t, "null", response.Body.String())
	require.Empty(t, history.targetID)
}

func TestActiveServerHandlersRequireAuthenticatedSessionIdentity(t *testing.T) {
	store := uisession.NewStore(uisession.Options{})
	router := gin.New()
	router.PUT("/api/internal/servers/active", NewPutActiveServerHandler(store, newTestServerRegistry()))

	recorder := performRequest(router, "", http.MethodPut, "/api/internal/servers/active", map[string]any{
		"serverId": "server-a",
	})

	require.Equal(t, http.StatusUnauthorized, recorder.Code)
	require.Contains(t, recorder.Body.String(), `"code":"SESSION_REQUIRED"`)
}
