package memory

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"testing"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/gormtest"
	activityservice "github.com/tidbcloud/AgentTeam/internal/service/resource/activity"
	reposservice "github.com/tidbcloud/AgentTeam/internal/service/resource/repos"

	"github.com/stretchr/testify/require"
	activitystore "github.com/tidbcloud/AgentTeam/internal/store/resource/activity"
	memorystore "github.com/tidbcloud/AgentTeam/internal/store/resource/memory"
	resourcerepostore "github.com/tidbcloud/AgentTeam/internal/store/resource/repos"
)

func newMemoryServiceForTest(t *testing.T) *MemoryService {
	return newMemoryServiceForTestWithOwnedAgents(t, []AgentOwnership{
		{ID: "agt_reader", Name: "reader"},
		{ID: "agt_alice", Name: "alice"},
		{ID: "agt_bob", Name: "bob"},
		{ID: "agt_missing_branch", Name: "missing-branch"},
		{ID: "agt_unseen", Name: "unseen"},
	})
}

func newMemoryServiceForTestWithOwnedAgents(t *testing.T, agents []AgentOwnership) *MemoryService {
	t.Helper()
	db, err := gormtest.OpenSQLiteDB(t)
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(&memorystore.Memory{}, &memorystore.MemoryContentCheckpoint{}, &memorystore.MemoryGrant{}, &memorystore.MemoryPin{}, &memorystore.AgentAssetAccessChangeOutbox{}, &resourcerepostore.ResourceRepository{}, &resourcerepostore.ResourceRepositoryAccessGrant{}, &resourcerepostore.ResourceRepositoryLifecycleEvent{}, &activitystore.ResourceActivityEvent{}, &activitystore.ResourceActivityLatestCommit{}, &activitystore.ResourceActivityProjectorCheckpoint{}))
	fixed := time.Date(2026, 6, 1, 1, 2, 3, 0, time.UTC)
	return NewMemoryServiceWithOptions(memorystore.New(db), resourcerepostore.New(db), &fakeAGSRepositoryStore{}, func() time.Time { return fixed }, MemoryServiceOptions{AgentOwnershipSource: fakeAgentOwnershipSource{agents: agents}, ActivityStore: activitystore.New(db)})
}

type fakeMemoryActivityProjector struct {
	repos []resourcerepostore.ResourceRepository
	opts  []activityservice.ResourceActivityProjectorRefreshOptions
}

func (p *fakeMemoryActivityProjector) RefreshRepositories(_ context.Context, repos []resourcerepostore.ResourceRepository, opts activityservice.ResourceActivityProjectorRefreshOptions) (activityservice.ResourceActivityProjectorRunResult, error) {
	p.repos = append(p.repos, repos...)
	p.opts = append(p.opts, opts)
	return activityservice.ResourceActivityProjectorRunResult{Repositories: len(repos)}, nil
}

type fakeAgentOwnershipSource struct {
	agents []AgentOwnership
	err    error
}

func (s fakeAgentOwnershipSource) ListOwnedAgents(context.Context, string, string) ([]AgentOwnership, error) {
	if s.err != nil {
		return nil, s.err
	}
	return append([]AgentOwnership(nil), s.agents...), nil
}

type sequenceAgentOwnershipSource struct {
	responses [][]AgentOwnership
	errs      []error
	calls     int
}

func (s *sequenceAgentOwnershipSource) ListOwnedAgents(context.Context, string, string) ([]AgentOwnership, error) {
	call := s.calls
	s.calls++
	if call < len(s.errs) && s.errs[call] != nil {
		return nil, s.errs[call]
	}
	if call >= len(s.responses) {
		return nil, nil
	}
	return append([]AgentOwnership(nil), s.responses[call]...), nil
}

type fakeRepositorySyncTokenIssuer struct {
	requests []reposservice.RepositorySyncTokenRequest
	lease    reposservice.RepositorySyncTokenLease
	err      error
}

func (i *fakeRepositorySyncTokenIssuer) CreateRepositorySyncToken(_ context.Context, req reposservice.RepositorySyncTokenRequest) (reposservice.RepositorySyncTokenLease, error) {
	i.requests = append(i.requests, req)
	if i.err != nil {
		return reposservice.RepositorySyncTokenLease{}, i.err
	}
	return i.lease, nil
}

type fakeAGSRepositoryStore struct {
	ensureRequests []reposservice.AGSRepositoryEnsureRequest
	repository     reposservice.RepositoryIdentity
	deleteErr      error
	deletes        []reposservice.RepositoryIdentity
	memoryInits    []MemoryRepositoryInitializeRequest
	memoryInitErr  error
	memoryForks    []MemoryRepositoryForkRequest
	memoryForkErr  error
	commitRequests []MemoryRepositoryCommitListRequest
	commitPayload  json.RawMessage
	grantRequests  []MemoryRepositoryAccessGrantRequest
	grantResult    MemoryRepositoryAccessGrantResult
}

func (s *fakeAGSRepositoryStore) EnsureRepository(_ context.Context, request reposservice.AGSRepositoryEnsureRequest) (reposservice.RepositoryIdentity, error) {
	s.ensureRequests = append(s.ensureRequests, request)
	if s.repository.FullName != "" || s.repository.Owner != "" || s.repository.Name != "" || s.repository.ID != "" {
		return s.repository, nil
	}
	repository := reposservice.RepositoryIdentity{Owner: request.Owner, Name: request.Name, FullName: request.Owner + "/" + request.Name, DefaultBranch: request.DefaultBranch}
	if repository.ID == "" {
		repository.ID = "repo-" + request.RepoRole
	}
	return repository, nil
}

func (s *fakeAGSRepositoryStore) DeleteRepository(_ context.Context, repository reposservice.RepositoryIdentity) error {
	s.deletes = append(s.deletes, repository)
	return s.deleteErr
}

func (s *fakeAGSRepositoryStore) InitializeMemoryRepository(_ context.Context, request MemoryRepositoryInitializeRequest) error {
	s.memoryInits = append(s.memoryInits, request)
	return s.memoryInitErr
}

func (s *fakeAGSRepositoryStore) ForkMemoryRepository(_ context.Context, request MemoryRepositoryForkRequest) error {
	s.memoryForks = append(s.memoryForks, request)
	return s.memoryForkErr
}

func (s *fakeAGSRepositoryStore) ListMemoryRepositoryCommits(_ context.Context, request MemoryRepositoryCommitListRequest) (json.RawMessage, error) {
	s.commitRequests = append(s.commitRequests, request)
	if s.commitPayload != nil {
		return s.commitPayload, nil
	}
	return json.RawMessage(`[]`), nil
}

func (s *fakeAGSRepositoryStore) GetMemoryRepositoryCompare(context.Context, MemoryRepositoryCompareRequest) (json.RawMessage, error) {
	return json.RawMessage(`{}`), nil
}

func (s *fakeAGSRepositoryStore) GrantMemoryRepositoryAccess(_ context.Context, request MemoryRepositoryAccessGrantRequest) (MemoryRepositoryAccessGrantResult, error) {
	s.grantRequests = append(s.grantRequests, request)
	if s.grantResult.Materialized {
		return s.grantResult, nil
	}
	return MemoryRepositoryAccessGrantResult{Materialized: true}, nil
}

func TestListMemoriesRefreshesVisibleMemoryActivity(t *testing.T) {
	ctx := context.Background()
	svc := newMemoryServiceForTest(t)
	projector := &fakeMemoryActivityProjector{}
	svc.activityProjector = projector
	svc.activityRefreshAfter = 30 * time.Minute
	now := svc.now()

	visible, err := svc.store.CreateMemory(ctx, memorystore.Memory{ID: "mem_visible", Name: "Visible", IsActive: true, CreatedBy: "usr_1", UpdatedBy: "usr_1", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	hidden, err := svc.store.CreateMemory(ctx, memorystore.Memory{ID: "mem_hidden", Name: "Hidden", IsActive: true, CreatedBy: "usr_2", UpdatedBy: "usr_2", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	_, err = svc.repositoryStore.Upsert(ctx, resourcerepostore.ResourceRepository{ID: "rr_mem_visible", WorkspaceID: "ws_1", ResourceType: resourcerepostore.ResourceTypeMemory, ResourceID: visible.ID, RepoRole: resourcerepostore.RepoRolePrimary, OwnerKind: "personal", RepoOwner: "usr_1", RepoName: "visible.mem", RepoFullName: "usr_1/visible.mem", CreatedBy: "usr_1", UpdatedBy: "usr_1", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	_, err = svc.repositoryStore.Upsert(ctx, resourcerepostore.ResourceRepository{ID: "rr_mem_hidden", WorkspaceID: "ws_1", ResourceType: resourcerepostore.ResourceTypeMemory, ResourceID: hidden.ID, RepoRole: resourcerepostore.RepoRolePrimary, OwnerKind: "personal", RepoOwner: "usr_2", RepoName: "hidden.mem", RepoFullName: "usr_2/hidden.mem", CreatedBy: "usr_2", UpdatedBy: "usr_2", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)

	page, err := svc.ListMemories(ctx, "ws_1", "usr_1", "all", "", 1, 20)
	require.NoError(t, err)
	require.Len(t, page.Items, 1)
	require.Equal(t, "mem_visible", page.Items[0].ID)
	require.Len(t, projector.repos, 1)
	require.Equal(t, visible.ID, projector.repos[0].ResourceID)
	require.Equal(t, resourcerepostore.ResourceTypeMemory, projector.repos[0].ResourceType)
	require.Equal(t, 30*time.Minute, projector.opts[0].RefreshAfter)
}

func TestGetAgentCloudMemoryUsesPersistedLastSyncWithoutRefreshingRepository(t *testing.T) {
	ctx := context.Background()
	svc := newMemoryServiceForTestWithOwnedAgents(t, []AgentOwnership{{ID: "agt_alice", Name: "alice", DisplayName: "Alice"}})
	commitAt := time.Date(2026, 6, 24, 2, 0, 0, 0, time.UTC)

	enabled, err := svc.EnableAgentCloudMemory(ctx, "wsp_1", "usr_1", "agt_alice", AgentCloudMemoryEnableMutation{})
	require.NoError(t, err)
	_, err = svc.BindAgentMemory(ctx, "wsp_1", "usr_1", "agt_alice", AgentMemoryBindingMutation{MemoryID: enabled.MemoryID, Branch: "agent/alice"})
	require.NoError(t, err)
	require.NotNil(t, enabled.Repository)

	_, err = svc.activityStore.UpsertEvent(ctx, activitystore.ResourceActivityEvent{
		ID:          "act_agent_alice_commit",
		WorkspaceID: "wsp_1",
		AssetType:   activitystore.AssetTypeMemory,
		AssetID:     enabled.MemoryID,
		EventType:   activitystore.EventTypeCommit,
		DetailsJSON: `{"ref":"refs/heads/agent/alice"}`,
		OccurredAt:  commitAt,
	})
	require.NoError(t, err)
	projector := &fakeMemoryActivityProjector{}
	svc.activityProjector = projector

	cloudMemory, err := svc.GetAgentCloudMemory(ctx, "wsp_1", "usr_1", "agt_alice")
	require.NoError(t, err)
	require.Equal(t, commitAt, cloudMemory.LastSyncAt)
	require.Empty(t, projector.repos)
	require.Empty(t, projector.opts)
}

func TestEnableAgentCloudMemoryUsesTemplateLocale(t *testing.T) {
	ctx := context.Background()
	svc := newMemoryServiceForTestWithOwnedAgents(t, []AgentOwnership{{ID: "agt_alice", Name: "alice", DisplayName: "Alice"}})
	repositories, ok := svc.repositories.(*fakeAGSRepositoryStore)
	require.True(t, ok)

	enabled, err := svc.EnableAgentCloudMemory(ctx, "ws_1", "usr_1", "agt_alice", AgentCloudMemoryEnableMutation{TemplateLocale: "ja"})

	require.NoError(t, err)
	require.True(t, enabled.Enabled)
	require.Len(t, repositories.memoryInits, 1)
	require.Equal(t, "ja", repositories.memoryInits[0].Locale)
}

func TestEnableAgentCloudMemoryRepositoryNameIncludesFullAgentID(t *testing.T) {
	ctx := context.Background()
	agentID := "agt_31yhcngdqmm3qo"
	svc := newMemoryServiceForTestWithOwnedAgents(t, []AgentOwnership{{ID: agentID, Name: "assistant", DisplayName: "小助手"}})
	repositories, ok := svc.repositories.(*fakeAGSRepositoryStore)
	require.True(t, ok)

	enabled, err := svc.EnableAgentCloudMemory(ctx, "ws_1", "usr_1", agentID, AgentCloudMemoryEnableMutation{})

	require.NoError(t, err)
	require.True(t, enabled.Enabled)
	require.Len(t, repositories.ensureRequests, 1)
	require.Equal(t, "usr_1", repositories.ensureRequests[0].Owner)
	require.Equal(t, "memory-agt-31yhcngdqmm3qo.mem", repositories.ensureRequests[0].Name)
}

func TestEnableAgentCloudMemoryDeletesCreatedMemoryWhenBindFails(t *testing.T) {
	ctx := context.Background()
	agent := AgentOwnership{ID: "agt_alice", Name: "alice", DisplayName: "Alice"}
	svc := newMemoryServiceForTestWithOwnedAgents(t, []AgentOwnership{agent})
	repositories, ok := svc.repositories.(*fakeAGSRepositoryStore)
	require.True(t, ok)
	svc.agentOwnershipSource = &sequenceAgentOwnershipSource{
		responses: [][]AgentOwnership{
			{agent},
			{agent},
			nil,
		},
	}

	_, err := svc.EnableAgentCloudMemory(ctx, "ws_1", "usr_1", "agt_alice", AgentCloudMemoryEnableMutation{TemplateLocale: "ja"})

	require.Error(t, err)
	require.Contains(t, err.Error(), "agent must be owned")
	require.Len(t, repositories.memoryInits, 1)
	require.Len(t, repositories.deletes, 1)
	require.Len(t, repositories.ensureRequests, 1)
	createdMemoryID := repositories.ensureRequests[0].ResourceID
	_, err = svc.store.GetMemory(ctx, "ws_1", createdMemoryID)
	require.Error(t, err)
	repos, err := svc.repositoryStore.ListForResource(ctx, "ws_1", resourcerepostore.ResourceTypeMemory, createdMemoryID)
	require.NoError(t, err)
	require.Empty(t, repos)
}

func TestListAgentCloudMemoryCommitsLazilyRepairsRepositoryAccess(t *testing.T) {
	ctx := context.Background()
	svc := newMemoryServiceForTestWithOwnedAgents(t, []AgentOwnership{{ID: "agt_alice", Name: "alice", DisplayName: "Alice"}})
	repositories, ok := svc.repositories.(*fakeAGSRepositoryStore)
	require.True(t, ok)
	repositories.commitPayload = json.RawMessage(`[{"sha":"abc"}]`)
	actor := reposservice.ResourceRepositoryActor{
		Provider: "agentteam",
		Subject:  "usr_1",
		Login:    "alice",
		Name:     "Alice",
		Email:    "alice@example.test",
	}

	enabled, err := svc.EnableAgentCloudMemory(ctx, "wsp_1", "usr_1", "agt_alice", AgentCloudMemoryEnableMutation{Actor: actor})
	require.NoError(t, err)

	payload, err := svc.ListAgentCloudMemoryCommits(ctx, "wsp_1", "usr_1", "agt_alice", AgentCloudMemoryCommitListOptions{
		Ref:     "main",
		Page:    2,
		PerPage: 10,
		Actor:   actor,
	})

	require.NoError(t, err)
	require.JSONEq(t, `[{"sha":"abc"}]`, string(payload))
	require.Len(t, repositories.grantRequests, 1)
	require.Equal(t, enabled.Repository.RepoFullName, repositories.grantRequests[0].Repository.FullName)
	require.Equal(t, resourcerepostore.AccessPermissionRead, repositories.grantRequests[0].Permission)
	require.Equal(t, actor, repositories.grantRequests[0].Actor)

	grants, err := svc.store.ListMemoryGrants(ctx, "wsp_1", enabled.MemoryID)
	require.NoError(t, err)
	require.Len(t, grants, 1)
	accessRows, err := svc.repositoryStore.ListAccessGrantsForIntent(ctx, "wsp_1", resourcerepostore.AccessIntentTypeMemoryGrant, grants[0].ID)
	require.NoError(t, err)
	require.Len(t, accessRows, 1)
	require.Equal(t, resourcerepostore.AccessStatusCurrent, accessRows[0].Status)
	require.True(t, accessRows[0].Materialized)
	require.Equal(t, resourcerepostore.AccessPermissionRead, accessRows[0].Permission)
	require.Equal(t, "owner", accessRows[0].IntentProfile)
	require.Equal(t, enabled.Repository.RepoFullName, accessRows[0].RepoFullName)

	_, err = svc.ListAgentCloudMemoryCommits(ctx, "wsp_1", "usr_1", "agt_alice", AgentCloudMemoryCommitListOptions{Ref: "main", Actor: actor})
	require.NoError(t, err)
	require.Len(t, repositories.commitRequests, 2)
	require.Len(t, repositories.grantRequests, 1)
}

func TestMemoryServiceEnforcesMemoryGrantRoles(t *testing.T) {
	ctx := context.Background()
	svc := newMemoryServiceForTest(t)
	m, err := svc.CreateMemory(ctx, "wsp_1", "owner", MemoryCreateMutation{Name: "Runbooks"})
	require.NoError(t, err)

	ownerMemory, err := svc.GetMemory(ctx, "wsp_1", "owner", m.ID)
	require.NoError(t, err)
	require.Equal(t, "owner", ownerMemory.Role)
	require.True(t, ownerMemory.CanManage)
	require.False(t, ownerMemory.Pinned)
	require.NoError(t, svc.PinMemory(ctx, "wsp_1", "owner", m.ID))
	ownerMemory, err = svc.GetMemory(ctx, "wsp_1", "owner", m.ID)
	require.NoError(t, err)
	require.True(t, ownerMemory.Pinned)
	require.NotNil(t, ownerMemory.PinnedAt)
	collection, err := svc.ListMemories(ctx, "wsp_1", "owner", "all", "", 1, 20)
	require.NoError(t, err)
	require.Len(t, collection.Items, 1)
	require.Equal(t, "owner", collection.Items[0].Role)
	require.True(t, collection.Items[0].CanManage)
	require.True(t, collection.Items[0].Pinned)

	_, err = svc.GetMemory(ctx, "wsp_1", "outsider", m.ID)
	require.ErrorIs(t, err, ErrForbidden)

	permissions, err := svc.ListMemoryPermissions(ctx, "wsp_1", "owner", m.ID)
	require.NoError(t, err)
	require.True(t, permissions.CanManage)
	require.NotNil(t, permissions.Owner)
	require.Equal(t, "owner", permissions.Owner.Role)

	managerGrant, err := svc.UpsertMemoryPermission(ctx, "wsp_1", "owner", m.ID, MemoryPermissionGrantMutation{SubjectType: "user", SubjectID: "manager", Role: "manage"})
	require.NoError(t, err)
	require.Equal(t, "manage", managerGrant.Role)
	managerMemory, err := svc.GetMemory(ctx, "wsp_1", "manager", m.ID)
	require.NoError(t, err)
	require.Equal(t, "manage", managerMemory.Role)
	require.True(t, managerMemory.CanManage)
	viewerGrant, err := svc.UpsertMemoryPermission(ctx, "wsp_1", "manager", m.ID, MemoryPermissionGrantMutation{SubjectType: "user", SubjectID: "viewer", Role: "view"})
	require.NoError(t, err)
	require.Equal(t, "view", viewerGrant.Role)
	_, err = svc.UpsertMemoryPermission(ctx, "wsp_1", "manager", m.ID, MemoryPermissionGrantMutation{SubjectType: "agent", SubjectID: "agt_reader", Role: "edit"})
	require.ErrorIs(t, err, ErrInvalidArgument)
	agentBinding, err := svc.BindAgentMemory(ctx, "wsp_1", "manager", "agt_reader", AgentMemoryBindingMutation{MemoryID: m.ID, Branch: "main"})
	require.NoError(t, err)
	require.Equal(t, m.ID, agentBinding.MemoryID)
	editorGrant, err := svc.UpsertMemoryPermission(ctx, "wsp_1", "manager", m.ID, MemoryPermissionGrantMutation{SubjectType: "user", SubjectID: "editor", Role: "edit"})
	require.NoError(t, err)
	require.Equal(t, "view", editorGrant.Role)
	// Legacy edit input is normalized to view. Only owner/manage can grant or
	// revoke memory permissions.
	_, err = svc.UpsertMemoryPermission(ctx, "wsp_1", "editor", m.ID, MemoryPermissionGrantMutation{SubjectType: "user", SubjectID: "viewer2", Role: "view"})
	require.ErrorIs(t, err, ErrForbidden)
	_, err = svc.ListMemoryPermissions(ctx, "wsp_1", "editor", m.ID)
	require.ErrorIs(t, err, ErrForbidden)

	viewerMemory, err := svc.GetMemory(ctx, "wsp_1", "viewer", m.ID)
	require.NoError(t, err)
	require.Equal(t, "view", viewerMemory.Role)
	require.False(t, viewerMemory.CanManage)
	_, err = svc.ListMemoryPermissions(ctx, "wsp_1", "viewer", m.ID)
	require.ErrorIs(t, err, ErrForbidden)
	_, err = svc.UpdateMemory(ctx, "wsp_1", "viewer", m.ID, MemoryUpdateMutation{Name: "Denied"})
	require.ErrorIs(t, err, ErrForbidden)

	item, err := svc.GetAgentAccessibleMemory(ctx, "wsp_1", "different-acting-user", "agt_reader")
	require.NoError(t, err)
	require.NotNil(t, item)
	require.Equal(t, m.ID, item.ID)
	require.Equal(t, "read", item.AccessLevel)
}

func TestMemoryServiceUpdatesLinkIdentifierAndDeletesMemory(t *testing.T) {
	ctx := context.Background()
	svc := newMemoryServiceForTest(t)
	m, err := svc.CreateMemory(ctx, "wsp_1", "owner", MemoryCreateMutation{Name: "Runbooks"})
	require.NoError(t, err)
	require.NoError(t, svc.PinMemory(ctx, "wsp_1", "owner", m.ID))
	_, err = svc.UpsertMemoryPermission(ctx, "wsp_1", "owner", m.ID, MemoryPermissionGrantMutation{SubjectType: "user", SubjectID: "manager", Role: "manage"})
	require.NoError(t, err)

	updated, err := svc.UpdateMemory(ctx, "wsp_1", "owner", m.ID, MemoryUpdateMutation{Name: "Operations docs", Slug: "Ops Docs"})
	require.NoError(t, err)
	require.Equal(t, "Operations docs", updated.Name)
	require.Equal(t, "ops-docs", updated.Slug)

	require.NoError(t, svc.DeleteMemory(ctx, "wsp_1", "owner", m.ID))
	wikiStore, ok := svc.repositories.(*fakeAGSRepositoryStore)
	require.True(t, ok)
	require.Len(t, wikiStore.deletes, 1)
	require.Equal(t, "owner/runbooks.mem", wikiStore.deletes[0].FullName)
	repos, err := svc.repositoryStore.ListForResource(ctx, "wsp_1", resourcerepostore.ResourceTypeMemory, m.ID)
	require.NoError(t, err)
	require.Empty(t, repos)
	lifecycleEvents, err := svc.repositoryStore.ListLifecycleEventsForResource(ctx, "wsp_1", resourcerepostore.ResourceTypeMemory, m.ID)
	require.NoError(t, err)
	require.Len(t, lifecycleEvents, 1)
	require.Equal(t, resourcerepostore.ResourceRepositoryLifecycleOperationDelete, lifecycleEvents[0].Operation)
	require.Equal(t, resourcerepostore.ResourceRepositoryLifecycleStatusSucceeded, lifecycleEvents[0].Status)
	_, err = svc.GetMemory(ctx, "wsp_1", "owner", m.ID)
	require.ErrorIs(t, err, ErrNotFound)
	permissions, err := svc.store.ListMemoryGrants(ctx, "wsp_1", m.ID)
	require.NoError(t, err)
	require.Empty(t, permissions)
}

func TestMemoryServiceKeepsMemoryWhenManagedRepositoryDeleteFails(t *testing.T) {
	ctx := context.Background()
	svc := newMemoryServiceForTest(t)
	m, err := svc.CreateMemory(ctx, "wsp_1", "owner", MemoryCreateMutation{Name: "Runbooks"})
	require.NoError(t, err)
	wikiStore, ok := svc.repositories.(*fakeAGSRepositoryStore)
	require.True(t, ok)
	wikiStore.deleteErr = errors.New("ags delete failed")

	err = svc.DeleteMemory(ctx, "wsp_1", "owner", m.ID)

	require.Error(t, err)
	require.Contains(t, err.Error(), "ags delete failed")
	_, err = svc.GetMemory(ctx, "wsp_1", "owner", m.ID)
	require.NoError(t, err)
	repos, err := svc.repositoryStore.ListForResource(ctx, "wsp_1", resourcerepostore.ResourceTypeMemory, m.ID)
	require.NoError(t, err)
	require.Len(t, repos, 1)
	lifecycleEvents, err := svc.repositoryStore.ListLifecycleEventsForResource(ctx, "wsp_1", resourcerepostore.ResourceTypeMemory, m.ID)
	require.NoError(t, err)
	require.Len(t, lifecycleEvents, 1)
	require.Equal(t, resourcerepostore.ResourceRepositoryLifecycleOperationDelete, lifecycleEvents[0].Operation)
	require.Equal(t, resourcerepostore.ResourceRepositoryLifecycleStatusFailed, lifecycleEvents[0].Status)
	require.Contains(t, lifecycleEvents[0].LastError, "ags delete failed")
}

func TestMemoryServiceCreatesAccessibleRepositoryMapping(t *testing.T) {
	ctx := context.Background()
	db, err := gormtest.OpenSQLiteDB(t)
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(&memorystore.Memory{}, &memorystore.MemoryContentCheckpoint{}, &memorystore.MemoryGrant{}, &memorystore.MemoryPin{}, &memorystore.AgentAssetAccessChangeOutbox{}, &resourcerepostore.ResourceRepository{}, &resourcerepostore.ResourceRepositoryAccessGrant{}, &resourcerepostore.ResourceRepositoryLifecycleEvent{}, &activitystore.ResourceActivityEvent{}, &activitystore.ResourceActivityLatestCommit{}))
	wikiStore := &fakeAGSRepositoryStore{
		repository: reposservice.RepositoryIdentity{
			Owner:         "usr_1",
			Name:          "runbooks.mem",
			FullName:      "usr_1/runbooks.mem",
			ID:            "repo-42",
			DefaultBranch: "main",
		},
	}
	fixed := time.Date(2026, 6, 1, 1, 2, 3, 0, time.UTC)
	svc := NewMemoryService(memorystore.New(db), resourcerepostore.New(db), wikiStore, func() time.Time { return fixed })

	m, err := svc.CreateMemory(ctx, "ws-1", "usr_1", MemoryCreateMutation{
		Name:           "Runbooks",
		TemplateLocale: "ja-JP",
		Actor: reposservice.ResourceRepositoryActor{
			Provider: "agentteam",
			Subject:  "usr_1",
			Login:    "lloyd",
			Name:     "Lloyd",
			Email:    "lloyd@example.test",
		},
	})
	require.NoError(t, err)
	require.Len(t, wikiStore.ensureRequests, 1)
	require.Equal(t, m.ID, wikiStore.ensureRequests[0].ResourceID)
	require.Equal(t, resourcerepostore.ResourceTypeMemory, wikiStore.ensureRequests[0].ResourceType)
	require.Equal(t, resourcerepostore.RepoRolePrimary, wikiStore.ensureRequests[0].RepoRole)
	require.Equal(t, "usr_1", wikiStore.ensureRequests[0].Owner)
	require.NotContains(t, wikiStore.ensureRequests[0].Owner, ".mem")
	require.Equal(t, "runbooks.mem", wikiStore.ensureRequests[0].Name)
	require.True(t, wikiStore.ensureRequests[0].RejectExisting)
	require.Equal(t, reposservice.RepositoryNameFieldName, wikiStore.ensureRequests[0].NameConflictField)
	require.Equal(t, reposservice.ResourceRepositoryActor{Provider: "agentteam", Subject: "usr_1", Login: "lloyd", Name: "Lloyd", Email: "lloyd@example.test"}, wikiStore.ensureRequests[0].Actor)
	require.Len(t, wikiStore.memoryInits, 1)
	require.Equal(t, "ja", wikiStore.memoryInits[0].Locale)
	require.Equal(t, "メモリーリポジトリを初期化", wikiStore.memoryInits[0].Message)
	require.Equal(t, wikiStore.repository, wikiStore.memoryInits[0].Repository)
	require.Equal(t, "Lloyd", wikiStore.memoryInits[0].AuthorName)
	require.Equal(t, "lloyd@example.test", wikiStore.memoryInits[0].AuthorEmail)

	repos, err := svc.GetMemoryRepositories(ctx, "ws-1", "usr_1", m.ID)
	require.NoError(t, err)
	require.NotNil(t, repos.Primary)
	require.Equal(t, "personal", m.StorageScope)
	require.Equal(t, "usr_1", m.StorageOwnerLogin)
	require.Equal(t, "usr_1", repos.Primary.RepoOwner)
	require.NotContains(t, repos.Primary.RepoOwner, ".mem")
	require.Equal(t, "runbooks.mem", repos.Primary.RepoName)
	require.Equal(t, "usr_1/runbooks.mem", repos.Primary.RepoFullName)
	require.Equal(t, "personal", repos.Primary.OwnerKind)
	require.Equal(t, "repo-42", repos.Primary.AGSRepoID)
	require.Equal(t, "main", repos.Primary.DefaultRef)
	require.False(t, db.Migrator().HasTable("meshx_resource_repositories"))
	require.True(t, db.Migrator().HasTable("resource_repositories"))
}

func TestMemoryServiceSupportsWorkspaceStorageScope(t *testing.T) {
	ctx := context.Background()
	svc := newMemoryServiceForTest(t)

	m, err := svc.CreateMemory(ctx, "wsp_1", "usr_1", MemoryCreateMutation{Name: "Runbooks", StorageScope: reposservice.RepositoryStorageScopeWorkspace})
	require.NoError(t, err)
	require.Equal(t, reposservice.RepositoryStorageScopeWorkspace, m.StorageScope)
	require.Equal(t, "wsp_1", m.StorageOwnerLogin)

	repos, err := svc.GetMemoryRepositories(ctx, "wsp_1", "usr_1", m.ID)
	require.NoError(t, err)
	require.NotNil(t, repos.Primary)
	require.Equal(t, "wsp_1", repos.Primary.RepoOwner)
	require.NotContains(t, repos.Primary.RepoOwner, ".mem")
	require.Equal(t, "runbooks.mem", repos.Primary.RepoName)
	require.Equal(t, "wsp_1/runbooks.mem", repos.Primary.RepoFullName)
	require.Equal(t, reposservice.RepositoryStorageScopeWorkspace, repos.Primary.OwnerKind)
}

func TestMemoryServiceRejectsRepoFullNameConflicts(t *testing.T) {
	ctx := context.Background()
	svc := newMemoryServiceForTest(t)
	now := svc.now()
	_, err := svc.repositoryStore.Upsert(ctx, resourcerepostore.ResourceRepository{
		ID:           "rr_existing",
		WorkspaceID:  "wsp_1",
		ResourceType: resourcerepostore.ResourceTypeKBSource,
		ResourceID:   "src_existing",
		RepoRole:     resourcerepostore.RepoRoleInsights,
		OwnerKind:    reposservice.RepositoryStorageScopePersonal,
		RepoOwner:    "usr_1",
		RepoName:     "runbooks.mem",
		RepoFullName: "usr_1/runbooks.mem",
		CreatedBy:    "usr_1",
		UpdatedBy:    "usr_1",
		CreatedAt:    now,
		UpdatedAt:    now,
	})
	require.NoError(t, err)

	_, err = svc.CreateMemory(ctx, "wsp_1", "usr_1", MemoryCreateMutation{Name: "Runbooks"})

	require.Error(t, err)
	require.ErrorIs(t, err, reposservice.ErrConflict)
	require.Equal(t, http.StatusConflict, reposservice.HTTPStatus(err))
	require.Equal(t, reposservice.RepositoryNameConflictCode, reposservice.Code(err))
	details, ok := reposservice.Details(err).(reposservice.RepositoryNameConflictDetails)
	require.True(t, ok)
	require.Equal(t, reposservice.RepositoryNameFieldName, details.Field)
	require.Equal(t, "runbooks-1", details.SuggestedRepoName)
	require.Equal(t, "usr_1", details.ConflictOwner)
	require.Equal(t, "runbooks.mem", details.ConflictRepo)
	require.Equal(t, resourcerepostore.ResourceTypeKBSource, details.ResourceType)
}

func TestMemoryServiceForksVisibleMemoryAndAllowsBindingFork(t *testing.T) {
	ctx := context.Background()
	svc := newMemoryServiceForTest(t)
	source, err := svc.CreateMemory(ctx, "wsp_1", "owner", MemoryCreateMutation{Name: "Shared Runbooks", RepoNameOverride: "shared-runbooks"})
	require.NoError(t, err)
	_, err = svc.UpsertMemoryPermission(ctx, "wsp_1", "owner", source.ID, MemoryPermissionGrantMutation{SubjectType: "user", SubjectID: "reader", Role: "view"})
	require.NoError(t, err)

	fork, err := svc.ForkMemory(ctx, "wsp_1", "reader", source.ID, MemoryForkMutation{Name: "Reader Runbooks", RepoNameOverride: "reader-runbooks"})
	require.NoError(t, err)
	require.NotEqual(t, source.ID, fork.ID)
	require.Equal(t, "reader", fork.CreatedBy)
	require.Equal(t, "Reader Runbooks", fork.Name)
	require.Equal(t, "owner", fork.Role)
	require.True(t, fork.CanManage)

	sourceAfter, err := svc.GetMemory(ctx, "wsp_1", "owner", source.ID)
	require.NoError(t, err)
	require.Equal(t, "owner", sourceAfter.CreatedBy)
	require.Equal(t, "shared-runbooks", sourceAfter.Slug)
	_, err = svc.GetMemory(ctx, "wsp_1", "owner", fork.ID)
	require.ErrorIs(t, err, ErrForbidden)

	binding, err := svc.BindAgentMemory(ctx, "wsp_1", "reader", "agt_reader", AgentMemoryBindingMutation{MemoryID: fork.ID, Branch: "main"})
	require.NoError(t, err)
	require.Equal(t, fork.ID, binding.MemoryID)

	sourceRepos, err := svc.GetMemoryRepositories(ctx, "wsp_1", "reader", source.ID)
	require.NoError(t, err)
	forkRepos, err := svc.GetMemoryRepositories(ctx, "wsp_1", "reader", fork.ID)
	require.NoError(t, err)
	require.NotNil(t, sourceRepos.Primary)
	require.NotNil(t, forkRepos.Primary)
	require.Equal(t, source.ID, sourceRepos.Primary.ResourceID)
	require.Equal(t, fork.ID, forkRepos.Primary.ResourceID)
	require.NotEqual(t, sourceRepos.Primary.RepoFullName, forkRepos.Primary.RepoFullName)
	require.Contains(t, forkRepos.Primary.RepoName, "reader-runbooks")

	wikiStore, ok := svc.repositories.(*fakeAGSRepositoryStore)
	require.True(t, ok)
	require.Len(t, wikiStore.memoryForks, 1)
	require.Equal(t, sourceRepos.Primary.RepoFullName, wikiStore.memoryForks[0].Source.FullName)
	require.Equal(t, forkRepos.Primary.RepoFullName, wikiStore.memoryForks[0].Target.FullName)
}

func TestMemoryServiceReturnsErrorWhenRepositoryInitializationFails(t *testing.T) {
	ctx := context.Background()
	db, err := gormtest.OpenSQLiteDB(t)
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(&memorystore.Memory{}, &memorystore.MemoryContentCheckpoint{}, &memorystore.MemoryGrant{}, &memorystore.MemoryPin{}, &memorystore.AgentAssetAccessChangeOutbox{}, &resourcerepostore.ResourceRepository{}, &resourcerepostore.ResourceRepositoryAccessGrant{}, &resourcerepostore.ResourceRepositoryLifecycleEvent{}, &activitystore.ResourceActivityEvent{}, &activitystore.ResourceActivityLatestCommit{}))
	wikiStore := &fakeAGSRepositoryStore{memoryInitErr: errors.New("push denied")}
	svc := NewMemoryService(memorystore.New(db), resourcerepostore.New(db), wikiStore, func() time.Time { return time.Date(2026, 6, 1, 1, 2, 3, 0, time.UTC) })

	_, err = svc.CreateMemory(ctx, "ws-1", "usr_1", MemoryCreateMutation{Name: "Runbooks", TemplateLocale: "zh-CN"})
	require.Error(t, err)
	require.Contains(t, err.Error(), "initialize memory repository")
	require.Len(t, wikiStore.ensureRequests, 1)
	require.Len(t, wikiStore.memoryInits, 1)
	lifecycleEvents, err := resourcerepostore.New(db).ListLifecycleEventsForResource(ctx, "ws-1", resourcerepostore.ResourceTypeMemory, wikiStore.ensureRequests[0].ResourceID)
	require.NoError(t, err)
	require.Len(t, lifecycleEvents, 1)
	require.Equal(t, resourcerepostore.ResourceRepositoryLifecycleOperationCreateBind, lifecycleEvents[0].Operation)
	require.Equal(t, resourcerepostore.ResourceRepositoryLifecycleStatusFailed, lifecycleEvents[0].Status)
	require.Contains(t, lifecycleEvents[0].LastError, "push denied")

	page, err := memorystore.New(db).ListMemoryCollection(ctx, memorystore.MemoryCollectionOptions{WorkspaceID: "ws-1", PrincipalID: "usr_1", View: "all", Page: 1, PerPage: 20})
	require.NoError(t, err)
	require.Empty(t, page.Items)
}

func TestMemoryServiceBindsAgentMemoryWithinOwnedScope(t *testing.T) {
	ctx := context.Background()
	svc := newMemoryServiceForTest(t)
	keep, err := svc.CreateMemory(ctx, "wsp_1", "owner", MemoryCreateMutation{Name: "Keep"})
	require.NoError(t, err)
	remove, err := svc.CreateMemory(ctx, "wsp_1", "owner", MemoryCreateMutation{Name: "Remove"})
	require.NoError(t, err)
	unmanageable, err := svc.CreateMemory(ctx, "wsp_1", "other-owner", MemoryCreateMutation{Name: "Other"})
	require.NoError(t, err)
	_, err = svc.BindAgentMemory(ctx, "wsp_1", "owner", "agt_reader", AgentMemoryBindingMutation{MemoryID: remove.ID, Branch: "main"})
	require.NoError(t, err)

	result, err := svc.BindAgentMemory(ctx, "wsp_1", "owner", "agt_reader", AgentMemoryBindingMutation{MemoryID: keep.ID, Branch: "main"})
	require.NoError(t, err)
	require.Equal(t, "agt_reader", result.AgentID)
	require.Equal(t, keep.ID, result.MemoryID)
	require.Equal(t, remove.ID, result.PreviousMemoryID)

	item, err := svc.GetAgentAccessibleMemory(ctx, "wsp_1", "owner", "agt_reader")
	require.NoError(t, err)
	require.NotNil(t, item)
	require.Equal(t, keep.ID, item.ID)

	_, err = svc.BindAgentMemory(ctx, "wsp_1", "owner", "agt_reader", AgentMemoryBindingMutation{MemoryID: unmanageable.ID, Branch: "main"})
	require.ErrorIs(t, err, ErrForbidden)
}

func TestMemoryServiceBindAndUnbindEnqueueAgentAssetAccessEvents(t *testing.T) {
	ctx := context.Background()
	svc := newMemoryServiceForTest(t)
	m, err := svc.CreateMemory(ctx, "wsp_1", "owner", MemoryCreateMutation{Name: "Agent Runbooks"})
	require.NoError(t, err)

	_, err = svc.BindAgentMemory(ctx, "wsp_1", "owner", "agt_alice", AgentMemoryBindingMutation{MemoryID: m.ID, Branch: "main"})
	require.NoError(t, err)

	leased, err := svc.LeaseAgentAssetAccessEvents(ctx, AgentAssetAccessEventLeaseOptions{LeaseOwner: "daemon-1", Limit: 10})
	require.NoError(t, err)
	require.Len(t, leased, 1)
	require.Equal(t, "wsp_1", leased[0].WorkspaceID)
	require.Equal(t, []string{"agt_alice"}, leased[0].AgentIDs)
	require.Equal(t, []string{memorystore.AssetTypeMemory}, leased[0].AssetTypes)
	require.Equal(t, memorystore.AgentAssetAccessSourceOutbox, leased[0].Source)
	require.NotZero(t, leased[0].Version)
	require.NotZero(t, leased[0].OccurredAt)
	bindVersion := leased[0].Version
	updated, err := svc.MarkAgentAssetAccessEventDelivered(ctx, leased[0].EventID, "daemon-1", time.Time{})
	require.NoError(t, err)
	require.True(t, updated)

	unbindResult, err := svc.UnbindAgentMemory(ctx, "wsp_1", "owner", "agt_alice")
	require.NoError(t, err)
	require.True(t, unbindResult.Unbound)

	leased, err = svc.LeaseAgentAssetAccessEvents(ctx, AgentAssetAccessEventLeaseOptions{LeaseOwner: "daemon-2", Limit: 10})
	require.NoError(t, err)
	require.Len(t, leased, 1)
	require.Equal(t, []string{"agt_alice"}, leased[0].AgentIDs)
	require.Equal(t, []string{memorystore.AssetTypeMemory}, leased[0].AssetTypes)
	require.Greater(t, leased[0].Version, bindVersion)
}

func TestMemoryServiceAgentMemoryBindingIsOneToOne(t *testing.T) {
	ctx := context.Background()
	svc := newMemoryServiceForTest(t)
	m, err := svc.CreateMemory(ctx, "wsp_1", "owner", MemoryCreateMutation{Name: "One Agent Memory"})
	require.NoError(t, err)

	result, err := svc.BindAgentMemory(ctx, "wsp_1", "owner", "agt_alice", AgentMemoryBindingMutation{
		MemoryID:                   m.ID,
		Branch:                     "main",
		ImportExistingLocalContent: true,
	})
	require.NoError(t, err)
	require.Equal(t, m.ID, result.MemoryID)

	item, err := svc.GetAgentAccessibleMemory(ctx, "wsp_1", "owner", "agt_alice")
	require.NoError(t, err)
	require.NotNil(t, item)
	require.Equal(t, "main", item.Branch)
	require.True(t, item.ImportExistingLocalContent)

	summary, err := svc.ListAgentMemoryBindings(ctx, "wsp_1", "owner")
	require.NoError(t, err)
	require.Len(t, summary.Bindings, 1)
	require.Equal(t, "agt_alice", summary.Bindings[0].AgentID)
	require.Equal(t, m.ID, summary.Bindings[0].MemoryID)
	require.Equal(t, "main", summary.Bindings[0].Branch)
	require.True(t, summary.Bindings[0].ImportExistingLocalContent)

	memoryByID := map[string]AgentMemoryBindingMemory{}
	for _, memory := range summary.Memories {
		memoryByID[memory.ID] = memory
	}
	require.Contains(t, memoryByID, m.ID)
	require.False(t, memoryByID[m.ID].CanBind)
	require.Equal(t, "agt_alice", memoryByID[m.ID].BoundAgentID)
	require.Equal(t, "main", memoryByID[m.ID].BoundBranch)
	require.True(t, memoryByID[m.ID].ImportExistingLocalContent)

	_, err = svc.BindAgentMemory(ctx, "wsp_1", "owner", "agt_missing_branch", AgentMemoryBindingMutation{MemoryID: m.ID})
	require.ErrorIs(t, err, ErrInvalidArgument)

	// Rebinding the memory to a different agent auto-displaces the prior binding
	// (the memory<->agent relationship stays 1:1) instead of returning a conflict.
	rebindResult, err := svc.BindAgentMemory(ctx, "wsp_1", "owner", "agt_bob", AgentMemoryBindingMutation{MemoryID: m.ID, Branch: "main"})
	require.NoError(t, err)
	require.Equal(t, m.ID, rebindResult.MemoryID)

	// agt_bob now holds the binding...
	bobItem, err := svc.GetAgentAccessibleMemory(ctx, "wsp_1", "owner", "agt_bob")
	require.NoError(t, err)
	require.NotNil(t, bobItem)
	require.Equal(t, "main", bobItem.Branch)

	// ...and agt_alice's prior binding was displaced. The relationship stays
	// one-to-one: exactly one binding remains, now for agt_bob.
	summary, err = svc.ListAgentMemoryBindings(ctx, "wsp_1", "owner")
	require.NoError(t, err)
	require.Len(t, summary.Bindings, 1)
	require.Equal(t, "agt_bob", summary.Bindings[0].AgentID)
}

func TestMemoryServiceAgentMemoryBindingRequiresOwnedAgent(t *testing.T) {
	ctx := context.Background()
	svc := newMemoryServiceForTestWithOwnedAgents(t, []AgentOwnership{{ID: "agt_alice", Name: "alice"}})
	m, err := svc.CreateMemory(ctx, "wsp_1", "owner", MemoryCreateMutation{Name: "One Agent Memory"})
	require.NoError(t, err)

	_, err = svc.BindAgentMemory(ctx, "wsp_1", "owner", "agt_alice", AgentMemoryBindingMutation{MemoryID: m.ID, Branch: "main"})
	require.NoError(t, err)
	_, err = svc.BindAgentMemory(ctx, "wsp_1", "owner", "agt_bob", AgentMemoryBindingMutation{MemoryID: m.ID, Branch: "main"})
	require.ErrorIs(t, err, ErrForbidden)
}

func TestMemoryServiceListAgentMemoryBindingsReturnsOwnedAgentBindingsAndMemoryOccupancy(t *testing.T) {
	ctx := context.Background()
	svc := newMemoryServiceForTestWithOwnedAgents(t, []AgentOwnership{
		{ID: "agt_alice", Name: "alice", DisplayName: "Alice Agent"},
		{ID: "agt_bob", Name: "bob", DisplayName: "Bob Agent"},
	})
	aliceMemory, err := svc.CreateMemory(ctx, "wsp_1", "owner", MemoryCreateMutation{Name: "Alice Runbooks"})
	require.NoError(t, err)
	bobMemory, err := svc.CreateMemory(ctx, "wsp_1", "owner", MemoryCreateMutation{Name: "Bob Notes"})
	require.NoError(t, err)
	viewOnlyMemory, err := svc.CreateMemory(ctx, "wsp_1", "other-owner", MemoryCreateMutation{Name: "View Only"})
	require.NoError(t, err)
	_, err = svc.UpsertMemoryPermission(ctx, "wsp_1", "other-owner", viewOnlyMemory.ID, MemoryPermissionGrantMutation{SubjectType: "user", SubjectID: "owner", Role: "view"})
	require.NoError(t, err)
	_, err = svc.BindAgentMemory(ctx, "wsp_1", "owner", "agt_alice", AgentMemoryBindingMutation{MemoryID: aliceMemory.ID, Branch: "main", ImportExistingLocalContent: true})
	require.NoError(t, err)
	_, err = svc.BindAgentMemory(ctx, "wsp_1", "owner", "agt_bob", AgentMemoryBindingMutation{MemoryID: bobMemory.ID, Branch: "dev"})
	require.NoError(t, err)

	_, err = svc.repositoryStore.Upsert(ctx, resourcerepostore.ResourceRepository{
		ID:           "repo_alice",
		WorkspaceID:  "wsp_1",
		ResourceType: resourcerepostore.ResourceTypeMemory,
		ResourceID:   aliceMemory.ID,
		RepoRole:     resourcerepostore.RepoRolePrimary,
		RepoFullName: "usr_1/alice-runbooks.mem",
		DefaultRef:   "main",
		CreatedBy:    "owner",
		UpdatedBy:    "owner",
	})
	require.NoError(t, err)

	summary, err := svc.ListAgentMemoryBindings(ctx, "wsp_1", "owner")
	require.NoError(t, err)
	require.Len(t, summary.OwnedAgents, 2)
	require.Len(t, summary.Bindings, 2)
	bindingByAgent := map[string]AgentMemoryBindingSummary{}
	for _, binding := range summary.Bindings {
		bindingByAgent[binding.AgentID] = binding
	}
	require.Contains(t, bindingByAgent, "agt_alice")
	require.Equal(t, aliceMemory.ID, bindingByAgent["agt_alice"].MemoryID)
	require.True(t, bindingByAgent["agt_alice"].ImportExistingLocalContent)
	require.Contains(t, bindingByAgent, "agt_bob")
	require.Equal(t, bobMemory.ID, bindingByAgent["agt_bob"].MemoryID)
	require.Len(t, summary.Memories, 3)
	memoryByID := map[string]AgentMemoryBindingMemory{}
	for _, memory := range summary.Memories {
		memoryByID[memory.ID] = memory
	}
	require.False(t, memoryByID[aliceMemory.ID].CanBind)
	require.Equal(t, "agt_alice", memoryByID[aliceMemory.ID].BoundAgentID)
	require.Equal(t, "Alice Agent", memoryByID[aliceMemory.ID].BoundAgentName)
	require.Equal(t, "main", memoryByID[aliceMemory.ID].BoundBranch)
	require.NotNil(t, memoryByID[aliceMemory.ID].Repository)
	require.Equal(t, "usr_1/alice-runbooks.mem", memoryByID[aliceMemory.ID].Repository.RepoFullName)
	require.False(t, memoryByID[viewOnlyMemory.ID].CanBind)
	require.Empty(t, memoryByID[viewOnlyMemory.ID].BoundAgentID)
}

func TestMemoryServiceBatchListAgentAccessibleMemories(t *testing.T) {
	ctx := context.Background()
	svc := newMemoryServiceForTest(t)
	issuer := &fakeRepositorySyncTokenIssuer{
		lease: reposservice.RepositorySyncTokenLease{
			Token:     "ags_rat_memory",
			ExpiresAt: time.Date(2026, 6, 15, 10, 0, 0, 0, time.UTC),
		},
	}
	svc.syncTokenIssuer = issuer
	aliceMemory, err := svc.CreateMemory(ctx, "wsp_1", "owner", MemoryCreateMutation{Name: "Alice Runbooks"})
	require.NoError(t, err)
	bobMemory, err := svc.CreateMemory(ctx, "wsp_1", "owner", MemoryCreateMutation{Name: "Bob Notes"})
	require.NoError(t, err)
	_, err = svc.BindAgentMemory(ctx, "wsp_1", "owner", "agt_alice", AgentMemoryBindingMutation{MemoryID: aliceMemory.ID, Branch: "main"})
	require.NoError(t, err)
	_, err = svc.BindAgentMemory(ctx, "wsp_1", "owner", "agt_bob", AgentMemoryBindingMutation{MemoryID: bobMemory.ID, Branch: "agent-bob"})
	require.NoError(t, err)

	// Inject primary repository refs so we can verify the batch path populates
	// the Repository field without going through the full AGS ensure flow.
	repoStore := svc.repositoryStore
	_, err = repoStore.Upsert(ctx, resourcerepostore.ResourceRepository{
		ID:           "repo_alice",
		WorkspaceID:  "wsp_1",
		ResourceType: resourcerepostore.ResourceTypeMemory,
		ResourceID:   aliceMemory.ID,
		RepoRole:     resourcerepostore.RepoRolePrimary,
		OwnerKind:    "mem",
		RepoOwner:    "usr_1",
		RepoName:     "alice-runbooks",
		RepoFullName: "usr_1/alice-runbooks.mem",
		AGSRepoID:    "ags_repo_alice",
		DefaultRef:   "main",
		CreatedBy:    "owner",
		UpdatedBy:    "owner",
	})
	require.NoError(t, err)

	result, err := svc.BatchListAgentAccessibleMemories(ctx, "wsp_1", "owner", []string{"agt_alice", "agt_bob", "agt_unseen"})
	require.NoError(t, err)
	require.Equal(t, "ags_rat_memory", result.SyncToken)
	require.Equal(t, issuer.lease.ExpiresAt, result.SyncTokenExpires)
	require.Len(t, issuer.requests, 1)
	require.ElementsMatch(t, []string{"usr_1/alice-runbooks.mem", "owner/bob-notes.mem"}, issuer.requests[0].Repositories)
	require.ElementsMatch(t, []string{reposservice.RepositorySyncCapabilityRead, reposservice.RepositorySyncCapabilityClone, reposservice.RepositorySyncCapabilityWrite}, issuer.requests[0].Capabilities)

	require.Contains(t, result.Agents, "agt_alice")
	require.NotNil(t, result.Agents["agt_alice"])
	aliceItem := result.Agents["agt_alice"]
	require.Equal(t, aliceMemory.ID, aliceItem.ID)
	require.Equal(t, MemorySyncProtocolLegacyGitV1, aliceItem.SyncProtocol)
	require.Equal(t, "main", aliceItem.Branch)
	require.NotNil(t, aliceItem.Repository)
	require.Equal(t, "usr_1/alice-runbooks.mem", aliceItem.Repository.RepoFullName)
	require.Equal(t, "ags_repo_alice", aliceItem.Repository.AGSRepoID)

	require.Contains(t, result.Agents, "agt_bob")
	require.NotNil(t, result.Agents["agt_bob"])
	require.Equal(t, bobMemory.ID, result.Agents["agt_bob"].ID)
	require.Equal(t, "agent-bob", result.Agents["agt_bob"].Branch)

	// Agent with no binding maps to nil so Desktop can distinguish
	// "no access" from "agent dropped from response".
	require.Contains(t, result.Agents, "agt_unseen")
	require.Nil(t, result.Agents["agt_unseen"])
}

func TestMemoryServiceBatchAccessSelectsNativeProtocolWithoutAGSCredential(t *testing.T) {
	ctx := context.Background()
	svc, db, seed := newNativeMemoryServiceTest(t)
	require.NoError(t, db.AutoMigrate(&activitystore.ResourceActivityEvent{}))
	require.False(t, db.Migrator().HasTable(&activitystore.ResourceActivityLatestCommit{}), "native-only access must not require or query the legacy latest-commit projection")
	agentID := "agt_native"
	require.NoError(t, db.Model(&memorystore.Memory{}).Where("id = ?", "mem_1").Updates(map[string]any{
		"agent_id": agentID,
	}).Error)
	svc.agentOwnershipSource = fakeAgentOwnershipSource{agents: []AgentOwnership{{ID: agentID, Name: "native"}}}
	issuer := &fakeRepositorySyncTokenIssuer{lease: reposservice.RepositorySyncTokenLease{Token: "must-not-be-issued"}}
	svc.syncTokenIssuer = issuer

	result, err := svc.BatchListAgentAccessibleMemories(ctx, "wsp_1", "usr_1", []string{agentID})
	require.NoError(t, err)
	require.Empty(t, result.SyncToken)
	require.Empty(t, issuer.requests, "native sync must not mint an AGS repository credential")

	item := result.Agents[agentID]
	require.NotNil(t, item)
	require.Equal(t, MemorySyncProtocolNativeContentV1, item.SyncProtocol)
	require.Nil(t, item.Repository)
	require.Empty(t, item.Branch)
	require.Equal(t, "ctn_1", item.ContentID)
	require.Equal(t, seed.VersionID, item.ContentVersionID)
	require.EqualValues(t, 1, item.ContentGeneration)
	require.Zero(t, item.AcknowledgedSequence)
}

func TestMemoryServiceBatchListAgentAccessibleMemoriesDeduplicatesAndIgnoresBlanks(t *testing.T) {
	ctx := context.Background()
	svc := newMemoryServiceForTest(t)
	_, err := svc.BatchListAgentAccessibleMemories(ctx, "wsp_1", "owner", []string{"agt_alice", "  ", "", "agt_alice"})
	require.NoError(t, err)
}

// TestGetAgentAccessibleMemoryRejectsNonOwnedAgent locks in the D-2 fix: a
// caller requesting another user's agent binding must be rejected with
// ErrForbidden, regardless of whether the agent currently has a binding.
func TestGetAgentAccessibleMemoryRejectsNonOwnedAgent(t *testing.T) {
	ctx := context.Background()
	svc := newMemoryServiceForTest(t)
	memory, err := svc.CreateMemory(ctx, "wsp_1", "owner", MemoryCreateMutation{Name: "Runbooks"})
	require.NoError(t, err)
	_, err = svc.BindAgentMemory(ctx, "wsp_1", "owner", "agt_alice", AgentMemoryBindingMutation{MemoryID: memory.ID, Branch: "main"})
	require.NoError(t, err)

	// Swap in a stricter ownership source whose subject only owns agt_alice.
	svc.agentOwnershipSource = fakeAgentOwnershipSource{agents: []AgentOwnership{{ID: "agt_alice", Name: "alice"}}}
	_, err = svc.GetAgentAccessibleMemory(ctx, "wsp_1", "owner", "agt_alice")
	require.NoError(t, err)
	_, err = svc.GetAgentAccessibleMemory(ctx, "wsp_1", "owner", "agt_bob")
	require.ErrorIs(t, err, ErrForbidden)
}

// TestBatchListAgentAccessibleMemoriesRejectsNonOwnedAgent locks in the
// fail-closed semantics of the D-2 batch fix: if any of the requested agent
// IDs is not owned by the caller, the whole request is rejected.
func TestBatchListAgentAccessibleMemoriesRejectsNonOwnedAgent(t *testing.T) {
	ctx := context.Background()
	svc := newMemoryServiceForTest(t)
	memory, err := svc.CreateMemory(ctx, "wsp_1", "owner", MemoryCreateMutation{Name: "Runbooks"})
	require.NoError(t, err)
	_, err = svc.BindAgentMemory(ctx, "wsp_1", "owner", "agt_alice", AgentMemoryBindingMutation{MemoryID: memory.ID, Branch: "main"})
	require.NoError(t, err)

	// Stricter ownership source: caller owns agt_alice only.
	svc.agentOwnershipSource = fakeAgentOwnershipSource{agents: []AgentOwnership{{ID: "agt_alice", Name: "alice"}}}
	_, err = svc.BatchListAgentAccessibleMemories(ctx, "wsp_1", "owner", []string{"agt_alice"})
	require.NoError(t, err)
	_, err = svc.BatchListAgentAccessibleMemories(ctx, "wsp_1", "owner", []string{"agt_alice", "agt_bob"})
	require.ErrorIs(t, err, ErrForbidden)
	_, err = svc.BatchListAgentAccessibleMemories(ctx, "wsp_1", "owner", []string{"agt_bob"})
	require.ErrorIs(t, err, ErrForbidden)
}

// TestListMemoryPermissionsRequiresManageRole locks in the C-1 fix: only
// owner/manage roles can list, grant, or revoke memory permissions. The edit
// role must be rejected even though it can otherwise mutate memory content.
func TestListMemoryPermissionsRequiresManageRole(t *testing.T) {
	ctx := context.Background()
	svc := newMemoryServiceForTest(t)
	memory, err := svc.CreateMemory(ctx, "wsp_1", "owner", MemoryCreateMutation{Name: "Runbooks"})
	require.NoError(t, err)
	_, err = svc.UpsertMemoryPermission(ctx, "wsp_1", "owner", memory.ID, MemoryPermissionGrantMutation{SubjectType: "user", SubjectID: "editor", Role: "edit"})
	require.NoError(t, err)

	_, err = svc.ListMemoryPermissions(ctx, "wsp_1", "editor", memory.ID)
	require.ErrorIs(t, err, ErrForbidden)
	_, err = svc.UpsertMemoryPermission(ctx, "wsp_1", "editor", memory.ID, MemoryPermissionGrantMutation{SubjectType: "user", SubjectID: "viewer", Role: "view"})
	require.ErrorIs(t, err, ErrForbidden)
	// editor's own grant cannot be removed by editor either.
	grants, err := svc.store.ListMemoryGrants(ctx, "wsp_1", memory.ID)
	require.NoError(t, err)
	var editorGrantID string
	for _, grant := range grants {
		if grant.SubjectID == "editor" {
			editorGrantID = grant.ID
		}
	}
	require.NotEmpty(t, editorGrantID)
	err = svc.DeleteMemoryPermission(ctx, "wsp_1", "editor", memory.ID, editorGrantID)
	require.ErrorIs(t, err, ErrForbidden)
}
