package app

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

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

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	now := time.Unix(100, 0).UTC()
	fixture := seedAppAdminFixture(t, pool, "agent_auto_wake_create", now)
	seedMachineForRuntimeList(t, pool, fixture.WorkspaceID, fixture.UserID, "mac_agent_auto_wake_create", now)

	disabled := false
	store := New(pool, Options{})
	fanout := &recordingCommittedMessageFanout{}
	store.SetCommittedMessageFanout(fanout)
	created, err := store.CreateAgentForUser(ctx, fixture.UserID, fixture.WorkspaceID, "mac_agent_auto_wake_create", agent.CreateAgentParams{
		Name:            "auto-wake-disabled",
		DisplayName:     "Auto Wake Disabled",
		Runtime:         "codex",
		Model:           "gpt-5.4",
		AutoWakeEnabled: &disabled,
	})
	require.NoError(t, err)
	require.False(t, created.AutoWakeEnabled)

	var stored bool
	require.NoError(t, pool.QueryRow(ctx, `SELECT auto_wake_enabled FROM agents WHERE id = $1`, created.AgentID).Scan(&stored))
	require.False(t, stored)
	require.Empty(t, fanout.deliveries)
	require.Empty(t, fanout.projectionOnly, "agent creation without explicit initial channels has no member-event projection")
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	now := time.Unix(200, 0).UTC()
	fixture := seedAppAdminFixture(t, pool, "agent_initial_channel_event", now)
	seedMachineForRuntimeList(t, pool, fixture.WorkspaceID, fixture.UserID, "mac_agent_initial_channel_event", now)
	firstChannelID := "chn_agent_initial_channel_event_a"
	secondChannelID := "chn_agent_initial_channel_event_b"
	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, firstChannelID, "initial-channel-event-a", "", "private", fixture.MemberID, fixture.MemberID, now)
	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, secondChannelID, "initial-channel-event-b", "", "private", fixture.MemberID, fixture.MemberID, now)

	store := New(pool, Options{})
	fanout := &recordingCommittedMessageFanout{
		projectionOnlyQuery: func(callbackCtx context.Context, delivery message.MessageDelivery) {
			var messageCount int
			require.NoError(t, pool.QueryRow(callbackCtx, `
SELECT COUNT(*)
FROM scope_messages
WHERE workspace_id = $1 AND id = $2
`, fixture.WorkspaceID, delivery.MessageID).Scan(&messageCount))
			require.Equal(t, 1, messageCount, "initial channel projection must run only after create-agent commit")
		},
	}
	store.SetCommittedMessageFanout(fanout)
	created, err := store.CreateAgentForUser(ctx, fixture.UserID, fixture.WorkspaceID, "mac_agent_initial_channel_event", agent.CreateAgentParams{
		Name:        "initial-channel-agent",
		DisplayName: "Initial Channel Agent",
		Runtime:     "codex",
		Model:       "gpt-5.4",
		ChannelIDs:  []string{secondChannelID, firstChannelID, secondChannelID, fixture.AllChannelID, " "},
	})
	require.NoError(t, err)

	require.Equal(t, 1, activePrivateAgentMembershipCount(t, pool, firstChannelID, created.MemberID, fixture.MemberID))
	require.Equal(t, 1, activePrivateAgentMembershipCount(t, pool, secondChannelID, created.MemberID, fixture.MemberID))
	require.Equal(t, 1, activePrivateAgentMembershipCount(t, pool, fixture.AllChannelID, created.MemberID, fixture.MemberID))
	require.Equal(t, 1, channelMemberEventMessageCount(t, pool, firstChannelID), "duplicate channelIds must not duplicate member events")
	require.Equal(t, 1, channelMemberEventMessageCount(t, pool, secondChannelID), "duplicate channelIds must not duplicate member events")
	require.Equal(t, 0, channelMemberEventMessageCount(t, pool, fixture.AllChannelID), "#all default roster remains silent")
	require.Empty(t, fanout.deliveries, "channel membership transcript events must not enter ordinary committed-message fanout")
	require.Len(t, fanout.projectionOnly, 2)
	require.Equal(t, []string{fixture.WorkspaceID, fixture.WorkspaceID}, fanout.projectionWorkspaces)
	require.Equal(t, []string{firstChannelID, secondChannelID}, []string{
		fanout.projectionMessages[0].ChannelID,
		fanout.projectionMessages[1].ChannelID,
	})

	var action, targetKind, targetMemberID, channelDisplayName string
	require.NoError(t, pool.QueryRow(ctx, `
SELECT
  JSON_UNQUOTE(JSON_EXTRACT(metadata, '$.channelMemberEvent.action')),
  JSON_UNQUOTE(JSON_EXTRACT(metadata, '$.channelMemberEvent.targetKind')),
  JSON_UNQUOTE(JSON_EXTRACT(metadata, '$.channelMemberEvent.targetMemberId')),
  JSON_UNQUOTE(JSON_EXTRACT(metadata, '$.channelMemberEvent.channelDisplayName'))
FROM scope_messages
WHERE scope_id = $1
  AND message_type = 'system'
  AND JSON_EXTRACT(metadata, '$.channelMemberEvent.action') IS NOT NULL
	`, firstChannelID).Scan(&action, &targetKind, &targetMemberID, &channelDisplayName))
	require.Equal(t, "added", action)
	require.Equal(t, "agent", targetKind)
	require.Equal(t, created.MemberID, targetMemberID)
	require.Equal(t, "initial-channel-event-a", channelDisplayName)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	now := time.Unix(300, 0).UTC()
	fixture := seedAppAdminFixture(t, pool, "agent_initial_channel_event_rollback", now)
	seedMachineForRuntimeList(t, pool, fixture.WorkspaceID, fixture.UserID, "mac_agent_initial_channel_event_rollback", now)
	channelID := "chn_agent_initial_channel_event_rollback"
	seedChannelParticipantProjectionForTests(t, pool, fixture.WorkspaceID, channelID, "initial-channel-event-rollback", "", "private", fixture.MemberID, fixture.MemberID, now)

	store := New(pool, Options{})
	fanout := &recordingCommittedMessageFanout{}
	store.SetCommittedMessageFanout(fanout)
	tx, err := pool.BeginTx(ctx, coredb.TxOptions{})
	require.NoError(t, err)

	created, memberEvents, err := store.CreateWorkspaceAgentInTx(ctx, tx, fixture.WorkspaceID, "mac_agent_initial_channel_event_rollback", agent.CreateAgentParams{
		Name:        "rollback-initial-channel-agent",
		DisplayName: "Rollback Initial Channel Agent",
		Runtime:     "codex",
		Model:       "gpt-5.4",
		ChannelIDs:  []string{channelID},
	})
	require.NoError(t, err)
	require.Len(t, memberEvents, 1, "caller-owned transaction must receive every projection handoff")
	require.True(t, memberEvents[0].Changed)
	require.Equal(t, channelID, memberEvents[0].Message.ChannelID)
	require.Empty(t, fanout.deliveries)
	require.Empty(t, fanout.projectionOnly, "caller-owned transaction must not emit before its caller commits")

	require.NoError(t, tx.Rollback(ctx))
	require.Empty(t, fanout.projectionOnly, "rolling back a caller-owned transaction must discard its projection handoffs")

	var agentCount int
	require.NoError(t, pool.QueryRow(ctx, `SELECT COUNT(*) FROM agents WHERE workspace_id = $1 AND id = $2`, fixture.WorkspaceID, created.AgentID).Scan(&agentCount))
	require.Zero(t, agentCount)
	require.Equal(t, 0, channelMemberEventMessageCount(t, pool, channelID))
}

func activePrivateAgentMembershipCount(t *testing.T, pool interface {
	QueryRow(context.Context, string, ...any) coredb.Row
}, channelID string, agentMemberID string, ownerMemberID string) int {
	t.Helper()

	var count int
	require.NoError(t, pool.QueryRow(context.Background(), `
SELECT COUNT(*)
FROM scope_memberships
WHERE scope_id = $1
  AND member_id = $2
  AND visibility = 'private'
  AND owner_member_id = $3
  AND removed_at IS NULL
  AND archived_at IS NULL
  AND effective_to_seq IS NULL
`, channelID, agentMemberID, ownerMemberID).Scan(&count))
	return count
}
