package app

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
)

// TestTaskReactionForwardReverseChannelMatrix proves the canonical Task reaction
// authorizer: a same-workspace active Agent+Machine+Member with active board
// Channel membership and no Task scope membership can add a Task reaction
// (forward); no Channel membership with retained Task membership must fail
// closed with no residue (reverse).
func TestTaskReactionForwardReverseChannelMatrix(t *testing.T) {
	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{})
	now := time.Now().UTC()
	fixture := seedAppAdminFixture(t, pool, "task_reaction_chan_matrix", now)
	const (
		machineID     = "mach_task_reaction_chan_matrix"
		agentID       = "agt_task_reaction_chan_matrix"
		agentMemberID = "mem_task_reaction_chan_matrix_agent"
	)
	seedAppAgentParticipant(
		t, pool, fixture.WorkspaceID, fixture.UserID, fixture.MemberID,
		fixture.AllChannelID, machineID, agentID, agentMemberID, "task-reaction-matrix-agent", now,
	)
	taskScopeID := "tsk_task_reaction_chan_matrix"
	// Forward seed: the task exists with its board, but the agent is NOT a Task
	// scope member. Only the board Channel membership exists, so the forward case
	// truly exercises "Channel member / no Task membership".
	seedTaskNotificationScope(t, pool, fixture, taskScopeID, "", now)
	messageID := "msg_task_reaction_chan_matrix"
	mustExec(t, pool, `
INSERT INTO scope_messages (
  id, workspace_id, scope_id, seq, workspace_global_seq,
  author_member_id, author_type, content, metadata, created_at, updated_at
) VALUES ($1, $2, $3, 1, 1, $4, 'human', 'Task reaction matrix', '{}', $5, $5)
`, messageID, fixture.WorkspaceID, taskScopeID, fixture.MemberID, now)
	mustExec(t, pool, `
UPDATE scopes SET latest_seq = 1, latest_global_seq = 1, latest_message_at = $2, updated_at = $2 WHERE id = $1
`, taskScopeID, now)

	auth := agent.AuthenticatedAgent{AgentID: agentID, WorkspaceID: fixture.WorkspaceID, MemberID: agentMemberID, MachineID: machineID}

	// Assert the forward precondition truly holds: the agent has board Channel
	// membership but NO direct Task scope membership.
	var taskMembershipCount int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_memberships sm
JOIN tasks t ON t.task_scope_id = sm.scope_id
WHERE t.task_scope_id = $1 AND sm.member_id = $2 AND sm.removed_at IS NULL
`, taskScopeID, agentMemberID).Scan(&taskMembershipCount))
	require.Zero(t, taskMembershipCount, "forward matrix requires no direct Task scope membership")

	// Forward: board Channel member with NO Task scope membership adds a Task
	// reaction (canonical Task->board Channel authority).
	_, err := store.Reaction().AddReactionForAgent(ctx, auth, messageID, "👍")
	require.NoError(t, err)
	var reactionCount int
	require.NoError(t, pool.QueryRow(ctx, `SELECT COUNT(*) FROM scope_message_reactions WHERE message_id = $1`, messageID).Scan(&reactionCount))
	require.Equal(t, 1, reactionCount)

	// Reverse: retain the agent's Task scope membership, then revoke the board
	// Channel membership. The retained Task membership must grant nothing.
	require.NoError(t, message.EnsureScopeMembership(context.Background(), pool, taskScopeID, agentMemberID, "private", fixture.MemberID, "", 0, 0, now))
	mustExec(t, pool, `
UPDATE scope_memberships
SET removed_at = $3, updated_at = $3
WHERE scope_id = $1 AND member_id = $2 AND removed_at IS NULL
`, fixture.AllChannelID, agentMemberID, now)
	_, err = store.Reaction().AddReactionForAgent(ctx, auth, messageID, "🎉")
	require.Error(t, err)
	require.ErrorIs(t, err, ErrForbidden)
	require.NoError(t, pool.QueryRow(ctx, `SELECT COUNT(*) FROM scope_message_reactions WHERE message_id = $1`, messageID).Scan(&reactionCount))
	require.Equal(t, 1, reactionCount, "revoked reaction write must leave no residue")
}
