package message

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
)

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

	candidates := []ScopeMessageVisibilityCandidate{
		scopeVisibilityCandidateWithReasonForTest("msg-owner", "owner", 3, ScopeMembershipRecord{ScopeID: "scope-visibility", MemberID: "viewer", Visibility: "public", EffectiveFromSeq: 1}, ScopeVisibilityReasonMembership),
		scopeVisibilityCandidateWithReasonForTest("msg-private-output", "viewer", 2, ScopeMembershipRecord{ScopeID: "scope-visibility", MemberID: "viewer", Visibility: "private", OwnerMemberID: "owner", EffectiveFromSeq: 1}, ScopeVisibilityReasonPrivateOutput),
		scopeVisibilityCandidateWithReasonAndGrantForTest("msg-grant", "grantee", 1, ScopeMembershipRecord{ScopeID: "scope-visibility", MemberID: "viewer", Visibility: "private", OwnerMemberID: "owner", EffectiveFromSeq: 5}, ScopeVisibilityReasonPrivateGrant, "grant-sql"),
		scopeVisibilityCandidateWithReasonAndGrantForTest("msg-grant", "grantee", 1, ScopeMembershipRecord{ScopeID: "scope-visibility", MemberID: "viewer", Visibility: "private", OwnerMemberID: "owner", EffectiveFromSeq: 5}, ScopeVisibilityReasonPrivateGrant, "grant-sql-duplicate"),
	}

	result := NewVisibilityFilter().FilterCandidates(candidates, VisibilityFilterRequest{
		ViewerMemberID: "viewer",
		ViewerType:     ScopeActorAgent,
		ScopeID:        "scope-visibility",
		RequestKind:    ScopeVisibilityRequestHistory,
	})

	require.Equal(t, []string{"msg-owner", "msg-private-output", "msg-grant"}, visibleMessageRefIDs(result.Messages))
	require.Equal(t, ScopeVisibilityReasonMembership, result.Messages[0].Reason)
	require.Equal(t, ScopeVisibilityReasonPrivateOutput, result.Messages[1].Reason)
	require.Equal(t, ScopeVisibilityReasonPrivateGrant, result.Messages[2].Reason)
	require.Equal(t, "grant-sql", result.Messages[2].GrantID)
	require.Equal(t, int64(1), result.NextCursor)
}

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

	candidates := []ScopeMessageVisibilityCandidate{
		scopeVisibilityCandidateForTest("msg-no-reason", "viewer", 3, ScopeMembershipRecord{ScopeID: "scope-visibility", MemberID: "viewer", Visibility: "public", EffectiveFromSeq: 1}),
		scopeVisibilityCandidateWithReasonForTest("msg-unknown-reason", "viewer", 2, ScopeMembershipRecord{ScopeID: "scope-visibility", MemberID: "viewer", Visibility: "public", EffectiveFromSeq: 1}, "new_unreviewed_reason"),
		scopeVisibilityCandidateWithReasonForTest("msg-wrong-viewer", "viewer", 1, ScopeMembershipRecord{ScopeID: "scope-visibility", MemberID: "other-viewer", Visibility: "public", EffectiveFromSeq: 1}, ScopeVisibilityReasonMembership),
	}

	result := NewVisibilityFilter().FilterCandidates(candidates, VisibilityFilterRequest{
		ViewerMemberID: "viewer",
		ViewerType:     ScopeActorAgent,
		ScopeID:        "scope-visibility",
		RequestKind:    ScopeVisibilityRequestHistory,
	})

	require.Empty(t, result.Messages)
	require.Zero(t, result.NextCursor)
}

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

	candidate := scopeVisibilityCandidateWithReasonForTest(
		"msg-grant-no-id",
		"grantee",
		4,
		ScopeMembershipRecord{ScopeID: "scope-visibility", MemberID: "private-agent", Visibility: "private", OwnerMemberID: "owner", EffectiveFromSeq: 5},
		ScopeVisibilityReasonPrivateGrant,
	)

	result := NewVisibilityFilter().FilterCandidates([]ScopeMessageVisibilityCandidate{candidate}, VisibilityFilterRequest{
		ViewerMemberID: "private-agent",
		ViewerType:     ScopeActorPrivateAgent,
		ScopeID:        "scope-visibility",
		RequestKind:    ScopeVisibilityRequestHistory,
	})

	require.Empty(t, result.Messages)
}

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

	ctx := context.Background()
	db := startMessagingTestPool(t, ctx)
	now := scopeVisibilityNowForTest()
	seedScopeVisibilityStoreData(t, ctx, db, now)

	result, err := NewVisibilityFilter().ListVisibleMessages(ctx, NewScopeRepositories(db).Messages, VisibilityFilterRequest{
		ViewerMemberID: "mem-visibility-viewer",
		ViewerType:     ScopeActorHuman,
		ScopeID:        "scope-visibility",
		RequestKind:    ScopeVisibilityRequestHistory,
		Limit:          2,
	})
	require.NoError(t, err)
	require.Len(t, result.Messages, 2)
	require.Equal(t, "msg-visibility-4", result.Messages[0].Message.ID)
	require.Equal(t, "msg-visibility-3", result.Messages[1].Message.ID)
	require.Equal(t, int64(3), result.NextCursor)
	require.True(t, result.HasMore)
}

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

	ctx := context.Background()
	db := startMessagingTestPool(t, ctx)
	now := scopeVisibilityNowForTest()
	seedScopeVisibilityPrivateStoreData(t, ctx, db, now)

	requestKinds := []string{
		ScopeVisibilityRequestHistory,
		ScopeVisibilityRequestSearch,
		ScopeVisibilityRequestRootContext,
		ScopeVisibilityRequestSourceRef,
		ScopeVisibilityRequestAttachment,
		ScopeVisibilityRequestDiagnostics,
	}
	for _, requestKind := range requestKinds {
		t.Run(requestKind, func(t *testing.T) {
			result, err := NewVisibilityFilter().ListVisibleMessages(ctx, NewScopeRepositories(db).Messages, VisibilityFilterRequest{
				ViewerMemberID: "mem-visibility-private",
				ViewerType:     ScopeActorPrivateAgent,
				ScopeID:        "scope-visibility-private",
				RequestKind:    requestKind,
				BeforeSeq:      5,
				Limit:          1,
			})

			require.NoError(t, err)
			require.Len(t, result.Messages, 1)
			require.Equal(t, "msg-private-4", result.Messages[0].Message.ID)
			require.Equal(t, ScopeVisibilityReasonPrivateGrant, result.Messages[0].Reason)
			require.NotEmpty(t, result.Messages[0].GrantID)
		})
	}
}

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

	ctx := context.Background()
	db := startMessagingTestPool(t, ctx)
	now := scopeVisibilityNowForTest()
	seedScopeVisibilityPrivateStoreData(t, ctx, db, now)

	result, err := NewVisibilityFilter().ListVisibleMessages(ctx, NewScopeRepositories(db).Messages, VisibilityFilterRequest{
		ViewerMemberID: "mem-visibility-private",
		ViewerType:     ScopeActorPrivateAgent,
		ScopeID:        "scope-visibility-private",
		RequestKind:    ScopeVisibilityRequestHistory,
		Limit:          2,
	})
	require.NoError(t, err)
	require.Len(t, result.Messages, 2)
	require.Equal(t, "msg-private-4", result.Messages[0].Message.ID)
	require.Equal(t, ScopeVisibilityReasonPrivateGrant, result.Messages[0].Reason)
	require.Contains(t, []string{"grant-visibility-private", "grant-visibility-private-duplicate"}, result.Messages[0].GrantID)
	require.Equal(t, "msg-private-3", result.Messages[1].Message.ID)
	require.Equal(t, ScopeVisibilityReasonPrivateGrant, result.Messages[1].Reason)
	require.Contains(t, []string{"grant-visibility-private", "grant-visibility-private-duplicate"}, result.Messages[1].GrantID)
	require.Equal(t, int64(3), result.NextCursor)
	require.True(t, result.HasMore)
}

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

	ctx := context.Background()
	db := startMessagingTestPool(t, ctx)
	now := scopeVisibilityNowForTest()
	seedScopeVisibilityPrivateStoreData(t, ctx, db, now)

	result, err := NewVisibilityFilter().ListVisibleMessages(ctx, NewScopeRepositories(db).Messages, VisibilityFilterRequest{
		ViewerMemberID:      "mem-visibility-private",
		ViewerType:          ScopeActorPrivateAgent,
		ScopeID:             "scope-visibility-private",
		RequestKind:         ScopeVisibilityRequestHistory,
		Limit:               2,
		CandidateBatchLimit: 2,
		MaxCandidateBatches: 1,
	})
	require.NoError(t, err)
	require.Len(t, result.Messages, 1)
	require.Equal(t, "msg-private-4", result.Messages[0].Message.ID)
	require.Equal(t, ScopeVisibilityReasonPrivateGrant, result.Messages[0].Reason)
	require.Equal(t, int64(4), result.NextCursor)
	require.True(t, result.HasMore)
	require.True(t, result.CandidateBudgetExhausted)

	continued, err := NewVisibilityFilter().ListVisibleMessages(ctx, NewScopeRepositories(db).Messages, VisibilityFilterRequest{
		ViewerMemberID:      "mem-visibility-private",
		ViewerType:          ScopeActorPrivateAgent,
		ScopeID:             "scope-visibility-private",
		RequestKind:         ScopeVisibilityRequestHistory,
		BeforeSeq:           result.NextCursor,
		Limit:               2,
		CandidateBatchLimit: 2,
		MaxCandidateBatches: 1,
	})
	require.NoError(t, err)
	require.Len(t, continued.Messages, 1)
	require.Equal(t, "msg-private-3", continued.Messages[0].Message.ID)
	require.Equal(t, ScopeVisibilityReasonPrivateGrant, continued.Messages[0].Reason)
	require.Equal(t, int64(3), continued.NextCursor)
	require.True(t, continued.HasMore)
	require.True(t, continued.CandidateBudgetExhausted)

	finalPage, err := NewVisibilityFilter().ListVisibleMessages(ctx, NewScopeRepositories(db).Messages, VisibilityFilterRequest{
		ViewerMemberID:      "mem-visibility-private",
		ViewerType:          ScopeActorPrivateAgent,
		ScopeID:             "scope-visibility-private",
		RequestKind:         ScopeVisibilityRequestHistory,
		BeforeSeq:           continued.NextCursor,
		Limit:               2,
		CandidateBatchLimit: 2,
		MaxCandidateBatches: 1,
	})
	require.NoError(t, err)
	require.Empty(t, finalPage.Messages)
	require.Equal(t, int64(0), finalPage.NextCursor)
	require.False(t, finalPage.HasMore)
	require.False(t, finalPage.CandidateBudgetExhausted)
}

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

	ctx := context.Background()
	db := startMessagingTestPool(t, ctx)
	now := scopeVisibilityNowForTest()
	records := seedScopeVisibilityPrivateStoreData(t, ctx, db, now)
	membership := ScopeMembershipRecord{
		ScopeID:          "scope-visibility-private",
		MemberID:         "mem-visibility-private",
		Visibility:       "private",
		OwnerMemberID:    "mem-visibility-private-owner",
		EffectiveFromSeq: 1,
	}

	expectedIDs := make([]string, 0)
	expectedReasons := make(map[string]string)
	for _, record := range records {
		candidate := ScopeMessageVisibilityCandidate{
			Message:    record,
			Membership: membership,
		}
		switch record.ID {
		case "msg-private-3", "msg-private-4":
			candidate.GrantID = "grant-visibility-private"
		}
		visible, reason := legacyVisibleByMembershipForTest(candidate, ScopeActorPrivateAgent)
		if !visible {
			continue
		}
		expectedIDs = append([]string{record.ID}, expectedIDs...)
		expectedReasons[record.ID] = reason
	}

	result, err := NewVisibilityFilter().ListVisibleMessages(ctx, NewScopeRepositories(db).Messages, VisibilityFilterRequest{
		ViewerMemberID: "mem-visibility-private",
		ViewerType:     ScopeActorPrivateAgent,
		ScopeID:        "scope-visibility-private",
		RequestKind:    ScopeVisibilityRequestHistory,
		Limit:          20,
	})
	require.NoError(t, err)
	gotIDs := make([]string, 0, len(result.Messages))
	for _, ref := range result.Messages {
		gotIDs = append(gotIDs, ref.Message.ID)
		require.Equal(t, expectedReasons[ref.Message.ID], ref.Reason)
		require.NotEmpty(t, ref.GrantID)
	}
	require.Equal(t, expectedIDs, gotIDs)
}

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

	ctx := context.Background()
	db := startMessagingTestPool(t, ctx)
	now := scopeVisibilityNowForTest()
	records := seedScopeVisibilityPrivateStoreData(t, ctx, db, now)
	backfillRecord := records[2]
	fromSeq := int64(5)
	_, err := db.Exec(ctx, `
UPDATE scope_memberships
SET effective_from_seq = $3
WHERE scope_id = $1 AND member_id = $2
`, "scope-visibility-private", "mem-visibility-private", fromSeq)
	require.NoError(t, err)

	candidates, err := NewScopeRepositories(db).Messages.LoadVisibilityCandidatesByMessageID(ctx, "workspace-visibility-private", "mem-visibility-private", backfillRecord.ID)
	require.NoError(t, err)
	require.NotEmpty(t, candidates)
	require.Equal(t, ScopeVisibilityReasonPrivateGrant, candidates[0].Reason)
	require.NotEmpty(t, candidates[0].GrantID)
	require.False(t, membershipCoversMessage(candidates[0].Membership, candidates[0].Message.Seq))

	history := NewVisibilityFilter().FilterCandidates(candidates, VisibilityFilterRequest{
		ViewerMemberID: "mem-visibility-private",
		ViewerType:     ScopeActorPrivateAgent,
		ScopeID:        "scope-visibility-private",
		RequestKind:    ScopeVisibilityRequestHistory,
	})
	require.Len(t, history.Messages, 1)

	rootContext := NewVisibilityFilter().FilterCandidates(candidates, VisibilityFilterRequest{
		ViewerMemberID: "mem-visibility-private",
		ViewerType:     ScopeActorPrivateAgent,
		ScopeID:        "scope-visibility-private",
		RequestKind:    ScopeVisibilityRequestRootContext,
	})
	require.Len(t, rootContext.Messages, 1)
}

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

	ctx := context.Background()
	db := startMessagingTestPool(t, ctx)
	now := scopeVisibilityNowForTest()

	insertMessagingWorkspace(t, ctx, db, "workspace-visibility-human-private", now)
	insertMessagingUser(t, ctx, db, "user-visibility-human-private-owner", "scope-visibility-human-private-owner@example.com", now)
	insertMessagingHumanMember(t, ctx, db, "mem-visibility-human-private-owner", "workspace-visibility-human-private", "user-visibility-human-private-owner", now)
	insertMessagingUser(t, ctx, db, "user-visibility-human-private-viewer", "scope-visibility-human-private-viewer@example.com", now)
	insertMessagingHumanMember(t, ctx, db, "mem-visibility-human-private-viewer", "workspace-visibility-human-private", "user-visibility-human-private-viewer", now)

	repos := NewScopeRepositories(db)
	require.NoError(t, repos.Scopes.UpsertScope(ctx, ScopeRecord{
		ID:                "scope-visibility-human-private",
		WorkspaceID:       "workspace-visibility-human-private",
		Type:              "channel",
		IdentityKey:       "channel:scope-visibility-human-private",
		LatestSeq:         1,
		LatestGlobalSeq:   1,
		LatestMessageAt:   &now,
		Status:            "active",
		CreatedByMemberID: "mem-visibility-human-private-owner",
		CreatedAt:         now,
		UpdatedAt:         now,
	}))
	require.NoError(t, repos.Memberships.Upsert(ctx, ScopeMembershipRecord{
		ScopeID:          "scope-visibility-human-private",
		MemberID:         "mem-visibility-human-private-viewer",
		Visibility:       "private",
		OwnerMemberID:    "mem-visibility-human-private-owner",
		EffectiveFromSeq: 1,
		CreatedAt:        now,
		UpdatedAt:        now,
	}))
	require.NoError(t, repos.Messages.Insert(ctx, ScopeMessageRecord{
		ID:                 "msg-visibility-human-private-owner",
		WorkspaceID:        "workspace-visibility-human-private",
		ScopeID:            "scope-visibility-human-private",
		Seq:                1,
		WorkspaceGlobalSeq: 1,
		AuthorMemberID:     "mem-visibility-human-private-owner",
		AuthorType:         ScopeActorHuman,
		MessageType:        "text",
		Content:            "owner message",
		CreatedAt:          now,
		UpdatedAt:          now,
	}))

	result, err := NewVisibilityFilter().ListVisibleMessages(ctx, repos.Messages, VisibilityFilterRequest{
		ViewerMemberID: "mem-visibility-human-private-viewer",
		ViewerType:     ScopeActorHuman,
		ScopeID:        "scope-visibility-human-private",
		RequestKind:    ScopeVisibilityRequestHistory,
		Limit:          10,
	})
	require.NoError(t, err)
	require.Empty(t, result.Messages)
}

func scopeVisibilityCandidateForTest(messageID string, authorMemberID string, seq int64, membership ScopeMembershipRecord) ScopeMessageVisibilityCandidate {
	return ScopeMessageVisibilityCandidate{
		Message: ScopeMessageRecord{
			ID:                 messageID,
			WorkspaceID:        "workspace-visibility",
			ScopeID:            "scope-visibility",
			Seq:                seq,
			WorkspaceGlobalSeq: seq,
			AuthorMemberID:     authorMemberID,
			AuthorType:         ScopeActorHuman,
			MessageType:        "text",
			Content:            messageID,
			CreatedAt:          scopeVisibilityNowForTest(),
			UpdatedAt:          scopeVisibilityNowForTest(),
		},
		Membership: membership,
	}
}

func scopeVisibilityCandidateWithReasonForTest(messageID string, authorMemberID string, seq int64, membership ScopeMembershipRecord, reason string) ScopeMessageVisibilityCandidate {
	candidate := scopeVisibilityCandidateForTest(messageID, authorMemberID, seq, membership)
	candidate.Reason = reason
	return candidate
}

func scopeVisibilityCandidateWithReasonAndGrantForTest(messageID string, authorMemberID string, seq int64, membership ScopeMembershipRecord, reason string, grantID string) ScopeMessageVisibilityCandidate {
	candidate := scopeVisibilityCandidateWithReasonForTest(messageID, authorMemberID, seq, membership, reason)
	candidate.GrantID = grantID
	return candidate
}

func legacyVisibleByMembershipForTest(candidate ScopeMessageVisibilityCandidate, viewerType string) (bool, string) {
	message := candidate.Message
	membership := candidate.Membership
	coversMessage := membershipCoversMessage(membership, message.Seq)
	privateAuthorityBackfill := legacyPrivateReadAuthorityBackfillCandidateForTest(candidate, viewerType)
	if !coversMessage && !privateAuthorityBackfill {
		return false, ""
	}
	if coversMessage && message.AuthorMemberID == membership.MemberID && viewerType == ScopeActorPrivateAgent {
		return true, ScopeVisibilityReasonPrivateOutput
	}
	switch membership.Visibility {
	case "public":
		return true, ScopeVisibilityReasonMembership
	case "private":
		if viewerType != ScopeActorPrivateAgent {
			return false, ""
		}
		if message.AuthorMemberID == membership.OwnerMemberID && membership.OwnerMemberID != "" {
			return true, ScopeVisibilityReasonAuthorInterval
		}
		if candidate.GrantID != "" {
			return true, ScopeVisibilityReasonPrivateGrant
		}
		return false, ""
	default:
		return false, ""
	}
}

func legacyPrivateReadAuthorityBackfillCandidateForTest(candidate ScopeMessageVisibilityCandidate, viewerType string) bool {
	membership := candidate.Membership
	if viewerType != ScopeActorPrivateAgent || membership.Visibility != "private" {
		return false
	}
	if candidate.GrantID == "" || membership.RemovedAt != nil {
		return false
	}
	if membership.EffectiveToSeq != nil && candidate.Message.Seq > *membership.EffectiveToSeq {
		return false
	}
	return true
}

func seedScopeVisibilityStoreData(t *testing.T, ctx context.Context, db coredb.QueryExecer, now time.Time) {
	t.Helper()

	insertMessagingWorkspace(t, ctx, db, "workspace-visibility", now)
	insertMessagingUser(t, ctx, db, "user-visibility-owner", "scope-visibility-owner@example.com", now)
	insertMessagingHumanMember(t, ctx, db, "mem-visibility-owner", "workspace-visibility", "user-visibility-owner", now)
	insertMessagingUser(t, ctx, db, "user-visibility-viewer", "scope-visibility-viewer@example.com", now)
	insertMessagingHumanMember(t, ctx, db, "mem-visibility-viewer", "workspace-visibility", "user-visibility-viewer", now)

	repos := NewScopeRepositories(db)
	require.NoError(t, repos.Scopes.UpsertScope(ctx, ScopeRecord{
		ID:                "scope-visibility",
		WorkspaceID:       "workspace-visibility",
		Type:              "channel",
		IdentityKey:       "channel:scope-visibility",
		LatestSeq:         4,
		LatestGlobalSeq:   4,
		LatestMessageAt:   &now,
		Status:            "active",
		CreatedByMemberID: "mem-visibility-owner",
		CreatedAt:         now,
		UpdatedAt:         now,
	}))
	require.NoError(t, repos.Memberships.Upsert(ctx, ScopeMembershipRecord{
		ScopeID:          "scope-visibility",
		MemberID:         "mem-visibility-viewer",
		Visibility:       "public",
		EffectiveFromSeq: 2,
		CreatedAt:        now,
		UpdatedAt:        now,
	}))
	for seq := int64(1); seq <= 4; seq++ {
		require.NoError(t, repos.Messages.Insert(ctx, ScopeMessageRecord{
			ID:                 "msg-visibility-" + string(rune('0'+seq)),
			WorkspaceID:        "workspace-visibility",
			ScopeID:            "scope-visibility",
			Seq:                seq,
			WorkspaceGlobalSeq: seq,
			AuthorMemberID:     "mem-visibility-owner",
			AuthorType:         ScopeActorHuman,
			MessageType:        "text",
			Content:            "message",
			CreatedAt:          now.Add(time.Duration(seq) * time.Second),
			UpdatedAt:          now.Add(time.Duration(seq) * time.Second),
		}))
	}
}

func seedScopeVisibilityPrivateStoreData(t *testing.T, ctx context.Context, db coredb.QueryExecer, now time.Time) []ScopeMessageRecord {
	t.Helper()

	insertMessagingWorkspace(t, ctx, db, "workspace-visibility-private", now)
	insertMessagingUser(t, ctx, db, "user-visibility-private-owner", "scope-visibility-private-owner@example.com", now)
	insertMessagingHumanMember(t, ctx, db, "mem-visibility-private-owner", "workspace-visibility-private", "user-visibility-private-owner", now)
	insertMessagingUser(t, ctx, db, "user-visibility-private-grantee", "scope-visibility-private-grantee@example.com", now)
	insertMessagingHumanMember(t, ctx, db, "mem-visibility-private-grantee", "workspace-visibility-private", "user-visibility-private-grantee", now)
	insertMessagingUser(t, ctx, db, "user-visibility-private-stranger", "scope-visibility-private-stranger@example.com", now)
	insertMessagingHumanMember(t, ctx, db, "mem-visibility-private-stranger", "workspace-visibility-private", "user-visibility-private-stranger", now)
	insertMessagingAgentMember(t, ctx, db, "mem-visibility-private", "workspace-visibility-private", now)

	repos := NewScopeRepositories(db)
	require.NoError(t, repos.Scopes.UpsertScope(ctx, ScopeRecord{
		ID:                "scope-visibility-private",
		WorkspaceID:       "workspace-visibility-private",
		Type:              "channel",
		IdentityKey:       "channel:scope-visibility-private",
		LatestSeq:         8,
		LatestGlobalSeq:   8,
		LatestMessageAt:   &now,
		Status:            "active",
		CreatedByMemberID: "mem-visibility-private-owner",
		CreatedAt:         now,
		UpdatedAt:         now,
	}))
	require.NoError(t, repos.Memberships.Upsert(ctx, ScopeMembershipRecord{
		ScopeID:          "scope-visibility-private",
		MemberID:         "mem-visibility-private",
		Visibility:       "private",
		OwnerMemberID:    "mem-visibility-private-owner",
		EffectiveFromSeq: 1,
		CreatedAt:        now,
		UpdatedAt:        now,
	}))
	revokedAt := now.Add(5 * time.Second)
	require.NoError(t, repos.PrivateVisibility.InsertGrant(ctx, ScopePrivateGrantRecord{
		ID:                "grant-visibility-private",
		WorkspaceID:       "workspace-visibility-private",
		ScopeID:           "scope-visibility-private",
		OwnerMemberID:     "mem-visibility-private-owner",
		GranteeMemberID:   "mem-visibility-private-grantee",
		GrantedByMemberID: "mem-visibility-private-owner",
		CreatedAt:         now.Add(2 * time.Second),
		RevokedAt:         &revokedAt,
	}))
	require.NoError(t, repos.PrivateVisibility.InsertGrant(ctx, ScopePrivateGrantRecord{
		ID:                "grant-visibility-private-duplicate",
		WorkspaceID:       "workspace-visibility-private",
		ScopeID:           "scope-visibility-private",
		OwnerMemberID:     "mem-visibility-private-owner",
		GranteeMemberID:   "mem-visibility-private-grantee",
		GrantedByMemberID: "mem-visibility-private-owner",
		CreatedAt:         now.Add(2 * time.Second),
		RevokedAt:         &revokedAt,
	}))

	authorsBySeq := map[int64]string{
		1: "mem-visibility-private-stranger",
		2: "mem-visibility-private-stranger",
		3: "mem-visibility-private-grantee",
		4: "mem-visibility-private-grantee",
		5: "mem-visibility-private-stranger",
		6: "mem-visibility-private-stranger",
		7: "mem-visibility-private-grantee",
		8: "mem-visibility-private-stranger",
	}
	records := make([]ScopeMessageRecord, 0, len(authorsBySeq))
	for seq := int64(1); seq <= 8; seq++ {
		authorMemberID := authorsBySeq[seq]
		record := ScopeMessageRecord{
			ID:                 "msg-private-" + string(rune('0'+seq)),
			WorkspaceID:        "workspace-visibility-private",
			ScopeID:            "scope-visibility-private",
			Seq:                seq,
			WorkspaceGlobalSeq: seq,
			AuthorMemberID:     authorMemberID,
			AuthorType:         ScopeActorHuman,
			MessageType:        "text",
			Content:            "message",
			CreatedAt:          now.Add(time.Duration(seq) * time.Second),
			UpdatedAt:          now.Add(time.Duration(seq) * time.Second),
		}
		require.NoError(t, repos.Messages.Insert(ctx, record))
		records = append(records, record)
	}
	return records
}

func scopeVisibilityNowForTest() time.Time {
	return time.Date(2026, 5, 13, 5, 0, 0, 0, time.UTC)
}
