package memory

import (
	"context"
	"fmt"
	"strings"
	"sync/atomic"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/AgentTeam/internal/store/resource/activity"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

func newTestStore(t *testing.T) *Store {
	t.Helper()
	db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(&Memory{}, &MemoryContentCheckpoint{}, &MemoryGrant{}, &MemoryPin{}, &AgentAssetAccessChangeOutbox{}, &activity.ResourceActivityEvent{}, &activity.ResourceActivityLatestCommit{}))
	return New(db)
}

func TestStoreListsMemoryCollectionWithLatestCommitTime(t *testing.T) {
	ctx := context.Background()
	store := newTestStore(t)
	base := time.Date(2026, 6, 24, 1, 0, 0, 0, time.UTC)
	metadataUpdate := base.Add(10 * time.Hour)
	latestCommit := base.Add(time.Hour)
	fallbackUpdate := base.Add(2 * time.Hour)

	_, err := store.CreateMemory(ctx, Memory{ID: "mem_commit", Name: "Commit-backed", IsActive: true, CreatedBy: "usr_1", UpdatedBy: "usr_1", CreatedAt: base, UpdatedAt: metadataUpdate})
	require.NoError(t, err)
	_, err = store.CreateMemory(ctx, Memory{ID: "mem_no_commit", Name: "Metadata fallback", IsActive: true, CreatedBy: "usr_1", UpdatedBy: "usr_1", CreatedAt: base, UpdatedAt: fallbackUpdate})
	require.NoError(t, err)
	require.NoError(t, store.db.Create(&activity.ResourceActivityLatestCommit{
		WorkspaceID: "ws-1",
		AssetType:   activity.AssetTypeMemory,
		AssetID:     "mem_commit",
		OccurredAt:  latestCommit,
		EventID:     "act_latest_commit",
	}).Error)
	require.NoError(t, store.db.Create(&activity.ResourceActivityLatestCommit{
		WorkspaceID: "ws-other",
		AssetType:   activity.AssetTypeMemory,
		AssetID:     "mem_commit",
		OccurredAt:  base.Add(20 * time.Hour),
		EventID:     "act_other_workspace_commit",
	}).Error)

	page, err := store.ListMemoryCollection(ctx, MemoryCollectionOptions{WorkspaceID: "ws-1", PrincipalID: "usr_1", View: "all", Page: 1, PerPage: 20})
	require.NoError(t, err)
	require.Len(t, page.Items, 2)
	require.Equal(t, "mem_no_commit", page.Items[0].ID, "memories without projected commits fall back to metadata updated_at for ordering")
	require.Equal(t, fallbackUpdate, page.Items[0].UpdatedAt)
	require.Equal(t, "mem_commit", page.Items[1].ID, "projected commit time should override metadata updated_at even when metadata is newer")
	require.Equal(t, latestCommit, page.Items[1].UpdatedAt)

	_, err = store.UpsertMemoryPin(ctx, MemoryPin{WorkspaceID: "ws-1", PrincipalID: "usr_1", MemoryID: "mem_commit", CreatedAt: base, UpdatedAt: base})
	require.NoError(t, err)
	pinned, err := store.ListMemoryCollection(ctx, MemoryCollectionOptions{WorkspaceID: "ws-1", PrincipalID: "usr_1", View: "pinned", Page: 1, PerPage: 20})
	require.NoError(t, err)
	require.Len(t, pinned.Items, 1)
	require.Equal(t, "mem_commit", pinned.Items[0].ID)
	require.Equal(t, latestCommit, pinned.Items[0].UpdatedAt, "pinned collection keeps pin ordering but still displays latest commit time")
}

func TestStoreCountsWorkspaceMemoriesByCreator(t *testing.T) {
	ctx := context.Background()
	store := newTestStore(t)
	now := time.Date(2026, 6, 26, 1, 0, 0, 0, time.UTC)
	deletedAt := now.Add(time.Hour)
	for _, memory := range []Memory{
		{ID: "mem_owned_1", Name: "Owned 1", IsActive: true, CreatedBy: "usr_1", UpdatedBy: "usr_1", CreatedAt: now, UpdatedAt: now},
		{ID: "mem_owned_2", Name: "Owned 2", IsActive: true, CreatedBy: "usr_1", UpdatedBy: "usr_1", CreatedAt: now, UpdatedAt: now},
		{ID: "mem_other", Name: "Other", IsActive: true, CreatedBy: "usr_2", UpdatedBy: "usr_2", CreatedAt: now, UpdatedAt: now},
		{ID: "mem_deleted", Name: "Deleted", IsActive: true, CreatedBy: "usr_1", UpdatedBy: "usr_1", CreatedAt: now, UpdatedAt: now, DeletedAt: &deletedAt},
		{ID: "mem_owned_3", Name: "Owned 3", IsActive: true, CreatedBy: "usr_1", UpdatedBy: "usr_1", CreatedAt: now, UpdatedAt: now},
	} {
		_, err := store.CreateMemory(ctx, memory)
		require.NoError(t, err)
	}

	total, err := store.CountWorkspaceMemories(ctx, "ws-1", "usr_1")
	require.NoError(t, err)
	require.EqualValues(t, 3, total)
}

// TestStoreListMemoryCollectionOrderByOnlyFullGroupBySafe guards against the
// regression fixed after the collection list returned HTTP 500 in production.
// The unit suite runs on SQLite, which (unlike MySQL's default sql_mode) does
// not enforce ONLY_FULL_GROUP_BY, so a bare, non-aggregated reference to the
// derived table latest_memory_commits.last_commit_at in ORDER BY passes tests
// yet fails on MySQL with Error 1055. This captures the rendered SQL and asserts
// the derived-table column stays aggregated.
func TestStoreListMemoryCollectionOrderByOnlyFullGroupBySafe(t *testing.T) {
	store := newTestStore(t)

	sqlStr := store.db.ToSQL(func(tx *gorm.DB) *gorm.DB {
		s := New(tx)
		var items []Memory
		return s.memoryListRowsQuery(context.Background(), MemoryCollectionOptions{
			WorkspaceID: "ws-1", PrincipalID: "usr_1", View: "all", Page: 1, PerPage: 20,
		}).Limit(20).Offset(0).Find(&items)
	})

	require.Contains(t, sqlStr, "GROUP BY `memories`.`id`", "collection list groups by memory id via the memory_grants join")
	require.Contains(t, sqlStr, "MAX(latest_memory_commits.last_commit_at)", "derived-table commit time must be aggregated in ORDER BY for ONLY_FULL_GROUP_BY")
	require.NotContains(t, sqlStr, "COALESCE(latest_memory_commits.last_commit_at", "a bare, non-aggregated derived-table column in ORDER BY trips MySQL Error 1055")
}

func TestMemoryListCommitQueryReadsLatestCommitProjection(t *testing.T) {
	store := newTestStore(t)

	sqlStr := store.db.ToSQL(func(tx *gorm.DB) *gorm.DB {
		s := New(tx)
		var items []Memory
		return s.memoryListRowsQuery(context.Background(), MemoryCollectionOptions{
			WorkspaceID: "wsp_a", PrincipalID: "usr_1", View: "all",
		}).Find(&items)
	})

	require.Contains(t, sqlStr, "resource_activity_latest_commits")
	require.Contains(t, sqlStr, `workspace_id = "wsp_a"`)
	require.Contains(t, sqlStr, `asset_type = "memory"`)
	require.Contains(t, sqlStr, "asset_id = memories.id")
	require.NotContains(t, sqlStr, "resource_activity_events")
}

func TestMemoryDecorationCommitQueryReadsLatestCommitProjection(t *testing.T) {
	store := newTestStore(t)

	sqlStr := store.db.ToSQL(func(tx *gorm.DB) *gorm.DB {
		var rows []struct {
			AssetID    string
			OccurredAt time.Time
		}
		return latestMemoryCommitRowsQuery(tx, "wsp_a", []string{"mem_a", "mem_b"}).Find(&rows)
	})

	require.Contains(t, sqlStr, "resource_activity_latest_commits")
	require.Contains(t, sqlStr, `workspace_id = "wsp_a"`)
	require.Contains(t, sqlStr, `asset_type = "memory"`)
	require.Contains(t, sqlStr, `asset_id IN ("mem_a","mem_b")`)
	require.NotContains(t, sqlStr, "resource_activity_events")
}

func TestStoreGetsAgentAccessibleMemoryByBinding(t *testing.T) {
	ctx := context.Background()
	store := newTestStore(t)
	now := time.Date(2026, 6, 8, 1, 0, 0, 0, time.UTC)
	_, err := store.CreateMemory(ctx, Memory{ID: "mem_allowed", Name: "Allowed", Description: "A", IsActive: true, CreatedBy: "user", UpdatedBy: "user", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	_, err = store.CreateMemory(ctx, Memory{ID: "mem_other", Name: "Other", Description: "B", IsActive: true, CreatedBy: "user", UpdatedBy: "user", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	_, err = store.CreateMemory(ctx, Memory{ID: "mem_inactive", Name: "Inactive", Description: "C", IsActive: true, CreatedBy: "user", UpdatedBy: "user", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	require.NoError(t, store.db.Model(&Memory{}).Where("id = ?", "mem_inactive").Update("is_active", false).Error)
	_, err = store.UpsertMemoryAgentBinding(ctx, MemoryAgentBinding{ID: "mab_allowed", WorkspaceID: "ws-1", MemoryID: "mem_allowed", AgentID: "agt_1", Branch: "main", CreatedBy: "admin", UpdatedBy: "admin", CreatedAt: now, UpdatedAt: now.Add(time.Minute)})
	require.NoError(t, err)
	_, err = store.UpsertMemoryAgentBinding(ctx, MemoryAgentBinding{ID: "mab_other", WorkspaceID: "ws-1", MemoryID: "mem_other", AgentID: "agt_2", Branch: "main", CreatedBy: "admin", UpdatedBy: "admin", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	_, err = store.UpsertMemoryAgentBinding(ctx, MemoryAgentBinding{ID: "mab_inactive", WorkspaceID: "ws-1", MemoryID: "mem_inactive", AgentID: "agt_inactive", Branch: "notes", CreatedBy: "admin", UpdatedBy: "admin", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)

	item, err := store.GetAgentAccessibleMemory(ctx, "ws-1", "agt_1", false)
	require.NoError(t, err)
	require.Equal(t, "mem_allowed", item.Memory.ID)
	require.Equal(t, "agt_1", item.Binding.AgentID)
	require.Equal(t, "main", item.Binding.Branch)

	_, err = store.GetAgentAccessibleMemory(ctx, "ws-1", "agt_inactive", false)
	require.ErrorIs(t, err, gorm.ErrRecordNotFound)

	rowsByAgent, err := store.ListAgentAccessibleMemories(ctx, "ws-1", []string{"agt_1", "agt_2", "agt_inactive", "agt_1", ""}, false)
	require.NoError(t, err)
	require.Equal(t, "mem_allowed", rowsByAgent["agt_1"].Memory.ID)
	require.Equal(t, "mem_other", rowsByAgent["agt_2"].Memory.ID)
	_, ok := rowsByAgent["agt_inactive"]
	require.False(t, ok)
}

func TestStoreAgentAccessibleMemoryLoadsBindingsBeforeBoundedLatestDecoration(t *testing.T) {
	ctx := context.Background()
	store := newTestStore(t)
	now := time.Date(2026, 6, 8, 1, 0, 0, 0, time.UTC)
	matchingCommitAt := now.Add(time.Hour)
	wrongBranchCommitAt := now.Add(2 * time.Hour)
	_, err := store.CreateMemory(ctx, Memory{ID: "mem_bound", Name: "Bound", IsActive: true, CreatedBy: "user", UpdatedBy: "user", CreatedAt: now, UpdatedAt: now.Add(24 * time.Hour)})
	require.NoError(t, err)
	_, err = store.CreateMemory(ctx, Memory{ID: "mem_no_commit", Name: "No commit", IsActive: true, CreatedBy: "user", UpdatedBy: "user", CreatedAt: now, UpdatedAt: now.Add(48 * time.Hour)})
	require.NoError(t, err)
	_, err = store.UpsertMemoryAgentBinding(ctx, MemoryAgentBinding{ID: "mab_bound", WorkspaceID: "ws-1", MemoryID: "mem_bound", AgentID: "agt_bound", Branch: "feature/memory", CreatedBy: "admin", UpdatedBy: "admin", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	_, err = store.UpsertMemoryAgentBinding(ctx, MemoryAgentBinding{ID: "mab_no_commit", WorkspaceID: "ws-1", MemoryID: "mem_no_commit", AgentID: "agt_no_commit", Branch: "main", CreatedBy: "admin", UpdatedBy: "admin", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	_, err = activity.New(store.db).UpsertEvent(ctx, activity.ResourceActivityEvent{
		ID:          "act_wrong_branch",
		WorkspaceID: "ws-1",
		AssetType:   activity.AssetTypeMemory,
		AssetID:     "mem_bound",
		EventType:   "commit_pushed",
		DetailsJSON: `{"ref":"refs/heads/main"}`,
		OccurredAt:  wrongBranchCommitAt,
	})
	require.NoError(t, err)
	_, err = activity.New(store.db).UpsertEvent(ctx, activity.ResourceActivityEvent{
		ID:          "act_matching_branch",
		WorkspaceID: "ws-1",
		AssetType:   activity.AssetTypeMemory,
		AssetID:     "mem_bound",
		EventType:   "commit_pushed",
		DetailsJSON: `{"ref":"refs/heads/feature/memory"}`,
		OccurredAt:  matchingCommitAt,
	})
	require.NoError(t, err)

	rowsByAgent, err := store.ListAgentAccessibleMemories(ctx, "ws-1", []string{"agt_bound", "agt_no_commit"}, false)
	require.NoError(t, err)
	require.True(t, rowsByAgent["agt_bound"].LastSyncAt.IsZero(), "base binding load must not touch legacy activity")
	require.True(t, rowsByAgent["agt_no_commit"].LastSyncAt.IsZero(), "metadata updated_at must not masquerade as a successful memory save")

	latest, err := store.LatestMemoryCommitTimesForBranches(ctx, "ws-1", map[string]string{"mem_bound": "feature/memory"})
	require.NoError(t, err)
	require.True(t, latest["mem_bound"].IsZero(), "a latest event on another branch is omitted rather than scanning older history")
	newMatchingCommitAt := wrongBranchCommitAt.Add(time.Hour)
	_, err = activity.New(store.db).UpsertEvent(ctx, activity.ResourceActivityEvent{
		ID: "act_latest_matching_branch", WorkspaceID: "ws-1", AssetType: activity.AssetTypeMemory,
		AssetID: "mem_bound", EventType: "commit_pushed", DetailsJSON: `{"ref":"refs/heads/feature/memory"}`,
		OccurredAt: newMatchingCommitAt,
	})
	require.NoError(t, err)
	latest, err = store.LatestMemoryCommitTimesForBranches(ctx, "ws-1", map[string]string{"mem_bound": "feature/memory"})
	require.NoError(t, err)
	require.Equal(t, newMatchingCommitAt, latest["mem_bound"])
}

func TestLatestMemoryCommitTimesForBranchesUsesBoundedProjectionShape(t *testing.T) {
	store := newTestStore(t)
	sql := store.db.ToSQL(func(tx *gorm.DB) *gorm.DB {
		var rows []latestMemoryCommitTimeRow
		return latestMemoryCommitTimesForBranchesQuery(tx, "wsp_1", []string{"mem_1", "mem_2"}).Scan(&rows)
	})
	require.Contains(t, sql, "FROM resource_activity_latest_commits AS latest")
	require.Contains(t, sql, "events.id = latest.event_id")
	require.Contains(t, sql, "latest.asset_id IN")
	require.NotContains(t, sql, "ORDER BY", "one-row-per-ID projection must not sort append-only history")
	require.NotContains(t, sql, "FROM resource_activity_events\n", "history must not drive the bounded lookup")
}

func TestLatestMemoryCommitTimesForBranchesPlanDoesNotScaleWithHistory(t *testing.T) {
	store := newTestStore(t)
	base := time.Date(2026, 8, 22, 1, 0, 0, 0, time.UTC)
	history := make([]activity.ResourceActivityEvent, 2_000)
	for index := range history {
		history[index] = activity.ResourceActivityEvent{
			ID: fmt.Sprintf("act_history_%04d", index), WorkspaceID: "wsp_1",
			AssetType: activity.AssetTypeMemory, AssetID: "mem_1", EventType: activity.EventTypeCommit,
			DetailsJSON: `{"ref":"refs/heads/main"}`, OccurredAt: base.Add(time.Duration(index) * time.Second), CreatedAt: base,
		}
	}
	require.NoError(t, store.db.CreateInBatches(history, 200).Error)
	latestEvent := activity.ResourceActivityEvent{
		ID: "act_projected", WorkspaceID: "wsp_1", AssetType: activity.AssetTypeMemory,
		AssetID: "mem_1", EventType: activity.EventTypeCommit,
		DetailsJSON: `{"ref":"refs/heads/main"}`, OccurredAt: base.Add(3_000 * time.Second), CreatedAt: base,
	}
	require.NoError(t, store.db.Create(&latestEvent).Error)
	require.NoError(t, store.db.Create(&activity.ResourceActivityLatestCommit{
		WorkspaceID: "wsp_1", AssetType: activity.AssetTypeMemory, AssetID: "mem_1",
		OccurredAt: latestEvent.OccurredAt, EventID: latestEvent.ID,
	}).Error)

	var queries atomic.Int64
	require.NoError(t, store.db.Callback().Row().Before("gorm:row").Register("test:count_latest_memory_commit_decoration", func(*gorm.DB) {
		queries.Add(1)
	}))
	latest, err := store.LatestMemoryCommitTimesForBranches(t.Context(), "wsp_1", map[string]string{"mem_1": "main"})
	require.NoError(t, err)
	require.Equal(t, latestEvent.OccurredAt, latest["mem_1"])
	require.EqualValues(t, 1, queries.Load(), "legacy sync-time decoration must stay one batch query regardless of activity history")

	type planRow struct {
		Detail string
	}
	var plan []planRow
	require.NoError(t, store.db.Raw(`EXPLAIN QUERY PLAN
SELECT latest.asset_id, latest.occurred_at, events.details_json
FROM resource_activity_latest_commits AS latest
JOIN resource_activity_events AS events ON events.id = latest.event_id
WHERE events.workspace_id = latest.workspace_id
  AND events.asset_type = latest.asset_type
  AND events.asset_id = latest.asset_id
  AND events.event_type = ?
  AND latest.workspace_id = ? AND latest.asset_type = ? AND latest.asset_id IN (?)`,
		activity.EventTypeCommit, "wsp_1", activity.AssetTypeMemory, "mem_1").Scan(&plan).Error)
	details := make([]string, 0, len(plan))
	for _, row := range plan {
		details = append(details, row.Detail)
	}
	joined := strings.Join(details, "\n")
	require.Contains(t, joined, "resource_activity_latest_commits", "plan must seek the request-bounded latest projection")
	require.Contains(t, joined, "resource_activity_events", "plan must fetch only the projected event by ID")
	require.NotContains(t, strings.ToUpper(joined), "SCAN RESOURCE_ACTIVITY_EVENTS", "append-only history must never drive the lookup")
}

func TestStoreMemoryAgentBindingEnforcesOneToOne(t *testing.T) {
	ctx := context.Background()
	store := newTestStore(t)
	now := time.Now().UTC()
	_, err := store.CreateMemory(ctx, Memory{ID: "mem_one", Name: "One", IsActive: true, CreatedBy: "user", UpdatedBy: "user", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	_, err = store.CreateMemory(ctx, Memory{ID: "mem_two", Name: "Two", IsActive: true, CreatedBy: "user", UpdatedBy: "user", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)

	_, err = store.UpsertMemoryAgentBinding(ctx, MemoryAgentBinding{ID: "mab_one", WorkspaceID: "ws-1", MemoryID: "mem_one", AgentID: "agt_1", Branch: "main", CreatedBy: "admin", UpdatedBy: "admin", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	updated, err := store.UpsertMemoryAgentBinding(ctx, MemoryAgentBinding{ID: "mab_two", WorkspaceID: "ws-1", MemoryID: "mem_two", AgentID: "agt_1", Branch: "notes", ImportExistingLocalContent: true, ReplaceLocalContentOnSwitch: true, CreatedBy: "admin", UpdatedBy: "admin", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	require.Equal(t, "mem_two", updated.MemoryID)
	require.Equal(t, "notes", updated.Branch)
	require.True(t, updated.ImportExistingLocalContent)
	require.True(t, updated.ReplaceLocalContentOnSwitch)

	_, err = store.UpsertMemoryAgentBinding(ctx, MemoryAgentBinding{ID: "mab_three", WorkspaceID: "ws-1", MemoryID: "mem_two", AgentID: "agt_2", Branch: "main", CreatedBy: "admin", UpdatedBy: "admin", CreatedAt: now, UpdatedAt: now})
	require.ErrorIs(t, err, ErrMemoryAgentBindingConflict)
}

func TestStoreAgentAssetAccessOutboxLeaseAckAndRetry(t *testing.T) {
	ctx := context.Background()
	store := newTestStore(t)
	now := time.Date(2026, 6, 20, 3, 0, 0, 0, time.UTC)

	first, err := store.EnqueueAgentAssetAccessChange(ctx, AgentAssetAccessChangeInput{
		WorkspaceID: "ws-1",
		AgentID:     "agt_1",
		AssetType:   AssetTypeMemory,
		OccurredAt:  now,
	})
	require.NoError(t, err)
	require.NotEmpty(t, first.ID)
	require.Equal(t, AgentAssetAccessOutboxStatusPending, first.Status)
	require.Equal(t, AgentAssetAccessSourceOutbox, first.Source)
	require.GreaterOrEqual(t, first.Version, now.UnixNano())

	second, err := store.EnqueueAgentAssetAccessChange(ctx, AgentAssetAccessChangeInput{
		WorkspaceID: "ws-1",
		AgentID:     "agt_1",
		AssetType:   AssetTypeMemory,
		OccurredAt:  now,
	})
	require.NoError(t, err)
	require.Greater(t, second.Version, first.Version)

	leased, err := store.LeaseDueAgentAssetAccessChanges(ctx, now, "daemon-1", time.Time{}, 10, nil)
	require.NoError(t, err)
	require.Len(t, leased, 2)
	require.Equal(t, AgentAssetAccessOutboxStatusLeased, leased[0].Status)
	require.Equal(t, "daemon-1", leased[0].LeaseOwner)
	require.NotNil(t, leased[0].LeaseExpiresAt)

	updated, err := store.MarkAgentAssetAccessChangeDelivered(ctx, leased[0].ID, "daemon-1", now.Add(time.Second))
	require.NoError(t, err)
	require.True(t, updated)
	delivered, err := store.GetAgentAssetAccessChange(ctx, leased[0].ID)
	require.NoError(t, err)
	require.Equal(t, AgentAssetAccessOutboxStatusDelivered, delivered.Status)
	require.NotNil(t, delivered.DeliveredAt)

	nextAttempt := now.Add(5 * time.Minute)
	retry, err := store.MarkAgentAssetAccessChangeAttemptFailed(ctx, leased[1], "daemon-1", "machine offline", nextAttempt, now.Add(2*time.Second))
	require.NoError(t, err)
	require.True(t, retry.Updated)
	require.False(t, retry.Failed)
	failedOnce, err := store.GetAgentAssetAccessChange(ctx, leased[1].ID)
	require.NoError(t, err)
	require.Equal(t, AgentAssetAccessOutboxStatusPending, failedOnce.Status)
	require.Equal(t, 1, failedOnce.Attempts)
	require.Equal(t, "machine offline", failedOnce.LastError)
	require.Equal(t, nextAttempt, failedOnce.NextAttemptAt)

	none, err := store.LeaseDueAgentAssetAccessChanges(ctx, now.Add(time.Minute), "daemon-2", time.Time{}, 10, nil)
	require.NoError(t, err)
	require.Empty(t, none)
	released, err := store.LeaseDueAgentAssetAccessChanges(ctx, nextAttempt, "daemon-2", time.Time{}, 10, nil)
	require.NoError(t, err)
	require.Len(t, released, 1)
	require.Equal(t, leased[1].ID, released[0].ID)
	require.Equal(t, "daemon-2", released[0].LeaseOwner)
}

func TestStoreAgentAssetAccessOutboxVersionIsScopedByAgentAndAssetType(t *testing.T) {
	ctx := context.Background()
	store := newTestStore(t)
	now := time.Date(2026, 6, 20, 3, 30, 0, 0, time.UTC)

	memoryFirst, err := store.EnqueueAgentAssetAccessChange(ctx, AgentAssetAccessChangeInput{
		WorkspaceID: "ws-1",
		AgentID:     "agt_1",
		AssetType:   AssetTypeMemory,
		OccurredAt:  now,
	})
	require.NoError(t, err)
	memorySecond, err := store.EnqueueAgentAssetAccessChange(ctx, AgentAssetAccessChangeInput{
		WorkspaceID: "ws-1",
		AgentID:     "agt_1",
		AssetType:   AssetTypeMemory,
		OccurredAt:  now,
	})
	require.NoError(t, err)
	require.Greater(t, memorySecond.Version, memoryFirst.Version)

	skillFirst, err := store.EnqueueAgentAssetAccessChange(ctx, AgentAssetAccessChangeInput{
		WorkspaceID: "ws-1",
		AgentID:     "agt_1",
		AssetType:   AssetTypeSkill,
		OccurredAt:  now,
	})
	require.NoError(t, err)
	require.Equal(t, memoryFirst.Version, skillFirst.Version, "different asset types keep independent stale-event generations")

	otherAgentFirst, err := store.EnqueueAgentAssetAccessChange(ctx, AgentAssetAccessChangeInput{
		WorkspaceID: "ws-1",
		AgentID:     "agt_2",
		AssetType:   AssetTypeMemory,
		OccurredAt:  now,
	})
	require.NoError(t, err)
	require.Equal(t, memoryFirst.Version, otherAgentFirst.Version, "different agents keep independent stale-event generations")
}

func TestStoreAgentAssetAccessOutboxLeaseFiltersWorkspaces(t *testing.T) {
	ctx := context.Background()
	store := newTestStore(t)
	now := time.Date(2026, 6, 20, 4, 0, 0, 0, time.UTC)

	ws1, err := store.EnqueueAgentAssetAccessChange(ctx, AgentAssetAccessChangeInput{
		WorkspaceID: "ws-1",
		AgentID:     "agt_1",
		AssetType:   AssetTypeSkill,
		OccurredAt:  now,
	})
	require.NoError(t, err)
	ws2, err := store.EnqueueAgentAssetAccessChange(ctx, AgentAssetAccessChangeInput{
		WorkspaceID: "ws-2",
		AgentID:     "agt_2",
		AssetType:   AssetTypeSkill,
		OccurredAt:  now,
	})
	require.NoError(t, err)

	leased, err := store.LeaseDueAgentAssetAccessChanges(ctx, now, "daemon-ws1", time.Time{}, 10, []string{" ws-1 ", "", "ws-1"})
	require.NoError(t, err)
	require.Len(t, leased, 1)
	require.Equal(t, ws1.ID, leased[0].ID)

	remaining, err := store.LeaseDueAgentAssetAccessChanges(ctx, now, "daemon-any", time.Time{}, 10, nil)
	require.NoError(t, err)
	require.Len(t, remaining, 1)
	require.Equal(t, ws2.ID, remaining[0].ID)
}

func TestStoreListsAgentAccessibleMemoriesFromOwnedRows(t *testing.T) {
	ctx := context.Background()
	store := newTestStore(t)
	now := time.Now().UTC()
	_, err := store.CreateMemory(ctx, Memory{ID: "mem_one", Name: "One", IsActive: true, CreatedBy: "user", UpdatedBy: "user", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	_, err = store.CreateMemory(ctx, Memory{ID: "mem_two", Name: "Two", IsActive: true, CreatedBy: "user", UpdatedBy: "user", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	_, err = store.CreateMemory(ctx, Memory{ID: "mem_other", Name: "Other", IsActive: true, CreatedBy: "user", UpdatedBy: "user", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	_, err = store.CreateMemory(ctx, Memory{ID: "mem_deleted", Name: "Deleted", IsActive: true, CreatedBy: "user", UpdatedBy: "user", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	_, err = store.UpsertMemoryAgentBinding(ctx, MemoryAgentBinding{ID: "mab_two", WorkspaceID: "ws-1", MemoryID: "mem_two", AgentID: "agt_2", Branch: "dev", CreatedBy: "admin", UpdatedBy: "admin", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	_, err = store.UpsertMemoryAgentBinding(ctx, MemoryAgentBinding{ID: "mab_one", WorkspaceID: "ws-1", MemoryID: "mem_one", AgentID: "agt_1", Branch: "main", CreatedBy: "admin", UpdatedBy: "admin", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	_, err = store.UpsertMemoryAgentBinding(ctx, MemoryAgentBinding{ID: "mab_other", WorkspaceID: "ws-2", MemoryID: "mem_other", AgentID: "agt_other", Branch: "main", CreatedBy: "admin", UpdatedBy: "admin", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	_, err = store.UpsertMemoryAgentBinding(ctx, MemoryAgentBinding{ID: "mab_deleted", WorkspaceID: "ws-1", MemoryID: "mem_deleted", AgentID: "agt_deleted", Branch: "main", CreatedBy: "admin", UpdatedBy: "admin", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	deletedAt := now.Add(time.Minute)
	require.NoError(t, store.db.WithContext(ctx).Model(&Memory{}).Where("id = ?", "mem_deleted").Update("deleted_at", deletedAt).Error)

	memoriesByAgent, err := store.ListAgentAccessibleMemories(ctx, "ws-1", []string{"agt_1", "agt_2", "agt_deleted"}, true)
	require.NoError(t, err)
	require.Len(t, memoriesByAgent, 2)
	require.Equal(t, "mem_one", memoriesByAgent["agt_1"].Memory.ID)
	require.Equal(t, "main", memoriesByAgent["agt_1"].Binding.Branch)
	require.Equal(t, "mem_two", memoriesByAgent["agt_2"].Memory.ID)
	require.Equal(t, "dev", memoriesByAgent["agt_2"].Binding.Branch)
	require.NotContains(t, memoriesByAgent, "agt_other")
	require.NotContains(t, memoriesByAgent, "agt_deleted")
}
