package admin_test

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"net/url"
	"testing"
	"time"

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

	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	. "github.com/tidbcloud/AgentTeam/internal/api/testkit"
	"github.com/tidbcloud/AgentTeam/internal/store/admin"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/attachment"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
)

func TestAdminOverviewReturnsCounts(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 adminAuth dto.AuthTokensResponse
	response := DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/auth/register", map[string]any{
		"email":    "overview-admin@example.com",
		"password": TestUserPassword,
		"name":     "overview-admin",
	}, nil, &adminAuth)
	require.Equal(t, http.StatusOK, response.StatusCode)

	var workspaces []dto.WorkspaceResponse
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces", nil, AuthHeaders(adminAuth.AccessToken), &workspaces)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, workspaces)
	workspaceID := workspaces[0].ID

	allChannelID := WorkspaceAllChannelID(t, runtime, workspaceID)

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

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

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

	now := time.Now().UTC()
	_, err := runtime.Pool.Exec(runtime.Ctx, `
UPDATE machines
SET connected_at = $2, disconnected_at = NULL, updated_at = $2
WHERE id = $1
`, machineResponse.Machine.ID, now)
	require.NoError(t, err)

	response = DoJSON(t, client, http.MethodPost, server.URL+WorkspaceScopeMessagesPath(workspaceID, allChannelID), map[string]any{
		"scopeId": allChannelID,
		"content": "@overview-agent admin activity check",
	}, WorkspaceHeaders(adminAuth.AccessToken, workspaceID), nil)
	require.Equal(t, http.StatusOK, response.StatusCode)

	var overview struct {
		Window   string              `json:"window"`
		Overview admin.AdminOverview `json:"overview"`
	}
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/admin/overview?window=7d", nil, AuthHeaders(adminAuth.AccessToken), &overview)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Equal(t, "7d", overview.Window)
	require.GreaterOrEqual(t, overview.Overview.TotalWorkspaces, int64(2))
	require.GreaterOrEqual(t, overview.Overview.TotalUsers, int64(2))
	require.GreaterOrEqual(t, overview.Overview.TotalAgents, int64(1))
	require.GreaterOrEqual(t, overview.Overview.TotalMachines, int64(1))
	require.GreaterOrEqual(t, overview.Overview.ActiveUsers, int64(1))
	require.GreaterOrEqual(t, overview.Overview.MessageCount, int64(1))
}

func TestAdminWorkspacesListCountsNormalChannelsOnly(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 adminAuth dto.AuthTokensResponse
	response := DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/auth/register", map[string]any{
		"email":    "workspace-admin@example.com",
		"password": TestUserPassword,
		"name":     "workspace-admin",
	}, nil, &adminAuth)
	require.Equal(t, http.StatusOK, response.StatusCode)

	var workspaces []dto.WorkspaceResponse
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces", nil, AuthHeaders(adminAuth.AccessToken), &workspaces)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, workspaces)
	workspaceID := workspaces[0].ID

	allChannelID := WorkspaceAllChannelID(t, runtime, workspaceID)

	var firstChannel ChannelResponse
	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/channels", map[string]any{
		"name":        "meta-agentteam",
		"description": "meta discussion",
	}, WorkspaceHeaders(adminAuth.AccessToken, workspaceID), &firstChannel)
	require.Equal(t, http.StatusOK, response.StatusCode)

	var secondChannel ChannelResponse
	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/channels", map[string]any{
		"name":        "tangenta-vector",
		"description": "vector discussion",
	}, WorkspaceHeaders(adminAuth.AccessToken, workspaceID), &secondChannel)
	require.Equal(t, http.StatusOK, response.StatusCode)

	var parentMessage message.AppMessage
	response = DoJSON(t, client, http.MethodPost, server.URL+WorkspaceScopeMessagesPath(workspaceID, allChannelID), map[string]any{
		"scopeId": allChannelID,
		"content": "open one reply thread",
	}, WorkspaceHeaders(adminAuth.AccessToken, workspaceID), &parentMessage)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, parentMessage.ID)

	var replyThread message.ReplyThreadSummary
	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/channels/"+allChannelID+"/threads", map[string]any{
		"parentMessageId": parentMessage.ID,
	}, WorkspaceHeaders(adminAuth.AccessToken, workspaceID), &replyThread)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, replyThread.ThreadChannelID)

	var totalChannelScopes int
	require.NoError(t, runtime.Pool.QueryRow(runtime.Ctx, `
SELECT COUNT(*)
FROM scopes
WHERE workspace_id = $1
  AND type IN ('channel', 'thread')
  AND status = 'active'
  AND archived_at IS NULL
`, workspaceID).Scan(&totalChannelScopes))
	require.Equal(t, 4, totalChannelScopes)

	var adminWorkspaces []admin.AdminWorkspaceSummary
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/admin/workspaces?query="+url.QueryEscape(workspaceID), nil, AuthHeaders(adminAuth.AccessToken), &adminWorkspaces)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Len(t, adminWorkspaces, 1)
	require.Equal(t, workspaceID, adminWorkspaces[0].ID)
	require.EqualValues(t, 3, adminWorkspaces[0].ChannelCount)
}

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

	runtime := StartRuntimeWithoutBackgroundWorkers(t)
	router := gin.New()
	runtime.RegisterRoutes(router)
	server := httptest.NewServer(router)
	defer server.Close()
	client := server.Client()

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

	var account dto.AdminPlatformServiceAccountResponse
	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/admin/platform-service-accounts", map[string]any{
		"id": "psacct_http_platform", "name": "HTTP Platform News", "description": "HTTP E2E",
	}, AuthHeaders(adminAuth.AccessToken), &account)
	require.Equal(t, http.StatusCreated, response.StatusCode)
	var accountPage struct {
		Items      []dto.AdminPlatformServiceAccountResponse `json:"items"`
		NextCursor string                                    `json:"nextCursor"`
	}
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/admin/platform-service-accounts?q=http+e2e&limit=100", nil, AuthHeaders(adminAuth.AccessToken), &accountPage)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Len(t, accountPage.Items, 1)
	require.Equal(t, account.ID, accountPage.Items[0].ID)

	accountURL := server.URL + "/api/v1/admin/platform-service-accounts/" + account.ID
	for round := 0; round < 8 && account.ProvisioningState != "ready"; round++ {
		response = DoJSON(t, client, http.MethodPost, accountURL+"/reconcile", nil, AuthHeaders(adminAuth.AccessToken), nil)
		require.Equal(t, http.StatusOK, response.StatusCode)
		response = DoJSON(t, client, http.MethodGet, accountURL, nil, AuthHeaders(adminAuth.AccessToken), &account)
		require.Equal(t, http.StatusOK, response.StatusCode)
	}
	require.Equal(t, "ready", account.ProvisioningState)
	avatarAssetID, err := runtime.Store.Attachment().UploadResourceAvatar(
		runtime.Ctx, workspaces[0].ID, "service_account", account.ID,
		attachment.CreateAttachmentParams{
			Filename: "platform-account.png", ContentType: "image/png", Blob: []byte("platform-account-avatar"),
		},
	)
	require.NoError(t, err)
	response = DoJSON(t, client, http.MethodPatch, accountURL+"/avatar", map[string]any{
		"expectedVersion": account.Version,
		"avatarAssetId":   avatarAssetID,
	}, AuthHeaders(adminAuth.AccessToken), &account)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Equal(t, avatarAssetID, account.AvatarAssetID)
	require.Equal(t, "/api/v1/avatar-assets/"+avatarAssetID, account.AvatarURL)

	var broadcast dto.AdminPlatformBroadcastResponse
	response = DoJSON(t, client, http.MethodPost, accountURL+"/broadcasts", map[string]any{
		"clientBroadcastId": "http-broadcast-1", "title": "HTTP announcement", "content": "HTTP platform announcement",
	}, AuthHeaders(adminAuth.AccessToken), &broadcast)
	require.Equal(t, http.StatusAccepted, response.StatusCode)
	require.Equal(t, "waiting_provisioning", broadcast.State)
	broadcastURL := accountURL + "/broadcasts/" + broadcast.ID
	broadcastDeadline := time.Now().Add(15 * time.Second)
	for broadcast.State != "completed" && time.Now().Before(broadcastDeadline) {
		response = DoJSON(t, client, http.MethodPost, accountURL+"/reconcile", nil, AuthHeaders(adminAuth.AccessToken), nil)
		require.Equal(t, http.StatusOK, response.StatusCode)
		response = DoJSON(t, client, http.MethodGet, broadcastURL, nil, AuthHeaders(adminAuth.AccessToken), &broadcast)
		require.Equal(t, http.StatusOK, response.StatusCode)
		if broadcast.State != "completed" {
			time.Sleep(100 * time.Millisecond)
		}
	}
	require.Equal(t, "completed", broadcast.State)
	require.Equal(t, int64(1), broadcast.TargetCount)
	require.Equal(t, int64(1), broadcast.DeliveredCount)
	var broadcastPage struct {
		Items      []dto.AdminPlatformBroadcastResponse `json:"items"`
		NextCursor string                               `json:"nextCursor"`
	}
	response = DoJSON(
		t, client, http.MethodGet,
		accountURL+"/broadcasts?workspaceId="+url.QueryEscape(workspaces[0].ID),
		nil, AuthHeaders(adminAuth.AccessToken), &broadcastPage,
	)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Len(t, broadcastPage.Items, 1)
	require.Equal(t, "HTTP announcement", broadcastPage.Items[0].Title)
	require.Equal(t, "platform-service-account-admin", broadcastPage.Items[0].OperatorDisplayName)
	require.NotEmpty(t, broadcastPage.Items[0].WorkspaceServiceAccountID)
	require.NotEmpty(t, broadcastPage.Items[0].WorkspaceMessageID)
	require.Equal(t, int64(1), func() int64 {
		var count int64
		require.NoError(t, runtime.Pool.QueryRow(runtime.Ctx, `
SELECT COUNT(*)
FROM scope_messages message
JOIN service_accounts account ON account.message_scope_id = message.scope_id
WHERE account.platform_service_account_id = $1
  AND message.client_message_id = $2
`, account.ID, "platform-broadcast:"+broadcast.ID).Scan(&count))
		return count
	}())

	response = DoJSON(t, client, http.MethodGet, accountURL, nil, AuthHeaders(adminAuth.AccessToken), &account)
	require.Equal(t, http.StatusOK, response.StatusCode)
	var localAccountID, localScopeID, localInstallationID, localAuthorMemberID string
	require.NoError(t, runtime.Pool.QueryRow(runtime.Ctx, `
SELECT id, message_scope_id, service_installation_id, author_member_id
FROM service_accounts
WHERE platform_service_account_id = $1
`, account.ID).Scan(&localAccountID, &localScopeID, &localInstallationID, &localAuthorMemberID))

	var deleted dto.AdminPlatformServiceAccountResponse
	response = DoJSON(t, client, http.MethodDelete, accountURL+fmt.Sprintf("?expectedVersion=%d", account.Version+1), nil, AuthHeaders(adminAuth.AccessToken), nil)
	require.Equal(t, http.StatusConflict, response.StatusCode, "platform direct delete must honor the root version fence")
	response = DoJSON(t, client, http.MethodDelete, accountURL+fmt.Sprintf("?expectedVersion=%d", account.Version), nil, AuthHeaders(adminAuth.AccessToken), &deleted)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Equal(t, account.ID, deleted.ID)
	response = DoJSON(t, client, http.MethodGet, accountURL, nil, AuthHeaders(adminAuth.AccessToken), nil)
	require.Equal(t, http.StatusNotFound, response.StatusCode)
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/admin/platform-service-accounts?limit=100", nil, AuthHeaders(adminAuth.AccessToken), &accountPage)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Empty(t, accountPage.Items)

	checks := []struct {
		name  string
		query string
		arg   string
	}{
		{name: "root", query: `SELECT COUNT(*) FROM platform_service_accounts WHERE id = $1`, arg: account.ID},
		{name: "local account", query: `SELECT COUNT(*) FROM service_accounts WHERE id = $1`, arg: localAccountID},
		{name: "workspace instance", query: `SELECT COUNT(*) FROM platform_service_account_workspace_instances WHERE platform_service_account_id = $1`, arg: account.ID},
		{name: "scope", query: `SELECT COUNT(*) FROM scopes WHERE id = $1`, arg: localScopeID},
		{name: "message", query: `SELECT COUNT(*) FROM scope_messages WHERE client_message_id = $1`, arg: "platform-broadcast:" + broadcast.ID},
		{name: "installation", query: `SELECT COUNT(*) FROM service_installations WHERE id = $1`, arg: localInstallationID},
		{name: "author", query: `SELECT COUNT(*) FROM members WHERE id = $1`, arg: localAuthorMemberID},
		{name: "broadcast", query: `SELECT COUNT(*) FROM platform_service_account_broadcasts WHERE platform_service_account_id = $1`, arg: account.ID},
		{name: "provisioning input", query: `SELECT COUNT(*) FROM platform_service_account_provisioning_inputs WHERE platform_service_account_id = $1`, arg: account.ID},
		{name: "name namespace", query: `SELECT COUNT(*) FROM service_account_name_namespaces WHERE platform_service_account_id = $1`, arg: account.ID},
		{name: "avatar", query: `SELECT COUNT(*) FROM avatars WHERE owner_kind = 'service_account' AND owner_id = $1`, arg: account.ID},
	}
	for _, check := range checks {
		var count int64
		require.NoError(t, runtime.Pool.QueryRow(runtime.Ctx, check.query, check.arg).Scan(&count), check.name)
		require.Zero(t, count, check.name)
	}
	var deletionAudits int64
	require.NoError(t, runtime.Pool.QueryRow(runtime.Ctx, `
SELECT COUNT(*)
FROM service_account_audit_events
WHERE event_type = 'platform_service_account.deleted'
  AND subject_id = $1
  AND platform_service_account_id IS NULL
`, account.ID).Scan(&deletionAudits))
	require.Equal(t, int64(1), deletionAudits)

}
