package im_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/im"
	"github.com/tidbcloud/AgentTeam/internal/store/taskboard"
)

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

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

	client := server.Client()
	auth, workspaceID, channelID := testkit.SetupTaskHTTPActor(
		t,
		client,
		server.URL,
		"task-conversation-actions@example.com",
		"task-conversation-actions",
	)
	var memberID string
	require.NoError(t, runtime.Pool.QueryRow(runtime.Ctx, `
SELECT id
FROM members
WHERE workspace_id = $1
  AND user_id = $2
  AND type = 'human'
`, workspaceID, auth.User.ID).Scan(&memberID))

	var created taskboard.TaskPage
	response := testkit.DoJSON(
		t,
		client,
		http.MethodPost,
		server.URL+"/api/v1/workspaces/"+url.PathEscape(workspaceID)+"/channels/"+url.PathEscape(channelID)+"/tasks",
		map[string]any{"tasks": []map[string]any{{"title": "Keep Task action state current"}}},
		testkit.WorkspaceHeaders(auth.AccessToken, workspaceID),
		&created,
	)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Len(t, created.Tasks, 1)
	task := created.Tasks[0]
	conversationID := "task:" + task.TaskScopeID
	now := time.Now().UTC()
	require.NoError(t, runtime.Store.IM().UpsertConversation(runtime.Ctx, workspaceID, memberID, im.ConversationIndexItem{
		ConversationID: conversationID,
		ScopeID:        task.TaskScopeID,
		Kind:           "task",
		SortAt:         now,
		Projection: map[string]any{
			"id":      conversationID,
			"scopeId": task.TaskScopeID,
			"kind":    "task",
			"title":   fmt.Sprintf("#t%d %s", task.TaskNumber, task.Title),
			"task": map[string]any{
				"id":        task.ID,
				"scopeId":   task.TaskScopeID,
				"channelId": task.ChannelID,
				"number":    task.TaskNumber,
				"title":     task.Title,
				"status":    task.Status,
			},
		},
	}))

	actionURL := server.URL + fmt.Sprintf(
		"/api/v1/workspaces/%s/conversations/%s/actions",
		url.PathEscape(workspaceID),
		url.PathEscape(conversationID),
	)
	performAction := func(action string) dto.ConversationItem {
		t.Helper()
		var item dto.ConversationItem
		actionResponse := testkit.DoJSON(
			t,
			client,
			http.MethodPost,
			actionURL,
			map[string]any{"action": action},
			testkit.WorkspaceHeaders(auth.AccessToken, workspaceID),
			&item,
		)
		require.Equal(t, http.StatusOK, actionResponse.StatusCode)
		return item
	}

	muted := performAction("mute")
	require.Equal(t, "mute", muted.SubscriptionState)
	indexed, err := runtime.Store.IM().GetConversation(runtime.Ctx, workspaceID, memberID, conversationID)
	require.NoError(t, err)
	require.Nil(t, indexed.ArchivedAt, "Unwatch must not archive the member-local index row")
	var mutedList dto.ConversationPage
	response = testkit.DoJSON(
		t,
		client,
		http.MethodGet,
		server.URL+fmt.Sprintf("/api/v1/workspaces/%s/conversations?source=index&kinds=task", url.PathEscape(workspaceID)),
		nil,
		testkit.WorkspaceHeaders(auth.AccessToken, workspaceID),
		&mutedList,
	)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Empty(t, mutedList.Items, "a muted Task must not be returned by the member-local Inbox index list")

	unmuted := performAction("unmute")
	require.Equal(t, "active", unmuted.SubscriptionState)
	var watchedList dto.ConversationPage
	response = testkit.DoJSON(
		t,
		client,
		http.MethodGet,
		server.URL+fmt.Sprintf("/api/v1/workspaces/%s/conversations?source=index&kinds=task", url.PathEscape(workspaceID)),
		nil,
		testkit.WorkspaceHeaders(auth.AccessToken, workspaceID),
		&watchedList,
	)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Len(t, watchedList.Items, 1)
	require.Equal(t, conversationID, watchedList.Items[0].ID)

	pinned := performAction("pin")
	require.NotNil(t, pinned.PinnedAt)
	indexed, err = runtime.Store.IM().GetConversation(runtime.Ctx, workspaceID, memberID, conversationID)
	require.NoError(t, err)
	require.NotNil(t, indexed.PinnedAt)

	unpinned := performAction("unpin")
	require.Nil(t, unpinned.PinnedAt)
	indexed, err = runtime.Store.IM().GetConversation(runtime.Ctx, workspaceID, memberID, conversationID)
	require.NoError(t, err)
	require.Nil(t, indexed.PinnedAt)

	hidden := performAction("hide")
	require.True(t, hidden.SidebarHidden)
	require.True(t, hidden.Archived)
	indexed, err = runtime.Store.IM().GetConversation(runtime.Ctx, workspaceID, memberID, conversationID)
	require.NoError(t, err)
	require.NotNil(t, indexed.ArchivedAt)
	require.Equal(t, true, indexed.Projection["sidebarHidden"])
	require.Equal(t, true, indexed.Projection["archived"])

	events, err := runtime.Store.IM().ListEventsAfter(runtime.Ctx, workspaceID, memberID, 0, 100)
	require.NoError(t, err)
	var archivedEvent *im.StreamEvent
	for index := range events {
		if events[index].Type == "conversation.archived" && events[index].ConversationID == conversationID {
			archivedEvent = &events[index]
			break
		}
	}
	require.NotNil(t, archivedEvent)
	eventConversation, ok := archivedEvent.Payload["conversation"].(map[string]any)
	require.True(t, ok)
	require.Equal(t, true, eventConversation["sidebarHidden"])
	require.Equal(t, true, eventConversation["archived"])
}

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

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

	client := server.Client()
	auth := testkit.RegisterUserForOrganizationTests(t, client, server.URL, "deleted-service-inbox@example.com", "deleted-service-inbox")
	var workspaces []dto.WorkspaceResponse
	response := testkit.DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces", nil, testkit.AuthHeaders(auth.AccessToken), &workspaces)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, workspaces)
	workspaceID := workspaces[0].ID
	memberID := workspaces[0].MemberID

	const scopeID = "dm_deleted_agent_inbox_http"
	conversationID := "dm:" + scopeID
	now := time.Date(2026, 7, 14, 12, 0, 0, 0, time.UTC)
	_, err := runtime.Pool.Exec(runtime.Ctx, `
INSERT INTO scopes (
  id, workspace_id, type, identity_key, latest_seq, latest_global_seq,
  latest_message_at, visible_message_count, visible_last_message_at,
  status, created_by_member_id, created_at, updated_at
) VALUES ($1, $2, 'dm', $3, 1, 1, $4, 1, $4, 'active', $5, $4, $4)
`, scopeID, workspaceID, "dm:"+workspaceID+":"+scopeID, now, memberID)
	require.NoError(t, err)
	_, err = runtime.Pool.Exec(runtime.Ctx, `
INSERT INTO scope_memberships (
  scope_id, member_id, visibility, last_read_seq, last_read_global_seq,
  effective_from_seq, effective_to_seq, created_at, updated_at
) VALUES ($1, $2, 'public', 0, 0, 0, NULL, $3, $3)
`, scopeID, memberID, now)
	require.NoError(t, err)
	require.NoError(t, runtime.Store.IM().UpsertConversation(runtime.Ctx, workspaceID, memberID, im.ConversationIndexItem{
		ConversationID: conversationID,
		ScopeID:        scopeID,
		Kind:           "dm",
		SortAt:         now,
		Projection: map[string]any{
			"id":       conversationID,
			"scopeId":  scopeID,
			"kind":     "agentDm",
			"route":    "/inbox/agents/agt_deleted_http",
			"title":    "Deleted Agent",
			"peerType": "agent",
			"peerId":   "agt_deleted_http",
		},
	}))
	_, err = runtime.Pool.Exec(runtime.Ctx, `DELETE FROM scopes WHERE workspace_id = $1 AND id = $2`, workspaceID, scopeID)
	require.NoError(t, err)

	conversationsURL := server.URL + fmt.Sprintf("/api/v1/workspaces/%s/conversations?source=index&kinds=dm", url.PathEscape(workspaceID))
	var before dto.ConversationPage
	response = testkit.DoJSON(t, client, http.MethodGet, conversationsURL, nil, testkit.WorkspaceHeaders(auth.AccessToken, workspaceID), &before)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Len(t, before.Items, 1)
	require.Equal(t, conversationID, before.Items[0].ID)

	actionURL := server.URL + fmt.Sprintf(
		"/api/v1/workspaces/%s/conversations/%s/actions",
		url.PathEscape(workspaceID),
		url.PathEscape(conversationID),
	)
	var hidden dto.ConversationItem
	response = testkit.DoJSON(t, client, http.MethodPost, actionURL, map[string]any{"action": "hide"}, testkit.WorkspaceHeaders(auth.AccessToken, workspaceID), &hidden)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Equal(t, conversationID, hidden.ID)
	require.True(t, hidden.SidebarHidden)
	require.True(t, hidden.Archived)

	indexed, err := runtime.Store.IM().GetConversation(runtime.Ctx, workspaceID, memberID, conversationID)
	require.NoError(t, err)
	require.NotNil(t, indexed.ArchivedAt)
	require.Equal(t, true, indexed.Projection["archived"])

	events, err := runtime.Store.IM().ListEventsAfter(runtime.Ctx, workspaceID, memberID, 0, 100)
	require.NoError(t, err)
	var archivedEvent *im.StreamEvent
	for index := range events {
		if events[index].Type == "conversation.archived" && events[index].ConversationID == conversationID {
			archivedEvent = &events[index]
			break
		}
	}
	require.NotNil(t, archivedEvent)
	eventConversation, ok := archivedEvent.Payload["conversation"].(map[string]any)
	require.True(t, ok)
	require.Equal(t, "agentDm", eventConversation["kind"])
	require.Equal(t, true, eventConversation["archived"])
	require.EqualValues(t, 0, eventConversation["unreadCount"], "replayed archive events must satisfy the realtime ConversationItem contract")

	var after dto.ConversationPage
	response = testkit.DoJSON(t, client, http.MethodGet, conversationsURL, nil, testkit.WorkspaceHeaders(auth.AccessToken, workspaceID), &after)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Empty(t, after.Items)
}
