package app

import (
	"context"
	"database/sql"
	"errors"
	"strings"
	"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/im"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
)

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

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

	store := New(pool, Options{})
	created, err := store.CreateAgentForUser(ctx, fixture.UserID, fixture.WorkspaceID, "mac_delete_agent_team_membership", agent.CreateAgentParams{
		Name:        "team-helper",
		DisplayName: "Team Helper",
		Runtime:     "codex",
		Model:       "gpt-5.4",
	})
	require.NoError(t, err)

	teamID := "wst_delete_agent_team_membership"
	mustExec(t, pool, `
INSERT INTO workspace_teams (
    id, workspace_id, handle, display_name, description, visibility, status,
    created_by_member_id, created_at, updated_at, active_handle_key
) VALUES ($1, $2, 'delete-agent-team', 'delete-agent-team', '', 'private', 'active', $3, $4, $4, $5)
`, teamID, fixture.WorkspaceID, fixture.MemberID, now, fixture.WorkspaceID+":delete-agent-team")
	mustExec(t, pool, `
INSERT INTO workspace_team_members (
    id, workspace_id, team_id, actor_kind, actor_id, role, role_label, role_overlay_prompt,
    role_overlay_version, wake_on_team_mention, sort_order, added_by_member_id, added_at, updated_at,
    removed_at, active_member_key, active_agent_team_key, active_leader_key
) VALUES
    ('wtm_delete_agent_active', $1, $2, 'agent', $3, 'leader', '', '', 1, TRUE, 10, $4, $5, $5,
     NULL, $6, $7, $8),
    ('wtm_delete_agent_history', $1, $2, 'agent', $3, 'member', '', '', 1, TRUE, 20, $4, $5, $5,
     $9, NULL, NULL, NULL)
`, fixture.WorkspaceID, teamID, created.MemberID, fixture.MemberID, now,
		teamID+":agent:"+created.MemberID,
		fixture.WorkspaceID+":agent:"+created.MemberID,
		fixture.WorkspaceID+":"+teamID,
		now.Add(-time.Minute))

	_, err = store.DeleteAgentForWorkspace(ctx, fixture.UserID, fixture.WorkspaceID, created.AgentID)
	require.NoError(t, err)

	var activeRemovedAt sql.NullTime
	var activeMemberKey, activeAgentTeamKey, activeLeaderKey sql.NullString
	var activeActorID, activeRoleOverlayPrompt string
	require.NoError(t, pool.QueryRow(ctx, `
SELECT removed_at, active_member_key, active_agent_team_key, active_leader_key, actor_id, role_overlay_prompt
FROM workspace_team_members
WHERE id = 'wtm_delete_agent_active'
`).Scan(&activeRemovedAt, &activeMemberKey, &activeAgentTeamKey, &activeLeaderKey, &activeActorID, &activeRoleOverlayPrompt))
	require.True(t, activeRemovedAt.Valid)
	require.False(t, activeMemberKey.Valid)
	require.False(t, activeAgentTeamKey.Valid)
	require.False(t, activeLeaderKey.Valid)
	require.Equal(t, created.MemberID, activeActorID)
	require.Empty(t, activeRoleOverlayPrompt)

	var historicalRemovedAt time.Time
	require.NoError(t, pool.QueryRow(ctx, `
SELECT removed_at
FROM workspace_team_members
WHERE id = 'wtm_delete_agent_history'
`).Scan(&historicalRemovedAt))
	require.Equal(t, now.Add(-time.Minute), historicalRemovedAt.UTC())

	var memberCount, agentCount, activeTeamMembershipCount int
	require.NoError(t, pool.QueryRow(ctx, `SELECT COUNT(*) FROM members WHERE id = $1`, created.MemberID).Scan(&memberCount))
	require.NoError(t, pool.QueryRow(ctx, `SELECT COUNT(*) FROM agents WHERE id = $1`, created.AgentID).Scan(&agentCount))
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM workspace_team_members
WHERE workspace_id = $1 AND actor_kind = 'agent' AND actor_id = $2 AND removed_at IS NULL
`, fixture.WorkspaceID, created.MemberID).Scan(&activeTeamMembershipCount))
	require.Equal(t, 1, memberCount)
	require.Zero(t, agentCount)
	require.Zero(t, activeTeamMembershipCount)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	now := time.Unix(300, 0).UTC()
	fixture := seedAppAdminFixture(t, pool, "delete_agent_dm_inbox", now)
	seedMachineForRuntimeList(t, pool, fixture.WorkspaceID, fixture.UserID, "mac_delete_agent_dm_inbox", now)

	store := New(pool, Options{})
	created, err := store.CreateAgentForUser(ctx, fixture.UserID, fixture.WorkspaceID, "mac_delete_agent_dm_inbox", agent.CreateAgentParams{
		Name:        "dm-helper",
		DisplayName: "DM Helper",
		Runtime:     "codex",
		Model:       "gpt-5.4",
	})
	require.NoError(t, err)

	dmScopeID := "dm_delete_agent_dm_inbox"
	dmConversationID := "dm:" + dmScopeID
	channelConversationID := "channel:" + fixture.AllChannelID
	otherDMScopeID := "dm_delete_agent_dm_inbox_other"
	otherDMConversationID := "dm:" + otherDMScopeID

	mustExec(t, pool, `
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)
`, dmScopeID, fixture.WorkspaceID, "dm:"+fixture.WorkspaceID+":delete-agent-dm-inbox", now, fixture.MemberID)
	require.NoError(t, message.EnsureScopeMembership(ctx, pool, dmScopeID, fixture.MemberID, "public", "", "active", 0, 0, now))
	require.NoError(t, message.EnsureScopeMembership(ctx, pool, dmScopeID, created.MemberID, "public", "", "auto-wake", 0, 0, now))

	mustExec(t, pool, `
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)
`, otherDMScopeID, fixture.WorkspaceID, "dm:"+fixture.WorkspaceID+":delete-agent-dm-inbox-other", now, fixture.MemberID)
	require.NoError(t, message.EnsureScopeMembership(ctx, pool, otherDMScopeID, fixture.MemberID, "public", "", "active", 0, 0, now))

	imStore := im.New(pool)
	require.NoError(t, imStore.UpsertConversation(ctx, fixture.WorkspaceID, fixture.MemberID, im.ConversationIndexItem{
		ConversationID: dmConversationID,
		ScopeID:        dmScopeID,
		Kind:           "dm",
		SortAt:         now,
		LastEventSeq:   1,
		Projection: map[string]any{
			"id":       dmConversationID,
			"scopeId":  dmScopeID,
			"kind":     "agentDm",
			"title":    "DM Helper",
			"peerType": "agent",
			"peerId":   created.AgentID,
		},
	}))
	require.NoError(t, imStore.UpsertConversation(ctx, fixture.WorkspaceID, created.MemberID, im.ConversationIndexItem{
		ConversationID: dmConversationID,
		ScopeID:        dmScopeID,
		Kind:           "dm",
		SortAt:         now,
		LastEventSeq:   1,
		Projection: map[string]any{
			"id":      dmConversationID,
			"scopeId": dmScopeID,
			"kind":    "humanDm",
			"title":   fixture.MemberID,
		},
	}))
	require.NoError(t, imStore.UpsertConversation(ctx, fixture.WorkspaceID, fixture.MemberID, im.ConversationIndexItem{
		ConversationID: channelConversationID,
		ScopeID:        fixture.AllChannelID,
		Kind:           "channel",
		SortAt:         now,
		LastEventSeq:   1,
		Projection: map[string]any{
			"id":      channelConversationID,
			"scopeId": fixture.AllChannelID,
			"kind":    "channel",
			"title":   "all",
		},
	}))
	require.NoError(t, imStore.UpsertConversation(ctx, fixture.WorkspaceID, fixture.MemberID, im.ConversationIndexItem{
		ConversationID: otherDMConversationID,
		ScopeID:        otherDMScopeID,
		Kind:           "dm",
		SortAt:         now,
		LastEventSeq:   1,
		Projection: map[string]any{
			"id":      otherDMConversationID,
			"scopeId": otherDMScopeID,
			"kind":    "humanDm",
			"title":   "Other DM",
		},
	}))

	require.NoError(t, imStore.AppendEvents(ctx, fixture.WorkspaceID, fixture.MemberID, []im.StreamEvent{
		{
			EventID:        "imevt_delete_agent_dm_inbox_deleted",
			Type:           "conversation.created",
			ScopeID:        dmScopeID,
			ConversationID: dmConversationID,
			Payload:        map[string]any{"conversationId": dmConversationID},
			CreatedAt:      now,
		},
		{
			EventID:        "imevt_delete_agent_dm_inbox_channel",
			Type:           "conversation.created",
			ScopeID:        fixture.AllChannelID,
			ConversationID: channelConversationID,
			Payload:        map[string]any{"conversationId": channelConversationID},
			CreatedAt:      now,
		},
	}))

	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', 'hello deleted agent', '{}', $5, $5)
`, "msg_delete_agent_dm_inbox", fixture.WorkspaceID, dmScopeID, fixture.MemberID, now)

	_, err = store.DeleteAgentForWorkspace(ctx, fixture.UserID, fixture.WorkspaceID, created.AgentID)
	require.NoError(t, err)

	var deletedDMIndexRows, channelIndexRows, otherDMIndexRows int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM im_conversation_index
WHERE workspace_id = $1 AND scope_id = $2
`, fixture.WorkspaceID, dmScopeID).Scan(&deletedDMIndexRows))
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM im_conversation_index
WHERE workspace_id = $1 AND conversation_id = $2
`, fixture.WorkspaceID, channelConversationID).Scan(&channelIndexRows))
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM im_conversation_index
WHERE workspace_id = $1 AND conversation_id = $2
`, fixture.WorkspaceID, otherDMConversationID).Scan(&otherDMIndexRows))
	require.Zero(t, deletedDMIndexRows)
	require.Equal(t, 1, channelIndexRows)
	require.Equal(t, 1, otherDMIndexRows)

	var dmStreamEvents, channelStreamEvents int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM im_stream_events
WHERE workspace_id = $1 AND scope_id = $2
`, fixture.WorkspaceID, dmScopeID).Scan(&dmStreamEvents))
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM im_stream_events
WHERE workspace_id = $1 AND scope_id = $2
`, fixture.WorkspaceID, fixture.AllChannelID).Scan(&channelStreamEvents))
	require.Equal(t, 1, dmStreamEvents, "agent deletion must not rewrite the member-local stream ledger")
	require.Equal(t, 1, channelStreamEvents)

	var dmMessageCount int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM scope_messages
WHERE workspace_id = $1 AND scope_id = $2
`, fixture.WorkspaceID, dmScopeID).Scan(&dmMessageCount))
	require.Equal(t, 1, dmMessageCount, "agent deletion must not delete DM transcript history")
}

type failingAgentDeleteDB struct {
	coredb.DB
}

func (db *failingAgentDeleteDB) BeginTx(ctx context.Context, options coredb.TxOptions) (coredb.Tx, error) {
	tx, err := db.DB.BeginTx(ctx, options)
	if err != nil {
		return nil, err
	}
	return &failingAgentDeleteTx{Tx: tx}, nil
}

type failingAgentDeleteTx struct {
	coredb.Tx
}

func (tx *failingAgentDeleteTx) Exec(ctx context.Context, query string, args ...any) (coredb.Result, error) {
	if strings.Contains(query, "DELETE FROM agents") {
		return nil, errors.New("injected agent delete failure")
	}
	return tx.Tx.Exec(ctx, query, args...)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	now := time.Unix(200, 0).UTC()
	fixture := seedAppAdminFixture(t, pool, "delete_agent_team_rollback", now)
	seedMachineForRuntimeList(t, pool, fixture.WorkspaceID, fixture.UserID, "mac_delete_agent_team_rollback", now)

	baseStore := New(pool, Options{})
	created, err := baseStore.CreateAgentForUser(ctx, fixture.UserID, fixture.WorkspaceID, "mac_delete_agent_team_rollback", agent.CreateAgentParams{
		Name:        "rollback-helper",
		DisplayName: "Rollback Helper",
		Runtime:     "codex",
		Model:       "gpt-5.4",
	})
	require.NoError(t, err)

	teamID := "wst_delete_agent_team_rollback"
	mustExec(t, pool, `
INSERT INTO workspace_teams (
    id, workspace_id, handle, display_name, description, visibility, status,
    created_by_member_id, created_at, updated_at, active_handle_key
) VALUES ($1, $2, 'delete-agent-rollback', 'delete-agent-rollback', '', 'private', 'active', $3, $4, $4, $5)
`, teamID, fixture.WorkspaceID, fixture.MemberID, now, fixture.WorkspaceID+":delete-agent-rollback")
	mustExec(t, pool, `
INSERT INTO workspace_team_members (
    id, workspace_id, team_id, actor_kind, actor_id, role, role_label, role_overlay_prompt,
    role_overlay_version, wake_on_team_mention, sort_order, added_by_member_id, added_at, updated_at,
    removed_at, active_member_key, active_agent_team_key, active_leader_key
) VALUES (
    'wtm_delete_agent_rollback', $1, $2, 'agent', $3, 'leader', '', '', 1, TRUE, 10, $4, $5, $5,
    NULL, $6, $7, $8
)
`, fixture.WorkspaceID, teamID, created.MemberID, fixture.MemberID, now,
		teamID+":agent:"+created.MemberID,
		fixture.WorkspaceID+":agent:"+created.MemberID,
		fixture.WorkspaceID+":"+teamID)

	dmScopeID := "dm_delete_agent_team_rollback"
	dmConversationID := "dm:" + dmScopeID
	mustExec(t, pool, `
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)
`, dmScopeID, fixture.WorkspaceID, "dm:"+fixture.WorkspaceID+":delete-agent-rollback", now, fixture.MemberID)
	require.NoError(t, message.EnsureScopeMembership(ctx, pool, dmScopeID, fixture.MemberID, "public", "", "active", 0, 0, now))
	require.NoError(t, message.EnsureScopeMembership(ctx, pool, dmScopeID, created.MemberID, "public", "", "auto-wake", 0, 0, now))
	require.NoError(t, im.New(pool).UpsertConversation(ctx, fixture.WorkspaceID, fixture.MemberID, im.ConversationIndexItem{
		ConversationID: dmConversationID,
		ScopeID:        dmScopeID,
		Kind:           "dm",
		SortAt:         now,
		Projection: map[string]any{
			"id":       dmConversationID,
			"scopeId":  dmScopeID,
			"kind":     "agentDm",
			"title":    "Rollback Helper",
			"peerType": "agent",
			"peerId":   created.AgentID,
		},
	}))

	store := New(&failingAgentDeleteDB{DB: pool}, Options{})
	_, err = store.DeleteAgentForWorkspace(ctx, fixture.UserID, fixture.WorkspaceID, created.AgentID)
	require.EqualError(t, err, "delete agent: injected agent delete failure")

	var agentCount, activeTeamMembershipCount, dmInboxProjectionCount int
	require.NoError(t, pool.QueryRow(ctx, `SELECT COUNT(*) FROM agents WHERE id = $1`, created.AgentID).Scan(&agentCount))
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM workspace_team_members
WHERE id = 'wtm_delete_agent_rollback'
  AND removed_at IS NULL
  AND active_member_key IS NOT NULL
  AND active_agent_team_key IS NOT NULL
  AND active_leader_key IS NOT NULL
`).Scan(&activeTeamMembershipCount))
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM im_conversation_index
WHERE workspace_id = $1 AND conversation_id = $2
`, fixture.WorkspaceID, dmConversationID).Scan(&dmInboxProjectionCount))
	require.Equal(t, 1, agentCount)
	require.Equal(t, 1, activeTeamMembershipCount)
	require.Equal(t, 1, dmInboxProjectionCount)
}
