package kb

import (
	"context"
	"encoding/json"
	"errors"
	"sort"
	"testing"
	"time"

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

	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/AgentTeam/internal/store/content"
	"github.com/tidbcloud/AgentTeam/internal/store/kb"
	activitystore "github.com/tidbcloud/AgentTeam/internal/store/resource/activity"
	memorystore "github.com/tidbcloud/AgentTeam/internal/store/resource/memory"
	reposstore "github.com/tidbcloud/AgentTeam/internal/store/resource/repos"
	"gorm.io/gorm"
)

func newKBServiceForTest(t *testing.T) *KBService {
	t.Helper()
	db, err := openServiceTestSQLiteDB(t)
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(
		&kb.KnowledgeBase{},
		&kb.KnowledgeBaseGroup{},
		&kb.KnowledgeBaseTag{},
		&kb.KnowledgeBaseTagAssignment{},
		&kb.KnowledgeBasePin{},
		&kb.KnowledgeBaseGrant{},
		&kb.KnowledgeBaseSourceRepositoryAccessOverride{},
		&kb.KnowledgeBaseSetupSession{},
		&kb.KnowledgeBaseSetupSourceArtifact{},
		&kb.KnowledgeBaseGenerationRequest{},
		&kb.KnowledgeBasePublicationRequest{},
		&kb.Source{},
		&kb.SourceMirror{},

		&kb.AnalysisRun{},
		&kb.SourceRevision{},
		&kb.Job{},
		&kb.AuditEvent{},
		&reposstore.ResourceRepository{},
		&reposstore.ResourceRepositoryLifecycleEvent{},
		&reposstore.ResourceRepositoryAccessGrant{},
		&activitystore.ResourceActivityEvent{},
		&activitystore.ResourceActivityLatestCommit{},
		&activitystore.ResourceActivityProjectorCheckpoint{},
		&memorystore.AgentAssetAccessChangeOutbox{},
	))
	fixed := time.Date(2026, 6, 1, 1, 2, 3, 0, time.UTC)
	return NewKBServiceWithOptions(kb.New(db), func() time.Time { return fixed }, KBServiceOptions{
		WikiStore:            &fakeAGSRepositoryStore{},
		RepositoryStore:      reposstore.New(db),
		AgentOwnershipSource: fakeAgentOwnershipSource{agents: []memory.AgentOwnership{{ID: "agt_reader"}, {ID: "agt_other"}}},
		AssetAccessOutbox:    memorystore.New(db),
		WorkspaceGovernanceAuthorizer: fakeWorkspaceGovernanceAuthorizer{authorize: func(_ context.Context, _, principalID string) (bool, error) {
			return principalID == "owner", nil
		}},
	})
}

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

func (i *fakeKBRepositorySyncTokenIssuer) 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 fakeKBActivityProjector struct {
	repos []reposstore.ResourceRepository
	opts  []activity.ResourceActivityProjectorRefreshOptions
}

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

func TestKBServiceCreatesMetadataOnlyKnowledgeBase(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)

	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "user-1", KBMutation{Name: "Runbooks"})

	require.NoError(t, err)
	require.Equal(t, "runbooks", base.Slug)
	require.Equal(t, reposservice.RepositoryStorageScopePersonal, base.StorageScope)
	require.Equal(t, "user-1", base.StorageOwnerLogin)
	require.Equal(t, "view", base.DefaultWorkspaceMemberRole)
	require.False(t, base.AllowKnowledgeQAWithoutSourceAccess)
	require.Equal(t, "{}", base.RetrievalPolicy)
	require.Equal(t, "{}", base.SourceCollectionState)
}

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

	base, err := svc.store.CreateKnowledgeBase(ctx, kb.KnowledgeBase{ID: "kb_visible", WorkspaceID: "wsp_1", Name: "Visible KB", Description: "A", IsActive: true, DefaultWorkspaceMemberRole: "view", CreatedBy: "owner", UpdatedBy: "owner", CreatedAt: now, UpdatedAt: now.Add(2 * time.Minute)})
	require.NoError(t, err)
	_, err = svc.store.CreateKnowledgeBase(ctx, kb.KnowledgeBase{ID: "kb_other", WorkspaceID: "wsp_1", Name: "Other KB", Description: "B", IsActive: true, DefaultWorkspaceMemberRole: "view", CreatedBy: "other", UpdatedBy: "other", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	visibleSource, err := svc.store.CreateSource(ctx, kb.Source{ID: "src_visible", WorkspaceID: "wsp_1", KnowledgeBaseID: base.ID, SourceType: "codebase", Status: KBSourceStatusReady, Name: "AgentTeam", DefaultRef: "main", SyncStatus: KBSourceStatusReady, IndexStatus: KBSourceStatusReady, Config: "{}", CreatedBy: "owner", UpdatedBy: "owner", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	hiddenSource, err := svc.store.CreateSource(ctx, kb.Source{ID: "src_other", WorkspaceID: "wsp_1", KnowledgeBaseID: "kb_other", SourceType: "codebase", Status: KBSourceStatusReady, Name: "Other", DefaultRef: "main", SyncStatus: KBSourceStatusReady, IndexStatus: KBSourceStatusReady, Config: "{}", CreatedBy: "other", UpdatedBy: "other", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	_, err = svc.repositoryStore.Upsert(ctx, reposstore.ResourceRepository{ID: "rr_src_visible", WorkspaceID: "wsp_1", ResourceType: reposstore.ResourceTypeKBSource, ResourceID: visibleSource.ID, RepoRole: reposstore.RepoRolePrimary, OwnerKind: "personal", RepoOwner: "owner", RepoName: "agentteam.kb", RepoFullName: "owner/agentteam.kb", CreatedBy: "owner", UpdatedBy: "owner", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	_, err = svc.repositoryStore.Upsert(ctx, reposstore.ResourceRepository{ID: "rr_src_other", WorkspaceID: "wsp_1", ResourceType: reposstore.ResourceTypeKBSource, ResourceID: hiddenSource.ID, RepoRole: reposstore.RepoRolePrimary, OwnerKind: "personal", RepoOwner: "other", RepoName: "other.kb", RepoFullName: "other/other.kb", CreatedBy: "other", UpdatedBy: "other", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	pageTwoBase, err := svc.store.CreateKnowledgeBase(ctx, kb.KnowledgeBase{ID: "kb_page_two", WorkspaceID: "wsp_1", Name: "Page two", Description: "C", IsActive: true, DefaultWorkspaceMemberRole: "none", CreatedBy: "owner", UpdatedBy: "owner", CreatedAt: now, UpdatedAt: now.Add(-time.Hour)})
	require.NoError(t, err)
	pageTwoSource, err := svc.store.CreateSource(ctx, kb.Source{ID: "src_page_two", WorkspaceID: "wsp_1", KnowledgeBaseID: pageTwoBase.ID, SourceType: "codebase", Status: KBSourceStatusReady, Name: "Page two", DefaultRef: "main", SyncStatus: KBSourceStatusReady, IndexStatus: KBSourceStatusReady, Config: "{}", CreatedBy: "owner", UpdatedBy: "owner", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	_, err = svc.repositoryStore.Upsert(ctx, reposstore.ResourceRepository{ID: "rr_src_page_two", WorkspaceID: "wsp_1", ResourceType: reposstore.ResourceTypeKBSource, ResourceID: pageTwoSource.ID, RepoRole: reposstore.RepoRolePrimary, OwnerKind: "personal", RepoOwner: "owner", RepoName: "page-two.kb", RepoFullName: "owner/page-two.kb", CreatedBy: "owner", UpdatedBy: "owner", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)

	page, err := svc.ListKnowledgeBases(ctx, "wsp_1", "owner", "all", "", nil, 1, 1)
	require.NoError(t, err)
	require.Len(t, page.Items, 1)
	require.Equal(t, base.ID, page.Items[0].ID)
	require.True(t, page.HasMore)
	require.Len(t, projector.repos, 1)
	require.Equal(t, visibleSource.ID, projector.repos[0].ResourceID)
	require.NotEqual(t, pageTwoSource.ID, projector.repos[0].ResourceID, "list refresh must stay bounded to sources on the requested page")
	require.Equal(t, reposstore.ResourceTypeKBSource, projector.repos[0].ResourceType)
	require.Equal(t, 30*time.Minute, projector.opts[0].RefreshAfter)

	projector.repos = nil
	projector.opts = nil
	sources, err := svc.ListSources(ctx, "wsp_1", "owner", base.ID, 1, 20)
	require.NoError(t, err)
	require.Len(t, sources, 1)
	require.Equal(t, visibleSource.ID, sources[0].ID)
	require.Len(t, projector.repos, 1)
	require.Equal(t, visibleSource.ID, projector.repos[0].ResourceID)
	require.Equal(t, reposstore.ResourceTypeKBSource, projector.repos[0].ResourceType)
}

func TestListRetrievableSourcesHidesSourcesWithoutCurrentRepositoryAccess(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)
	visible, err := svc.CreateSource(ctx, "wsp_1", "owner", base.ID, KBSourceMutation{SourceType: "codebase", Name: "Visible"})
	require.NoError(t, err)
	hidden, err := svc.CreateSource(ctx, "wsp_1", "owner", base.ID, KBSourceMutation{SourceType: "codebase", Name: "Hidden"})
	require.NoError(t, err)

	readProfile := kb.KnowledgeBaseRepositoryAccessProfileRead
	grant, err := svc.UpsertKnowledgeBasePermission(ctx, "wsp_1", "owner", base.ID, KBPermissionGrantMutation{
		SubjectType:             "user",
		SubjectID:               "viewer",
		Role:                    "view",
		RepositoryAccessProfile: &readProfile,
	})
	require.NoError(t, err)
	noneProfile := kb.KnowledgeBaseRepositoryAccessProfileNone
	_, err = svc.UpdateKnowledgeBasePermissionRepositoryOverrides(ctx, "wsp_1", "owner", base.ID, grant.ID, []KBPermissionRepositoryOverrideMutation{
		{SourceID: hidden.ID, RepoRole: reposstore.RepoRolePrimary, RepositoryAccessProfile: &noneProfile},
		{SourceID: hidden.ID, RepoRole: reposstore.RepoRoleInsights, RepositoryAccessProfile: &noneProfile},
	})
	require.NoError(t, err)

	sources, err := svc.ListRetrievableSources(ctx, "wsp_1", "viewer", base.ID, 1, 100)
	require.NoError(t, err)
	require.Len(t, sources, 1)
	require.Equal(t, visible.ID, sources[0].ID)
	require.NotEqual(t, hidden.Name, sources[0].Name)
}

func TestKnowledgeQASourceAccessPolicyDoesNotGrantDirectRepositoryAccess(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)
	source, err := svc.CreateSource(ctx, "wsp_1", "owner", base.ID, KBSourceMutation{SourceType: "codebase", Name: "AgentTeam"})
	require.NoError(t, err)
	grant, err := svc.UpsertKnowledgeBasePermission(ctx, "wsp_1", "owner", base.ID, KBPermissionGrantMutation{
		SubjectType: "user", SubjectID: "viewer", Role: "view", RoleSet: true,
	})
	require.NoError(t, err)
	require.Equal(t, kb.KnowledgeBaseRepositoryAccessProfileNone, grant.RepositoryAccessProfile)
	repositories, err := svc.repositoryStore.ListForResource(ctx, "wsp_1", reposstore.ResourceTypeKBSource, source.ID)
	require.NoError(t, err)
	require.NotEmpty(t, repositories)
	independentAccess, err := svc.repositoryStore.UpsertAccessGrant(ctx, reposstore.ResourceRepositoryAccessGrant{
		WorkspaceID: "wsp_1", ResourceType: reposstore.ResourceTypeKBSource,
		ResourceID: source.ID, RepoRole: repositories[0].RepoRole,
		RepoFullName: repositories[0].RepoFullName, SubjectType: "user", SubjectID: "other-viewer",
		Permission: reposstore.AccessPermissionRead, IntentType: "independent", IntentID: "manual-grant-1",
		Materialized: true, Status: reposstore.AccessStatusCurrent,
		CreatedBy: "owner", UpdatedBy: "owner", CreatedAt: svc.now(), UpdatedAt: svc.now(),
	})
	require.NoError(t, err)

	_, err = svc.ListRetrievableSources(ctx, "wsp_1", "viewer", base.ID, 1, 100)
	require.ErrorIs(t, err, ErrForbidden)
	directAccessBefore, err := svc.repositoryStore.ListAccessGrantsForIntent(
		ctx, "wsp_1", reposstore.AccessIntentTypeKBGrant, grant.ID,
	)
	require.NoError(t, err)
	require.Empty(t, directAccessBefore)

	enabled := true
	updated, err := svc.UpdateKnowledgeBase(ctx, "wsp_1", "owner", base.ID, KBMutation{
		Name: base.Name, Description: base.Description,
		AllowKnowledgeQAWithoutSourceAccess: &enabled,
	})
	require.NoError(t, err)
	require.True(t, updated.AllowKnowledgeQAWithoutSourceAccess)

	sources, err := svc.ListRetrievableSources(ctx, "wsp_1", "viewer", base.ID, 1, 100)
	require.NoError(t, err)
	require.Len(t, sources, 1)
	require.Equal(t, source.ID, sources[0].ID)
	directAccessAfter, err := svc.repositoryStore.ListAccessGrantsForIntent(
		ctx, "wsp_1", reposstore.AccessIntentTypeKBGrant, grant.ID,
	)
	require.NoError(t, err)
	require.Empty(t, directAccessAfter, "the Q&A policy must not materialize repository collaborator intent")
	preservedIndependentAccess, err := svc.repositoryStore.GetAccessGrant(ctx, "wsp_1", independentAccess.ID)
	require.NoError(t, err)
	require.True(t, preservedIndependentAccess.Materialized)
	require.Equal(t, reposstore.AccessStatusCurrent, preservedIndependentAccess.Status)

	directRetrievalBases, err := svc.resolveActingUserRetrievalBases(ctx, "wsp_1", "viewer", []string{base.ID})
	require.NoError(t, err)
	require.Empty(t, directRetrievalBases, "generic repository-backed retrieval must still require direct access")

	disabled := false
	_, err = svc.UpdateKnowledgeBase(ctx, "wsp_1", "viewer", base.ID, KBMutation{
		Name: base.Name, Description: base.Description,
		AllowKnowledgeQAWithoutSourceAccess: &disabled,
	})
	require.ErrorIs(t, err, ErrForbidden)
	unchanged, err := svc.GetKnowledgeBase(ctx, "wsp_1", "owner", base.ID)
	require.NoError(t, err)
	require.True(t, unchanged.AllowKnowledgeQAWithoutSourceAccess)
}

func TestListRetrievableSourcesHonorsExplicitDenyOverWorkspaceDefaults(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	defaultRole := "view"
	defaultProfile := kb.KnowledgeBaseRepositoryAccessProfileRead
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{
		Name:                           "Runbooks",
		DefaultWorkspaceMemberRole:     &defaultRole,
		DefaultRepositoryAccessProfile: &defaultProfile,
	})
	require.NoError(t, err)
	_, err = svc.CreateSource(ctx, "wsp_1", "owner", base.ID, KBSourceMutation{SourceType: "codebase", Name: "Private for viewer"})
	require.NoError(t, err)
	noneProfile := kb.KnowledgeBaseRepositoryAccessProfileNone
	_, err = svc.UpsertKnowledgeBasePermission(ctx, "wsp_1", "owner", base.ID, KBPermissionGrantMutation{
		SubjectType:             "user",
		SubjectID:               "viewer",
		Role:                    "none",
		RepositoryAccessProfile: &noneProfile,
	})
	require.NoError(t, err)

	_, err = svc.ListRetrievableSources(ctx, "wsp_1", "viewer", base.ID, 1, 100)
	require.ErrorIs(t, err, ErrForbidden)
}

func TestKBServiceCreatesWorkspaceScopedKnowledgeBaseSources(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)

	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "user-1", KBMutation{Name: "Runbooks", StorageScope: reposservice.RepositoryStorageScopeWorkspace})
	require.NoError(t, err)
	require.Equal(t, reposservice.RepositoryStorageScopeWorkspace, base.StorageScope)
	require.Equal(t, "wsp_1", base.StorageOwnerLogin)

	source, err := svc.CreateSource(ctx, "wsp_1", "user-1", base.ID, KBSourceMutation{
		SourceType: "codebase",
		RepoMode:   "create",
		Name:       "AgentTeam",
	})
	require.NoError(t, err)

	repos, err := svc.repositoryStore.ListForResource(ctx, "wsp_1", reposstore.ResourceTypeKBSource, source.ID)
	require.NoError(t, err)
	require.Len(t, repos, 2)
	byRole := map[string]reposstore.ResourceRepository{}
	for _, repo := range repos {
		byRole[repo.RepoRole] = repo
	}
	require.Equal(t, "wsp_1/agentteam", byRole[reposstore.RepoRolePrimary].RepoFullName)
	require.Equal(t, "wsp_1/agentteam.kb", byRole[reposstore.RepoRoleInsights].RepoFullName)
	require.Equal(t, reposservice.RepositoryStorageScopeWorkspace, byRole[reposstore.RepoRolePrimary].OwnerKind)
	require.Equal(t, reposservice.RepositoryStorageScopeWorkspace, byRole[reposstore.RepoRoleInsights].OwnerKind)
}

func TestKBServiceRejectsReservedKBSourceSuffix(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "user-1", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)

	_, err = svc.CreateSource(ctx, "wsp_1", "user-1", base.ID, KBSourceMutation{
		SourceType: "codebase",
		Name:       "AgentTeam",
		Repo: KBSourceRepositoryCreateInput{
			RepoNameOverride: "agentteam.kb",
		},
	})

	require.Error(t, err)
	require.ErrorIs(t, err, reposservice.ErrConflict)
	require.Equal(t, reposservice.RepositoryReservedSuffixCode, reposservice.Code(err))
	details, ok := reposservice.Details(err).(reposservice.RepositoryNameConflictDetails)
	require.True(t, ok)
	require.Equal(t, reposservice.RepositoryNameFieldRepoNameOverride, details.Field)
	require.Equal(t, "agentteam", details.SuggestedRepoName)
}

func TestKBServiceAutoAllocatesRepoNameAcrossResourceConflicts(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	now := svc.now()
	_, err := svc.repositoryStore.Upsert(ctx, reposstore.ResourceRepository{
		ID:           "rr_existing",
		WorkspaceID:  "wsp_1",
		ResourceType: reposstore.ResourceTypeMemory,
		ResourceID:   "mem_existing",
		RepoRole:     reposstore.RepoRolePrimary,
		OwnerKind:    reposservice.RepositoryStorageScopePersonal,
		RepoOwner:    "user-1",
		RepoName:     "agentteam",
		RepoFullName: "user-1/agentteam",
		CreatedBy:    "user-1",
		UpdatedBy:    "user-1",
		CreatedAt:    now,
		UpdatedAt:    now,
	})
	require.NoError(t, err)
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "user-1", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)

	source, err := svc.CreateSource(ctx, "wsp_1", "user-1", base.ID, KBSourceMutation{
		SourceType: "codebase",
		Name:       "AgentTeam",
	})

	require.NoError(t, err)
	repos, err := svc.repositoryStore.ListForResource(ctx, "wsp_1", reposstore.ResourceTypeKBSource, source.ID)
	require.NoError(t, err)
	require.Len(t, repos, 2)
	require.Equal(t, "user-1/agentteam-1", findSourceRepositoryForRole(repos, reposstore.RepoRolePrimary).RepoFullName)
	require.Equal(t, "user-1/agentteam-1.kb", findSourceRepositoryForRole(repos, reposstore.RepoRoleInsights).RepoFullName)
}

func TestKBServiceAutoAllocatesRepoNamesForDuplicateSourceNames(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "user-1", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)

	first, err := svc.CreateSource(ctx, "wsp_1", "user-1", base.ID, KBSourceMutation{SourceType: "codebase", Name: "Docs"})
	require.NoError(t, err)
	second, err := svc.CreateSource(ctx, "wsp_1", "user-1", base.ID, KBSourceMutation{SourceType: "codebase", Name: "Docs"})
	require.NoError(t, err)
	require.Equal(t, first.Name, second.Name)

	firstRepos, err := svc.repositoryStore.ListForResource(ctx, "wsp_1", reposstore.ResourceTypeKBSource, first.ID)
	require.NoError(t, err)
	secondRepos, err := svc.repositoryStore.ListForResource(ctx, "wsp_1", reposstore.ResourceTypeKBSource, second.ID)
	require.NoError(t, err)
	require.Equal(t, "user-1/docs", findSourceRepositoryForRole(firstRepos, reposstore.RepoRolePrimary).RepoFullName)
	require.Equal(t, "user-1/docs-1", findSourceRepositoryForRole(secondRepos, reposstore.RepoRolePrimary).RepoFullName)
	require.Equal(t, "user-1/docs-1.kb", findSourceRepositoryForRole(secondRepos, reposstore.RepoRoleInsights).RepoFullName)
}

func TestKBServiceCreateKnowledgeBaseDoesNotProvisionDefaultRepository(t *testing.T) {
	ctx := context.Background()
	db, err := openServiceTestSQLiteDB(t)
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(
		&kb.KnowledgeBase{},
		&kb.KnowledgeBaseGroup{},
		&kb.KnowledgeBaseTag{},
		&kb.KnowledgeBaseTagAssignment{},
		&kb.KnowledgeBasePin{},
		&kb.KnowledgeBaseGrant{},
		&kb.KnowledgeBaseSourceRepositoryAccessOverride{},
		&kb.Source{},
		&kb.SourceMirror{},

		&kb.AnalysisRun{},
		&kb.SourceRevision{},
		&kb.Job{},
		&kb.AuditEvent{},
	))
	fakeStore := &fakeAGSRepositoryStore{ensureErr: errors.New("ags unavailable")}
	svc := NewKBServiceWithOptions(kb.New(db), func() time.Time {
		return time.Date(2026, 6, 1, 1, 2, 3, 0, time.UTC)
	}, KBServiceOptions{WikiStore: fakeStore, RepositoryStore: reposstore.New(db)})

	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "user-1", KBMutation{Name: "Runbooks"})

	require.NoError(t, err)
	require.NotEmpty(t, base.ID)
	require.Empty(t, fakeStore.ensureRequests)
}

func TestKBServiceWorkspacePublicKnowledgeBasesAreVisibleToMembers(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	defaultRole := "view"
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks", DefaultWorkspaceMemberRole: &defaultRole})
	require.NoError(t, err)

	collection, err := svc.ListKnowledgeBases(ctx, "wsp_1", "member", "all", "", nil, 1, 20)
	require.NoError(t, err)
	require.Len(t, collection.Items, 1)
	require.Equal(t, base.ID, collection.Items[0].ID)
	require.Equal(t, "view", collection.Items[0].Role)
	require.False(t, collection.Items[0].CanManage)

	memberBase, err := svc.GetKnowledgeBase(ctx, "wsp_1", "member", base.ID)
	require.NoError(t, err)
	require.Equal(t, "view", memberBase.Role)
	require.False(t, memberBase.CanManage)

	memberPermissions, err := svc.ListKnowledgeBasePermissions(ctx, "wsp_1", "member", base.ID)
	require.NoError(t, err)
	require.False(t, memberPermissions.CanManage)
	require.Equal(t, "view", memberPermissions.MyRole)
	require.Empty(t, memberPermissions.Members)
	require.Empty(t, memberPermissions.Agents)
	require.NotNil(t, memberPermissions.Self)
	require.Equal(t, "member", memberPermissions.Self.SubjectID)
	require.Equal(t, "view", memberPermissions.Self.Role)
	_, err = svc.CreateSource(ctx, "wsp_1", "member", base.ID, KBSourceMutation{Name: "member.md", SourceType: "file_repository"})
	require.ErrorIs(t, err, ErrForbidden)
}

func TestKBServiceArchivesKnowledgeBaseWithoutClearingDescription(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "user-1", KBMutation{
		Name:        "Runbooks",
		Description: "Ops docs",
	})
	require.NoError(t, err)

	archived, err := svc.ArchiveKnowledgeBase(ctx, "wsp_1", "user-1", base.ID)

	require.NoError(t, err)
	require.False(t, archived.IsActive)
	require.Equal(t, "Ops docs", archived.Description)
}

func TestKBServiceCreatesRepositorySourcesAndAudit(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "user-1", KBMutation{Name: "Runbooks", Description: "Ops docs"})
	require.NoError(t, err)

	fileSource, err := svc.CreateSource(ctx, "wsp_1", "user-1", base.ID, KBSourceMutation{
		SourceType: "file_repository",
		RepoMode:   "create",
		Name:       "Runbook files",
		Repo: KBSourceRepositoryCreateInput{
			Name:             "Runbook files",
			RepoNameOverride: "runbook-files",
			Description:      "Runbook file repository",
			DefaultBranch:    "main",
		},
	})
	require.NoError(t, err)
	require.Equal(t, "file_repository", fileSource.SourceType)
	fileRepos, err := svc.repositoryStore.ListForResource(ctx, "wsp_1", reposstore.ResourceTypeKBSource, fileSource.ID)
	require.NoError(t, err)
	require.Len(t, fileRepos, 2)
	primaryRepo := fileRepos[0]
	for _, repo := range fileRepos {
		if repo.RepoRole == reposstore.RepoRolePrimary {
			primaryRepo = repo
		}
	}
	require.Equal(t, "user-1/runbook-files", primaryRepo.RepoFullName)

	codeSource, err := svc.CreateSource(ctx, "wsp_1", "user-1", base.ID, KBSourceMutation{
		SourceType:   "codebase",
		Name:         "AgentTeam",
		DefaultRef:   "main",
		IndexingMode: "repo_wiki_and_files",
	})
	require.NoError(t, err)
	require.Equal(t, "codebase", codeSource.SourceType)
	codeRevision, err := svc.store.GetCurrentSourceRevision(ctx, "wsp_1", base.ID, codeSource.ID)
	require.NoError(t, err)
	require.Equal(t, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", codeRevision.SourceCommitSHA)
	codeRepos, err := svc.repositoryStore.ListForResource(ctx, "wsp_1", reposstore.ResourceTypeKBSource, codeSource.ID)
	require.NoError(t, err)
	require.Len(t, codeRepos, 2)
	codePrimaryRepo := codeRepos[0]
	for _, repo := range codeRepos {
		if repo.RepoRole == reposstore.RepoRolePrimary {
			codePrimaryRepo = repo
		}
	}
	require.Equal(t, "user-1/agentteam", codePrimaryRepo.RepoFullName)

	connectorSource, err := svc.CreateSource(ctx, "wsp_1", "user-1", base.ID, KBSourceMutation{
		SourceType: "external_connector",
		Name:       "Google Drive",
		Config:     `{"provider":"google_drive"}`,
	})
	require.NoError(t, err)
	require.Equal(t, "external_connector", connectorSource.SourceType)
	require.Equal(t, "kb_repo_materialized", connectorSource.IndexingMode)

	databaseSource, err := svc.CreateSource(ctx, "wsp_1", "user-1", base.ID, KBSourceMutation{
		SourceType: "database_schema",
		Name:       "Production schema",
		Config:     `{"databaseType":"mysql"}`,
	})
	require.NoError(t, err)
	require.Equal(t, "database_schema", databaseSource.SourceType)
	require.Equal(t, "kb_repo_materialized", databaseSource.IndexingMode)

	audit, err := svc.ListAuditEvents(ctx, "wsp_1", "user-1", base.ID, 1, 20)
	require.NoError(t, err)
	require.NotEmpty(t, audit)
}

func TestKBServiceDeleteSourceDeletesManagedRepositoriesAndMappings(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	fakeStore := svc.wikiStore.(*fakeAGSRepositoryStore)
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)
	source, err := svc.CreateSource(ctx, "wsp_1", "owner", base.ID, KBSourceMutation{SourceType: "codebase", Name: "AgentTeam"})
	require.NoError(t, err)
	readProfile := kb.KnowledgeBaseRepositoryAccessProfileRead
	manageProfile := kb.KnowledgeBaseRepositoryAccessProfileManage
	grant, err := svc.UpsertKnowledgeBasePermission(ctx, "wsp_1", "owner", base.ID, KBPermissionGrantMutation{
		SubjectType:             "user",
		SubjectID:               "viewer",
		Role:                    "view",
		RepositoryAccessProfile: &readProfile,
		RepositoryOverrides:     []KBPermissionRepositoryOverrideMutation{{SourceID: source.ID, RepoRole: reposstore.RepoRolePrimary, RepositoryAccessProfile: &manageProfile}},
	})
	require.NoError(t, err)
	overrideRows, err := svc.store.ListKnowledgeBaseSourceRepositoryAccessOverrides(ctx, "wsp_1", base.ID, []string{grant.ID})
	require.NoError(t, err)
	require.Len(t, overrideRows, 1)

	require.NoError(t, svc.DeleteSource(ctx, "wsp_1", "owner", base.ID, source.ID))

	require.Len(t, fakeStore.deletes, 2)
	deletedFullNames := []string{fakeStore.deletes[0].FullName, fakeStore.deletes[1].FullName}
	require.ElementsMatch(t, []string{"owner/agentteam", "owner/agentteam.kb"}, deletedFullNames)
	_, err = svc.store.GetSource(ctx, "wsp_1", base.ID, source.ID)
	require.ErrorIs(t, err, gorm.ErrRecordNotFound)
	repos, err := svc.repositoryStore.ListForResource(ctx, "wsp_1", reposstore.ResourceTypeKBSource, source.ID)
	require.NoError(t, err)
	require.Empty(t, repos)
	overrideRows, err = svc.store.ListKnowledgeBaseSourceRepositoryAccessOverrides(ctx, "wsp_1", base.ID, []string{grant.ID})
	require.NoError(t, err)
	require.Empty(t, overrideRows)
	lifecycleEvents, err := svc.repositoryStore.ListLifecycleEventsForResource(ctx, "wsp_1", reposstore.ResourceTypeKBSource, source.ID)
	require.NoError(t, err)
	require.Len(t, lifecycleEvents, 2)
	for _, event := range lifecycleEvents {
		require.Equal(t, reposstore.ResourceRepositoryLifecycleOperationDelete, event.Operation)
		require.Equal(t, reposstore.ResourceRepositoryLifecycleStatusSucceeded, event.Status)
	}
}

func TestKBServiceDeleteSourceAllowsSameRepoNameReuse(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)
	source, err := svc.CreateSource(ctx, "wsp_1", "owner", base.ID, KBSourceMutation{SourceType: "codebase", Name: "TiDB"})
	require.NoError(t, err)
	require.NoError(t, svc.DeleteSource(ctx, "wsp_1", "owner", base.ID, source.ID))

	recreated, err := svc.CreateSource(ctx, "wsp_1", "owner", base.ID, KBSourceMutation{SourceType: "codebase", Name: "TiDB"})

	require.NoError(t, err)
	require.NotEqual(t, source.ID, recreated.ID)
	rows, err := svc.repositoryStore.ListByRepoFullName(ctx, "owner/tidb")
	require.NoError(t, err)
	require.Len(t, rows, 1)
	require.Equal(t, recreated.ID, rows[0].ResourceID)
}

func TestKBServiceCreateSourceCleansOrphanKBSourceRepositoryMappings(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)
	input := KBSourceMutation{SourceType: "codebase", Name: "TiDB", Repo: KBSourceRepositoryCreateInput{RepoNameOverride: "tidb"}}
	source, err := svc.CreateSource(ctx, "wsp_1", "owner", base.ID, input)
	require.NoError(t, err)
	_, err = svc.CreateSource(ctx, "wsp_1", "owner", base.ID, input)
	require.ErrorIs(t, err, reposservice.ErrConflict)
	require.NoError(t, svc.store.DeleteSource(ctx, "wsp_1", base.ID, source.ID))

	recreated, err := svc.CreateSource(ctx, "wsp_1", "owner", base.ID, input)

	require.NoError(t, err)
	require.NotEqual(t, source.ID, recreated.ID)
	staleRepos, err := svc.repositoryStore.ListForResource(ctx, "wsp_1", reposstore.ResourceTypeKBSource, source.ID)
	require.NoError(t, err)
	require.Empty(t, staleRepos)
	rows, err := svc.repositoryStore.ListByRepoFullName(ctx, "owner/tidb")
	require.NoError(t, err)
	require.Len(t, rows, 1)
	require.Equal(t, recreated.ID, rows[0].ResourceID)
	insightRows, err := svc.repositoryStore.ListByRepoFullName(ctx, "owner/tidb.kb")
	require.NoError(t, err)
	require.Len(t, insightRows, 1)
	require.Equal(t, recreated.ID, insightRows[0].ResourceID)
}

func TestKBServiceDeleteSourceKeepsResourceWhenManagedRepositoryDeleteFails(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	fakeStore := svc.wikiStore.(*fakeAGSRepositoryStore)
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)
	source, err := svc.CreateSource(ctx, "wsp_1", "owner", base.ID, KBSourceMutation{SourceType: "codebase", Name: "AgentTeam"})
	require.NoError(t, err)
	fakeStore.deleteErr = errors.New("ags delete failed")

	err = svc.DeleteSource(ctx, "wsp_1", "owner", base.ID, source.ID)

	require.Error(t, err)
	require.Contains(t, err.Error(), "ags delete failed")
	_, err = svc.store.GetSource(ctx, "wsp_1", base.ID, source.ID)
	require.NoError(t, err)
	repos, err := svc.repositoryStore.ListForResource(ctx, "wsp_1", reposstore.ResourceTypeKBSource, source.ID)
	require.NoError(t, err)
	require.Len(t, repos, 2)
	lifecycleEvents, err := svc.repositoryStore.ListLifecycleEventsForResource(ctx, "wsp_1", reposstore.ResourceTypeKBSource, source.ID)
	require.NoError(t, err)
	require.Len(t, lifecycleEvents, 2)
	for _, event := range lifecycleEvents {
		require.Equal(t, reposstore.ResourceRepositoryLifecycleOperationDelete, event.Operation)
		require.Equal(t, reposstore.ResourceRepositoryLifecycleStatusFailed, event.Status)
		require.Contains(t, event.LastError, "ags delete failed")
	}
}

// TestKBServiceCreateSourceCleansUpRepoWhenPostCreateGrantFails verifies that
// when EnsureRepository successfully creates an AGS repo but the post-create
// grant step fails, the just-created repo is returned to the caller
// (ensureSourceRepositories) and cleaned up by CreateSource's compensation
// logic so it does not become an orphan that blocks future same-name imports.
func TestKBServiceCreateSourceCleansUpRepoWhenPostCreateGrantFails(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	grantErr := errors.New("grant failed: insufficient permissions")
	fakeStore := &fakeAGSRepositoryStore{ensureGrantErr: grantErr}
	svc.wikiStore = fakeStore
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)

	_, err = svc.CreateSource(ctx, "wsp_1", "owner", base.ID, KBSourceMutation{SourceType: "codebase", Name: "AgentTeam"})
	require.ErrorIs(t, err, grantErr)

	// EnsureRepository was called (repo was "created") but grant failed.
	require.Len(t, fakeStore.ensureRequests, 1, "first repo ensure was attempted")

	// The just-created repo must be deleted by CreateSource's cleanup path
	// to avoid leaving an orphan that blocks future same-name imports.
	require.NotEmpty(t, fakeStore.deletes, "just-created repo must be cleaned up when post-create grant fails")
	deletedFullNames := make([]string, 0, len(fakeStore.deletes))
	for _, identity := range fakeStore.deletes {
		deletedFullNames = append(deletedFullNames, identity.FullName)
	}
	require.Contains(t, deletedFullNames, "owner/agentteam",
		"the just-created AGS repo must be deleted after grant failure")
}

func TestKBServiceCreateSourceRecordsRepositoryLifecycleFailureAfterPartialProvisioning(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	provisionErr := errors.New("insights repo unavailable")
	fakeStore := &fakeAGSRepositoryStore{ensureErr: provisionErr, ensureErrAfter: 1}
	svc.wikiStore = fakeStore
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)

	_, err = svc.CreateSource(ctx, "wsp_1", "owner", base.ID, KBSourceMutation{SourceType: "codebase", Name: "AgentTeam"})

	require.ErrorIs(t, err, provisionErr)
	require.Len(t, fakeStore.ensureRequests, 2)
	sourceID := fakeStore.ensureRequests[0].ResourceID
	sources, err := svc.store.ListSources(ctx, "wsp_1", base.ID, 1, 10)
	require.NoError(t, err)
	require.Empty(t, sources)
	repos, err := svc.repositoryStore.ListForResource(ctx, "wsp_1", reposstore.ResourceTypeKBSource, sourceID)
	require.NoError(t, err)
	require.Empty(t, repos)
	lifecycleEvents, err := svc.repositoryStore.ListLifecycleEventsForResource(ctx, "wsp_1", reposstore.ResourceTypeKBSource, sourceID)
	require.NoError(t, err)
	require.Len(t, lifecycleEvents, 2)
	require.Equal(t, reposstore.ResourceRepositoryLifecycleOperationCreateBind, lifecycleEvents[0].Operation)
	require.Equal(t, reposstore.ResourceRepositoryLifecycleStatusFailed, lifecycleEvents[0].Status)
	require.Equal(t, "owner/agentteam", lifecycleEvents[0].RepoFullName)
	require.Contains(t, lifecycleEvents[0].LastError, "insights repo unavailable")
	// The second event records the successful cleanup of the orphan repo.
	require.Equal(t, reposstore.ResourceRepositoryLifecycleOperationDelete, lifecycleEvents[1].Operation)
	require.Equal(t, reposstore.ResourceRepositoryLifecycleStatusSucceeded, lifecycleEvents[1].Status)
}

// TestKBServiceCreateSourceCleansUpOrphanAGSRepoOnPartialProvisioningFailure
// verifies that when the second AGS repo (insights) fails to provision, the
// first successfully created AGS repo (mirror/resolved) is cleaned up via
// DeleteRepository. Without this cleanup, the orphan AGS repo blocks future
// same-name imports with "ags_repo_already_exists".
func TestKBServiceCreateSourceCleansUpOrphanAGSRepoOnPartialProvisioningFailure(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	provisionErr := errors.New("insights repo unavailable")
	fakeStore := &fakeAGSRepositoryStore{ensureErr: provisionErr, ensureErrAfter: 1}
	svc.wikiStore = fakeStore
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)

	_, err = svc.CreateSource(ctx, "wsp_1", "owner", base.ID, KBSourceMutation{SourceType: "codebase", Name: "AgentTeam"})
	require.ErrorIs(t, err, provisionErr)

	// The first AGS repo (resolved/mirror) was created successfully but the
	// second (insights) failed. The first repo must be deleted to avoid
	// leaving an orphan that blocks future same-name imports.
	require.NotEmpty(t, fakeStore.deletes, "orphan AGS repos must be cleaned up after partial provisioning failure")
	deletedFullNames := make([]string, 0, len(fakeStore.deletes))
	for _, identity := range fakeStore.deletes {
		deletedFullNames = append(deletedFullNames, identity.FullName)
	}
	require.Contains(t, deletedFullNames, "owner/agentteam",
		"the first successfully provisioned AGS repo must be deleted after partial failure")
}

// TestKBServiceCreateSourcePreflightRejectsBadTokenBeforeProvisioning verifies
// that the preflight ls-remote check runs before any AGS repos are created,
// so a bad token or unreachable remote does not leave orphan AGS repos.
func TestKBServiceCreateSourcePreflightRejectsBadTokenBeforeProvisioning(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	fakeStore := svc.wikiStore.(*fakeAGSRepositoryStore)
	preflightErr := errors.New("preflight: authentication required")
	svc.sourceMirrorPreflighter = &fakeSourceMirrorPreflighter{err: preflightErr}
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)

	_, err = svc.CreateSource(ctx, "wsp_1", "owner", base.ID, KBSourceMutation{
		SourceType: "codebase",
		Name:       "TestRepo",
		RepoMode:   "mirror",
		Mirror: KBSourceMirrorMutation{
			UpstreamType: kb.SourceMirrorUpstreamTypeGenericGit,
			RemoteURL:    "https://93.184.216.34/org/repo.git",
			DefaultRef:   "main",
			AccessToken:  "bad-token",
		},
	})
	require.ErrorIs(t, err, preflightErr)

	// No AGS repos should have been provisioned because preflight failed.
	require.Empty(t, fakeStore.ensureRequests, "preflight failure must not provision any AGS repos")
	require.Empty(t, fakeStore.deletes, "preflight failure must not require any AGS repo cleanup")
}

func TestKBServiceCreatesPrivateSourceRepositories(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	fakeStore := svc.wikiStore.(*fakeAGSRepositoryStore)
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)

	source, err := svc.CreateSource(ctx, "wsp_1", "owner", base.ID, KBSourceMutation{
		SourceType: "file_repository",
		RepoMode:   "create",
		Name:       "Runbook files",
	})
	require.NoError(t, err)

	repos, err := svc.repositoryStore.ListForResource(ctx, "wsp_1", reposstore.ResourceTypeKBSource, source.ID)
	require.NoError(t, err)
	require.Len(t, repos, 2)
	visibilityByRole := map[string]string{}
	for _, repo := range repos {
		visibilityByRole[repo.RepoRole] = repo.Visibility
	}
	require.Equal(t, "private", visibilityByRole[reposstore.RepoRolePrimary])
	require.Equal(t, "private", visibilityByRole[reposstore.RepoRoleInsights])

	ensureVisibilityByRole := map[string]string{}
	for _, request := range fakeStore.ensureRequests {
		ensureVisibilityByRole[request.RepoRole] = request.Visibility
	}
	require.Equal(t, "private", ensureVisibilityByRole[reposstore.RepoRolePrimary])
	require.Equal(t, "private", ensureVisibilityByRole[reposstore.RepoRoleInsights])
}

func TestKBServiceCreateSourceProvisioningFailureDoesNotLeaveReadySource(t *testing.T) {
	ctx := context.Background()
	db, err := openServiceTestSQLiteDB(t)
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(
		&kb.KnowledgeBase{},
		&kb.KnowledgeBaseGroup{},
		&kb.KnowledgeBaseTag{},
		&kb.KnowledgeBaseTagAssignment{},
		&kb.KnowledgeBasePin{},
		&kb.KnowledgeBaseGrant{},
		&kb.KnowledgeBaseSourceRepositoryAccessOverride{},
		&kb.Source{},
		&kb.SourceMirror{},

		&kb.AnalysisRun{},
		&kb.SourceRevision{},
		&kb.Job{},
		&kb.AuditEvent{},
		&reposstore.ResourceRepository{},
		&reposstore.ResourceRepositoryLifecycleEvent{},
		&reposstore.ResourceRepositoryAccessGrant{},
		&activitystore.ResourceActivityEvent{},
		&activitystore.ResourceActivityLatestCommit{},
	))
	provisionErr := errors.New("managed repo unavailable")
	svc := NewKBServiceWithOptions(kb.New(db), func() time.Time {
		return time.Date(2026, 6, 1, 1, 2, 3, 0, time.UTC)
	}, KBServiceOptions{WikiStore: &fakeAGSRepositoryStore{ensureErr: provisionErr}, RepositoryStore: reposstore.New(db)})
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)

	_, err = svc.CreateSource(ctx, "wsp_1", "owner", base.ID, KBSourceMutation{SourceType: "database_schema", Name: "Prod schema"})
	require.ErrorIs(t, err, provisionErr)

	sources, err := svc.store.ListSources(ctx, "wsp_1", base.ID, 1, 10)
	require.NoError(t, err)
	require.Empty(t, sources)
}

func TestKBServiceCreatesMirrorSourceWithSanitizedMetadata(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)
	enabled := true

	source, err := svc.CreateSource(ctx, "wsp_1", "owner", base.ID, KBSourceMutation{
		SourceType: "codebase",
		RepoMode:   "mirror",
		Name:       "External AgentTeam",
		DefaultRef: "main",
		Mirror: KBSourceMirrorMutation{
			UpstreamType:    kb.SourceMirrorUpstreamTypeGitHub,
			RemoteURL:       "https://github.com/pingcap/agentteam.git",
			CredentialRef:   "automation-secret://GITHUB_TOKEN",
			DefaultRef:      "main",
			IntervalSeconds: ptrTo(300),
			Enabled:         &enabled,
		},
	})

	require.NoError(t, err)
	require.Equal(t, "codebase", source.SourceType)
	mirrorRepo, err := svc.repositoryStore.Get(ctx, "wsp_1", reposstore.ResourceTypeKBSource, source.ID, reposstore.RepoRoleMirror)
	require.NoError(t, err)
	require.Equal(t, "owner/external-agentteam", mirrorRepo.RepoFullName)
	insightsRepo, err := svc.repositoryStore.Get(ctx, "wsp_1", reposstore.ResourceTypeKBSource, source.ID, reposstore.RepoRoleInsights)
	require.NoError(t, err)
	require.Equal(t, "owner/external-agentteam.kb", insightsRepo.RepoFullName)
	_, err = svc.repositoryStore.Get(ctx, "wsp_1", reposstore.ResourceTypeKBSource, source.ID, reposstore.RepoRolePrimary)
	require.ErrorIs(t, err, gorm.ErrRecordNotFound)
	var mirrorEnsure reposservice.AGSRepositoryEnsureRequest
	for _, request := range svc.wikiStore.(*fakeAGSRepositoryStore).ensureRequests {
		if request.RepoRole == reposstore.RepoRoleMirror {
			mirrorEnsure = request
		}
	}
	require.Equal(t, reposstore.RepoRoleMirror, mirrorEnsure.RepoRole)
	require.False(t, mirrorEnsure.AutoInit)
	require.NotNil(t, source.Mirror)
	require.Equal(t, "https://github.com/pingcap/agentteam.git", source.Mirror.RemoteURL)
	require.Equal(t, "automation-secret://GITHUB_TOKEN", source.Mirror.CredentialRef)
	require.Equal(t, kb.SourceMirrorSyncStatusIdle, source.Mirror.SyncStatus)

	sources, err := svc.ListSources(ctx, "wsp_1", "owner", base.ID, 1, 20)
	require.NoError(t, err)
	require.Len(t, sources, 1)
	require.NotNil(t, sources[0].Mirror)
	require.Equal(t, source.ID, sources[0].Mirror.SourceID)
}

func TestKBServiceCreatesMirrorSourceForPrivateGitRemoteWhenEnabled(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	svc.allowPrivateSourceMirrorHosts = true
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)
	enabled := true

	source, err := svc.CreateSource(ctx, "wsp_1", "owner", base.ID, KBSourceMutation{
		SourceType: "codebase",
		RepoMode:   "mirror",
		Name:       "Internal AgentTeam",
		DefaultRef: "main",
		Mirror: KBSourceMirrorMutation{
			UpstreamType:    kb.SourceMirrorUpstreamTypeGenericGit,
			RemoteURL:       "https://10.0.0.5/pingcap/agentteam.git",
			DefaultRef:      "main",
			IntervalSeconds: ptrTo(300),
			Enabled:         &enabled,
		},
	})

	require.NoError(t, err)
	require.NotNil(t, source.Mirror)
	require.Equal(t, "https://10.0.0.5/pingcap/agentteam.git", source.Mirror.RemoteURL)
}

func TestKBServicePreservesSourceConfigWithoutLocaleInjection(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)

	source, err := svc.CreateSource(ctx, "wsp_1", "owner", base.ID, KBSourceMutation{
		SourceType: "codebase",
		Name:       "AgentTeam",
		Config:     `{"attachMode":"create"}`,
	})
	require.NoError(t, err)

	var config map[string]any
	require.NoError(t, json.Unmarshal([]byte(source.Config), &config))
	require.NotContains(t, config, "locale")
	require.Equal(t, "create", config["attachMode"])
}

func TestKBServiceRejectsUnsupportedSourceTypes(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "user-1", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)

	_, err = svc.CreateSource(ctx, "wsp_1", "user-1", base.ID, KBSourceMutation{Name: "Unknown", SourceType: "spreadsheet"})

	require.ErrorIs(t, err, ErrInvalidArgument)
}

func TestKBServiceRequiresWorkspaceScope(t *testing.T) {
	svc := newKBServiceForTest(t)
	_, err := svc.CreateKnowledgeBase(context.Background(), "", "user-1", KBMutation{Name: "Runbooks"})
	require.ErrorIs(t, err, ErrInvalidArgument)
}

func TestKBServiceListsAgentAccessibleKnowledgeBasesByAgentNotActingUser(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks", Description: "Ops docs"})
	require.NoError(t, err)
	_, err = svc.GrantAgentKnowledgeBaseAccess(ctx, "wsp_1", "owner", "agt_reader", base.ID, "read")
	require.NoError(t, err)

	items, err := svc.ListAgentAccessibleKnowledgeBases(ctx, "wsp_1", "different-acting-user", "agt_reader", 1, 20)
	require.NoError(t, err)
	require.Len(t, items, 1)
	require.Equal(t, base.ID, items[0].ID)
	require.Equal(t, "read", items[0].AccessLevel)

	items, err = svc.ListAgentAccessibleKnowledgeBases(ctx, "wsp_1", "different-acting-user", "agt_other", 1, 20)
	require.NoError(t, err)
	require.Empty(t, items)
}

func TestKBServiceAgentKnowledgeBaseAccessRequiresOwnedAgent(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)

	svc.agentOwnershipSource = nil
	_, err = svc.ListAgentAccessibleKnowledgeBases(ctx, "wsp_1", "owner", "agt_reader", 1, 20)
	require.ErrorIs(t, err, ErrInvalidArgument)

	svc.agentOwnershipSource = fakeAgentOwnershipSource{agents: []memory.AgentOwnership{{ID: "agt_reader"}}}
	_, err = svc.ListAgentAccessibleKnowledgeBases(ctx, "wsp_1", "owner", "agt_other", 1, 20)
	require.ErrorIs(t, err, ErrForbidden)
	_, err = svc.GrantAgentKnowledgeBaseAccess(ctx, "wsp_1", "owner", "agt_other", base.ID, "read")
	require.ErrorIs(t, err, ErrForbidden)
	_, err = svc.SetAgentKnowledgeBaseAccess(ctx, "wsp_1", "owner", "agt_other", AgentKnowledgeBaseBulkAccessMutation{KnowledgeBaseIDs: []string{base.ID}})
	require.ErrorIs(t, err, ErrForbidden)
	_, err = svc.UpsertKnowledgeBasePermission(ctx, "wsp_1", "owner", base.ID, KBPermissionGrantMutation{SubjectType: "agent", SubjectID: "agt_other", Role: "view"})
	require.ErrorIs(t, err, ErrForbidden)

	_, err = svc.GrantAgentKnowledgeBaseAccess(ctx, "wsp_1", "owner", "agt_reader", base.ID, "read")
	require.NoError(t, err)
	items, err := svc.ListAgentAccessibleKnowledgeBases(ctx, "wsp_1", "owner", "agt_reader", 1, 20)
	require.NoError(t, err)
	require.Len(t, items, 1)
}

func TestKBServiceSetsOneAgentKnowledgeBaseAccess(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	first, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "First"})
	require.NoError(t, err)
	second, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Second"})
	require.NoError(t, err)

	_, err = svc.GrantAgentKnowledgeBaseAccess(ctx, "wsp_1", "owner", "agt_reader", first.ID, "read")
	require.NoError(t, err)
	_, err = svc.GrantAgentKnowledgeBaseAccess(ctx, "wsp_1", "owner", "agt_reader", second.ID, "read")
	require.NoError(t, err)

	result, err := svc.SetAgentKnowledgeBaseAccessForKnowledgeBase(ctx, "wsp_1", "owner", "agt_reader", first.ID, "write")
	require.NoError(t, err)
	require.Equal(t, "write", result.AccessLevel)

	result, err = svc.SetAgentKnowledgeBaseAccessForKnowledgeBase(ctx, "wsp_1", "owner", "agt_reader", first.ID, "none")
	require.NoError(t, err)
	require.Equal(t, "none", result.AccessLevel)

	items, err := svc.ListAgentAccessibleKnowledgeBases(ctx, "wsp_1", "owner", "agt_reader", 1, 20)
	require.NoError(t, err)
	require.Len(t, items, 1)
	require.Equal(t, second.ID, items[0].ID)
}

func TestKBServiceBatchListsAgentAccessibleKnowledgeBasesForSync(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	expires := time.Date(2026, 6, 18, 2, 0, 0, 0, time.UTC)
	issuer := &fakeKBRepositorySyncTokenIssuer{lease: reposservice.RepositorySyncTokenLease{Token: "ags_rat_kb", ExpiresAt: expires}}
	svc.syncTokenIssuer = issuer

	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks", Description: "Ops docs"})
	require.NoError(t, err)
	source, err := svc.CreateSource(ctx, "wsp_1", "owner", base.ID, KBSourceMutation{SourceType: "codebase", Name: "TiDB", Description: "AgentTeam code", DefaultRef: "main", IndexStatus: "ready"})
	require.NoError(t, err)
	_, err = svc.GrantAgentKnowledgeBaseAccess(ctx, "wsp_1", "owner", "agt_reader", base.ID, "write")
	require.NoError(t, err)

	result, err := svc.BatchListAgentAccessibleKnowledgeBases(ctx, "wsp_1", "owner", AgentKnowledgeBaseAccessBatchOptions{AgentIDs: []string{"agt_reader", "agt_reader", "agt_other", " "}})
	require.NoError(t, err)
	require.Equal(t, "ags_rat_kb", result.SyncToken)
	require.True(t, result.SyncTokenExpires.Equal(expires))
	require.Len(t, result.Agents, 2)
	require.Empty(t, result.Agents["agt_other"])
	require.Len(t, result.Agents["agt_reader"], 1)

	gotKB := result.Agents["agt_reader"][0]
	require.Equal(t, base.ID, gotKB.ID)
	require.Equal(t, "Runbooks", gotKB.Name)
	require.Equal(t, "write", gotKB.AccessLevel)
	require.Len(t, gotKB.Sources, 1)
	require.Equal(t, source.ID, gotKB.Sources[0].ID)
	require.Equal(t, "main", gotKB.Sources[0].DefaultRef)

	roles := map[string]AgentAccessibleKBRepositoryForSync{}
	for _, repo := range gotKB.Sources[0].Repositories {
		roles[repo.RepoRole] = repo
	}
	require.Contains(t, roles, reposstore.RepoRolePrimary)
	require.Contains(t, roles, reposstore.RepoRoleInsights)
	require.Equal(t, "main", roles[reposstore.RepoRolePrimary].Branch)
	require.Equal(t, reposstore.AccessStatusCurrent, roles[reposstore.RepoRolePrimary].AccessStatus)
	require.Equal(t, kb.KnowledgeBaseRepositoryAccessProfileWrite, roles[reposstore.RepoRolePrimary].AccessProfile)
	require.True(t, roles[reposstore.RepoRolePrimary].Writable)
	require.Equal(t, "owner/tidb", roles[reposstore.RepoRolePrimary].Repository.RepoFullName)
	require.Equal(t, "owner/tidb.kb", roles[reposstore.RepoRoleInsights].Repository.RepoFullName)

	require.Len(t, issuer.requests, 1)
	require.Equal(t, "wsp_1", issuer.requests[0].WorkspaceID)
	require.Equal(t, "owner", issuer.requests[0].ActorPrincipalID)
	require.ElementsMatch(t, []string{"owner/tidb", "owner/tidb.kb"}, issuer.requests[0].Repositories)
	require.ElementsMatch(t, []string{reposservice.RepositorySyncCapabilityRead, reposservice.RepositorySyncCapabilityClone}, issuer.requests[0].Capabilities)

	svc.contentAuthority = &mutableKBSourceAuthorityReader{states: map[string]string{source.ID: content.AuthorityStateNativeActive}}
	issuer.requests = nil
	native, err := svc.BatchListAgentAccessibleKnowledgeBases(ctx, "wsp_1", "owner", AgentKnowledgeBaseAccessBatchOptions{AgentIDs: []string{"agt_reader"}})
	require.NoError(t, err)
	require.Empty(t, native.Agents["agt_reader"], "native sources are no longer AGS clone targets")
	require.Empty(t, native.SyncToken)
	require.Empty(t, issuer.requests, "native sources must not mint AGS repository tokens")
}

func TestKBServiceBatchAgentAccessRequiresOwnedAgent(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)

	_, err := svc.BatchListAgentAccessibleKnowledgeBases(ctx, "wsp_1", "owner", AgentKnowledgeBaseAccessBatchOptions{AgentIDs: []string{"agt_unowned"}})
	require.ErrorIs(t, err, ErrForbidden)
}

func TestKBServiceAgentAccessMutationsEnqueueKBAssetRefresh(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)

	_, err = svc.GrantAgentKnowledgeBaseAccess(ctx, "wsp_1", "owner", "agt_reader", base.ID, "read")
	require.NoError(t, err)
	events, err := svc.assetAccessOutbox.LeaseDueAgentAssetAccessChanges(ctx, svc.now(), "test", svc.now().Add(time.Minute), 10, nil)
	require.NoError(t, err)
	require.Len(t, events, 1)
	require.Equal(t, "wsp_1", events[0].WorkspaceID)
	require.Equal(t, "agt_reader", events[0].AgentID)
	require.Equal(t, memorystore.AssetTypeKB, events[0].AssetType)

	other, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Onboarding"})
	require.NoError(t, err)
	_, err = svc.SetAgentKnowledgeBaseAccess(ctx, "wsp_1", "owner", "agt_other", AgentKnowledgeBaseBulkAccessMutation{KnowledgeBaseIDs: []string{other.ID}})
	require.NoError(t, err)
	events, err = svc.assetAccessOutbox.LeaseDueAgentAssetAccessChanges(ctx, svc.now(), "test2", svc.now().Add(time.Minute), 10, nil)
	require.NoError(t, err)
	require.Len(t, events, 1)
	require.Equal(t, "agt_other", events[0].AgentID)
	require.Equal(t, memorystore.AssetTypeKB, events[0].AssetType)
}

func TestKBServiceGrantAgentAccessRequiresManage(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)
	_, err = svc.UpsertKnowledgeBasePermission(ctx, "wsp_1", "owner", base.ID, KBPermissionGrantMutation{SubjectType: "user", SubjectID: "viewer", Role: "view"})
	require.NoError(t, err)
	_, err = svc.UpsertKnowledgeBasePermission(ctx, "wsp_1", "owner", base.ID, KBPermissionGrantMutation{SubjectType: "user", SubjectID: "manager", Role: "manage"})
	require.NoError(t, err)

	_, err = svc.GrantAgentKnowledgeBaseAccess(ctx, "wsp_1", "viewer", "agt_reader", base.ID, "read")
	require.ErrorIs(t, err, ErrForbidden)
	items, err := svc.ListAgentAccessibleKnowledgeBases(ctx, "wsp_1", "viewer", "agt_reader", 1, 20)
	require.NoError(t, err)
	require.Empty(t, items)

	_, err = svc.GrantAgentKnowledgeBaseAccess(ctx, "wsp_1", "manager", "agt_reader", base.ID, "read")
	require.NoError(t, err)
	items, err = svc.ListAgentAccessibleKnowledgeBases(ctx, "wsp_1", "manager", "agt_reader", 1, 20)
	require.NoError(t, err)
	require.Len(t, items, 1)
	require.Equal(t, base.ID, items[0].ID)
}

func TestKBServiceGrantAgentAccessGrantsSourceRepositoryReadAccess(t *testing.T) {
	ctx := context.Background()
	db, err := openServiceTestSQLiteDB(t)
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(
		&kb.KnowledgeBase{},
		&kb.KnowledgeBaseGroup{},
		&kb.KnowledgeBaseTag{},
		&kb.KnowledgeBaseTagAssignment{},
		&kb.KnowledgeBasePin{},
		&kb.KnowledgeBaseGrant{},
		&kb.KnowledgeBaseSourceRepositoryAccessOverride{},
		&kb.Source{},
		&kb.SourceMirror{},
		&kb.AnalysisRun{},
		&kb.SourceRevision{},
		&kb.Job{},
		&kb.AuditEvent{},
		&reposstore.ResourceRepository{},
		&reposstore.ResourceRepositoryLifecycleEvent{},
		&reposstore.ResourceRepositoryAccessGrant{},
		&activitystore.ResourceActivityEvent{},
		&activitystore.ResourceActivityLatestCommit{},
	))
	now := func() time.Time { return time.Date(2026, 6, 1, 1, 2, 3, 0, time.UTC) }
	wikiStore := &fakeAGSRepositoryStore{}
	svc := NewKBServiceWithOptions(kb.New(db), now, KBServiceOptions{
		WikiStore:            wikiStore,
		RepositoryStore:      reposstore.New(db),
		AgentOwnershipSource: fakeAgentOwnershipSource{agents: []memory.AgentOwnership{{ID: "agt_reader"}}},
	})
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)
	_, err = svc.CreateSource(ctx, "wsp_1", "owner", base.ID, KBSourceMutation{SourceType: "codebase", Name: "TiDB", IndexStatus: "ready"})
	require.NoError(t, err)
	wikiStore.grants = nil

	_, err = svc.GrantAgentKnowledgeBaseAccess(ctx, "wsp_1", "owner", "agt_reader", base.ID, "read")

	require.NoError(t, err)
	require.Len(t, wikiStore.grants, 2)
	byRepo := map[string]reposservice.ResourceRepositoryCollaboratorGrant{}
	for _, grant := range wikiStore.grants {
		byRepo[grant.Repository.FullName] = grant
	}
	for _, repo := range []string{"owner/tidb", "owner/tidb.kb"} {
		grant, ok := byRepo[repo]
		require.Truef(t, ok, "missing grant for %s", repo)
		require.Equal(t, "read", grant.Permission)
		require.Equal(t, reposservice.ResourceRepositoryActor{Provider: "agentteam", Subject: "agt_reader", Login: "agt-reader", Name: "agt_reader"}, grant.Actor)
	}
}

func TestKBServiceBulkSetsAgentKnowledgeBaseAccessWithinVisibleScope(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	keep, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "member-owner", KBMutation{Name: "Keep"})
	require.NoError(t, err)
	remove, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "member-owner", KBMutation{Name: "Remove"})
	require.NoError(t, err)
	defaultRole := "view"
	unmanageable, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "other-owner", KBMutation{Name: "Other", DefaultWorkspaceMemberRole: &defaultRole})
	require.NoError(t, err)
	denied, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "other-owner", KBMutation{Name: "Denied"})
	require.NoError(t, err)
	_, err = svc.UpsertKnowledgeBasePermission(ctx, "wsp_1", "other-owner", denied.ID, KBPermissionGrantMutation{SubjectType: "user", SubjectID: "member-owner", Role: "none", RoleSet: true})
	require.NoError(t, err)
	_, err = svc.GrantAgentKnowledgeBaseAccess(ctx, "wsp_1", "member-owner", "agt_reader", remove.ID, "read")
	require.NoError(t, err)
	_, err = svc.GrantAgentKnowledgeBaseAccess(ctx, "wsp_1", "other-owner", "agt_reader", unmanageable.ID, "read")
	require.NoError(t, err)

	result, err := svc.SetAgentKnowledgeBaseAccess(ctx, "wsp_1", "member-owner", "agt_reader", AgentKnowledgeBaseBulkAccessMutation{KnowledgeBaseIDs: []string{keep.ID, unmanageable.ID}})
	require.NoError(t, err)
	require.Equal(t, "agt_reader", result.AgentID)
	require.ElementsMatch(t, []string{keep.ID, unmanageable.ID}, result.KnowledgeBaseIDs)
	require.ElementsMatch(t, []string{keep.ID}, result.GrantedKnowledgeBaseIDs)
	require.ElementsMatch(t, []string{remove.ID}, result.RevokedKnowledgeBaseIDs)

	items, err := svc.ListAgentAccessibleKnowledgeBases(ctx, "wsp_1", "member-owner", "agt_reader", 1, 20)
	require.NoError(t, err)
	var ids []string
	for _, item := range items {
		ids = append(ids, item.ID)
	}
	require.ElementsMatch(t, []string{keep.ID, unmanageable.ID}, ids)

	_, err = svc.SetAgentKnowledgeBaseAccess(ctx, "wsp_1", "member-owner", "agt_reader", AgentKnowledgeBaseBulkAccessMutation{KnowledgeBaseIDs: []string{denied.ID}})
	require.ErrorIs(t, err, ErrForbidden)
}

func TestKBServiceBulkSetsAgentKnowledgeBaseAccessLevels(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	wikiStore := svc.wikiStore.(*fakeAGSRepositoryStore)
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)
	source, err := svc.CreateSource(ctx, "wsp_1", "owner", base.ID, KBSourceMutation{SourceType: "codebase", Name: "TiDB", IndexStatus: "ready"})
	require.NoError(t, err)
	require.NotEmpty(t, source.ID)
	wikiStore.grants = nil

	result, err := svc.SetAgentKnowledgeBaseAccess(ctx, "wsp_1", "owner", "agt_reader", AgentKnowledgeBaseBulkAccessMutation{KnowledgeBases: []AgentKnowledgeBaseAccessMutation{{KnowledgeBaseID: base.ID, AccessLevel: "write"}}})
	require.NoError(t, err)
	require.ElementsMatch(t, []string{base.ID}, result.GrantedKnowledgeBaseIDs)

	grant, err := svc.store.GetKnowledgeBaseGrant(ctx, "wsp_1", base.ID, "agent", "agt_reader")
	require.NoError(t, err)
	require.Equal(t, kb.KnowledgeBaseRepositoryAccessProfileWrite, grant.RepositoryAccessProfile)

	items, err := svc.ListAgentAccessibleKnowledgeBases(ctx, "wsp_1", "owner", "agt_reader", 1, 20)
	require.NoError(t, err)
	require.Len(t, items, 1)
	require.Equal(t, "write", items[0].AccessLevel)

	rows, err := svc.repositoryStore.ListAccessGrantsForIntent(ctx, "wsp_1", reposstore.AccessIntentTypeKBGrant, grant.ID)
	require.NoError(t, err)
	require.Len(t, rows, 2)
	for _, row := range rows {
		require.Equal(t, reposstore.AccessPermissionWrite, row.Permission)
		require.Equal(t, kb.KnowledgeBaseRepositoryAccessProfileWrite, row.IntentProfile)
	}
}

func TestKBServiceBulkAgentRemovalKeepsGrantWhenRepositoryRevokeBlocked(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	wikiStore := svc.wikiStore.(*fakeAGSRepositoryStore)
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)
	_, err = svc.CreateSource(ctx, "wsp_1", "owner", base.ID, KBSourceMutation{SourceType: "codebase", Name: "TiDB", IndexStatus: "ready"})
	require.NoError(t, err)
	_, err = svc.GrantAgentKnowledgeBaseAccess(ctx, "wsp_1", "owner", "agt_reader", base.ID, "read")
	require.NoError(t, err)
	wikiStore.revokeErr = errors.New("ags unavailable")

	_, err = svc.SetAgentKnowledgeBaseAccess(ctx, "wsp_1", "owner", "agt_reader", AgentKnowledgeBaseBulkAccessMutation{})

	require.ErrorIs(t, err, ErrConflict)
	grant, err := svc.store.GetKnowledgeBaseGrant(ctx, "wsp_1", base.ID, "agent", "agt_reader")
	require.NoError(t, err)
	rows, err := svc.repositoryStore.ListAccessGrantsForIntent(ctx, "wsp_1", reposstore.AccessIntentTypeKBGrant, grant.ID)
	require.NoError(t, err)
	require.NotEmpty(t, rows)
	for _, row := range rows {
		require.Equal(t, reposstore.AccessStatusBlocked, row.Status)
		require.Equal(t, "ags unavailable", row.LastError)
	}
	permissions, err := svc.ListKnowledgeBasePermissions(ctx, "wsp_1", "owner", base.ID)
	require.NoError(t, err)
	require.Len(t, permissions.Agents, 1)
	require.Equal(t, kbRepositoryAccessStatusBlocked, permissions.Agents[0].RepositoryAccessStatus)
	require.Equal(t, "ags unavailable", permissions.Agents[0].RepositoryAccessLastError)
}

func TestKBServiceEnforcesKnowledgeBaseGrantRoles(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)
	require.Equal(t, "owner", base.Role)
	require.True(t, base.CanManage)
	collection, err := svc.ListKnowledgeBases(ctx, "wsp_1", "owner", "all", "", nil, 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)

	permissions, err := svc.ListKnowledgeBasePermissions(ctx, "wsp_1", "owner", base.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.UpsertKnowledgeBasePermission(ctx, "wsp_1", "owner", base.ID, KBPermissionGrantMutation{SubjectType: "user", SubjectID: "manager", Role: "manage"})
	require.NoError(t, err)
	require.Equal(t, "manage", managerGrant.Role)
	managerBase, err := svc.GetKnowledgeBase(ctx, "wsp_1", "manager", base.ID)
	require.NoError(t, err)
	require.Equal(t, "manage", managerBase.Role)
	require.True(t, managerBase.CanManage)
	managerCollection, err := svc.ListKnowledgeBases(ctx, "wsp_1", "manager", "all", "", nil, 1, 20)
	require.NoError(t, err)
	require.Len(t, managerCollection.Items, 1)
	require.Equal(t, "manage", managerCollection.Items[0].Role)
	require.True(t, managerCollection.Items[0].CanManage)
	viewerGrant, err := svc.UpsertKnowledgeBasePermission(ctx, "wsp_1", "manager", base.ID, KBPermissionGrantMutation{SubjectType: "user", SubjectID: "viewer", Role: "view"})
	require.NoError(t, err)
	require.Equal(t, "view", viewerGrant.Role)
	agentGrant, err := svc.UpsertKnowledgeBasePermission(ctx, "wsp_1", "manager", base.ID, KBPermissionGrantMutation{SubjectType: "agent", SubjectID: "agt_reader", Role: "edit"})
	require.NoError(t, err)
	require.Equal(t, "view", agentGrant.Role)

	viewerBase, err := svc.GetKnowledgeBase(ctx, "wsp_1", "viewer", base.ID)
	require.NoError(t, err)
	require.Equal(t, "view", viewerBase.Role)
	require.False(t, viewerBase.CanManage)
	viewerCollection, err := svc.ListKnowledgeBases(ctx, "wsp_1", "viewer", "all", "", nil, 1, 20)
	require.NoError(t, err)
	require.Len(t, viewerCollection.Items, 1)
	require.Equal(t, "view", viewerCollection.Items[0].Role)
	require.False(t, viewerCollection.Items[0].CanManage)
	_, err = svc.CreateSource(ctx, "wsp_1", "viewer", base.ID, KBSourceMutation{Name: "viewer.md", SourceType: "file_repository"})
	require.ErrorIs(t, err, ErrForbidden)
	viewerPermissions, err := svc.ListKnowledgeBasePermissions(ctx, "wsp_1", "viewer", base.ID)
	require.NoError(t, err)
	require.False(t, viewerPermissions.CanManage)
	require.Equal(t, "view", viewerPermissions.MyRole)
	require.NotNil(t, viewerPermissions.Self)
	require.Equal(t, viewerGrant.ID, viewerPermissions.Self.ID)
	require.Equal(t, "viewer", viewerPermissions.Self.SubjectID)
	require.Empty(t, viewerPermissions.Members)
	require.Empty(t, viewerPermissions.Agents)
	_, err = svc.UpsertKnowledgeBasePermission(ctx, "wsp_1", "viewer", base.ID, KBPermissionGrantMutation{SubjectType: "user", SubjectID: "other", Role: "edit"})
	require.ErrorIs(t, err, ErrForbidden)

	_, err = svc.CreateSource(ctx, "wsp_1", "manager", base.ID, KBSourceMutation{Name: "manager", SourceType: "file_repository"})
	require.NoError(t, err)
	items, err := svc.ListAgentAccessibleKnowledgeBases(ctx, "wsp_1", "owner", "agt_reader", 1, 20)
	require.NoError(t, err)
	require.Len(t, items, 1)
	require.Equal(t, base.ID, items[0].ID)
}

func TestKBServiceListsCurrentUserRepositoryAccessOnKnowledgeBases(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)
	_, err = svc.CreateSource(ctx, "wsp_1", "owner", base.ID, KBSourceMutation{SourceType: "codebase", Name: "TiDB", IndexStatus: "ready"})
	require.NoError(t, err)
	readProfile := kb.KnowledgeBaseRepositoryAccessProfileRead
	_, err = svc.UpsertKnowledgeBasePermission(ctx, "wsp_1", "owner", base.ID, KBPermissionGrantMutation{SubjectType: "user", SubjectID: "viewer", Role: "view", RepositoryAccessProfile: &readProfile})
	require.NoError(t, err)

	collection, err := svc.ListKnowledgeBases(ctx, "wsp_1", "viewer", "all", "", nil, 1, 20)
	require.NoError(t, err)
	require.Len(t, collection.Items, 1)
	require.Equal(t, "view", collection.Items[0].Role)
	require.False(t, collection.Items[0].CanManage)
	require.Equal(t, kb.KnowledgeBaseRepositoryAccessProfileRead, collection.Items[0].RepositoryAccessProfile)
	require.Equal(t, kbRepositoryAccessStatusCurrent, collection.Items[0].RepositoryAccessStatus)
}

func TestKBServiceAnnotatesSourceDetailWithCurrentUserRepositoryAccess(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	defaultRole := "view"
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks", DefaultWorkspaceMemberRole: &defaultRole})
	require.NoError(t, err)
	source, err := svc.CreateSource(ctx, "wsp_1", "owner", base.ID, KBSourceMutation{SourceType: "codebase", Name: "TiDB", IndexStatus: "ready"})
	require.NoError(t, err)

	viewerDetail, err := svc.GetKnowledgeBaseSourceDetail(ctx, "wsp_1", "viewer", base.ID, source.ID)
	require.NoError(t, err)
	require.NotNil(t, viewerDetail.Repositories.Primary)
	require.NotNil(t, viewerDetail.Repositories.Insights)
	require.Equal(t, kb.KnowledgeBaseRepositoryAccessProfileNone, viewerDetail.RepositoryAccess[reposstore.RepoRolePrimary].EffectiveRepositoryAccessProfile)
	require.Equal(t, kbRepositoryAccessStatusNotGranted, viewerDetail.RepositoryAccess[reposstore.RepoRolePrimary].RepositoryAccessStatus)
	require.Equal(t, kb.KnowledgeBaseRepositoryAccessProfileNone, viewerDetail.RepositoryAccess[reposstore.RepoRoleInsights].EffectiveRepositoryAccessProfile)
	require.Equal(t, kbRepositoryAccessStatusNotGranted, viewerDetail.RepositoryAccess[reposstore.RepoRoleInsights].RepositoryAccessStatus)

	readProfile := kb.KnowledgeBaseRepositoryAccessProfileRead
	grant, err := svc.UpsertKnowledgeBasePermission(ctx, "wsp_1", "owner", base.ID, KBPermissionGrantMutation{SubjectType: "user", SubjectID: "viewer", Role: "view", RepositoryAccessProfile: &readProfile})
	require.NoError(t, err)
	require.Equal(t, kbRepositoryAccessStatusCurrent, grant.RepositoryAccessStatus)

	readDetail, err := svc.GetKnowledgeBaseSourceDetail(ctx, "wsp_1", "viewer", base.ID, source.ID)
	require.NoError(t, err)
	require.Equal(t, kb.KnowledgeBaseRepositoryAccessProfileRead, readDetail.RepositoryAccess[reposstore.RepoRolePrimary].EffectiveRepositoryAccessProfile)
	require.Equal(t, kbRepositoryAccessStatusCurrent, readDetail.RepositoryAccess[reposstore.RepoRolePrimary].RepositoryAccessStatus)
	require.Equal(t, kb.KnowledgeBaseRepositoryAccessProfileRead, readDetail.RepositoryAccess[reposstore.RepoRoleInsights].EffectiveRepositoryAccessProfile)
	require.Equal(t, kbRepositoryAccessStatusCurrent, readDetail.RepositoryAccess[reposstore.RepoRoleInsights].RepositoryAccessStatus)

	noneProfile := kb.KnowledgeBaseRepositoryAccessProfileNone
	_, err = svc.UpdateKnowledgeBasePermissionRepositoryOverrides(ctx, "wsp_1", "owner", base.ID, grant.ID, []KBPermissionRepositoryOverrideMutation{{SourceID: source.ID, RepoRole: reposstore.RepoRolePrimary, RepositoryAccessProfile: &noneProfile}})
	require.NoError(t, err)

	overrideDetail, err := svc.GetKnowledgeBaseSourceDetail(ctx, "wsp_1", "viewer", base.ID, source.ID)
	require.NoError(t, err)
	require.Equal(t, kb.KnowledgeBaseRepositoryAccessProfileNone, overrideDetail.RepositoryAccess[reposstore.RepoRolePrimary].EffectiveRepositoryAccessProfile)
	require.Equal(t, kbRepositoryAccessStatusNotGranted, overrideDetail.RepositoryAccess[reposstore.RepoRolePrimary].RepositoryAccessStatus)
	require.Equal(t, kb.KnowledgeBaseRepositoryAccessProfileRead, overrideDetail.RepositoryAccess[reposstore.RepoRoleInsights].EffectiveRepositoryAccessProfile)
	require.Equal(t, kbRepositoryAccessStatusCurrent, overrideDetail.RepositoryAccess[reposstore.RepoRoleInsights].RepositoryAccessStatus)
}

func TestKBServiceAnnotatesOverviewSourcesWithCurrentUserRepositoryAccess(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)
	source, err := svc.CreateSource(ctx, "wsp_1", "owner", base.ID, KBSourceMutation{SourceType: "codebase", Name: "TiDB", IndexStatus: "ready"})
	require.NoError(t, err)

	readProfile := kb.KnowledgeBaseRepositoryAccessProfileRead
	grant, err := svc.UpsertKnowledgeBasePermission(ctx, "wsp_1", "owner", base.ID, KBPermissionGrantMutation{SubjectType: "user", SubjectID: "viewer", Role: "view", RepositoryAccessProfile: &readProfile})
	require.NoError(t, err)
	require.Equal(t, kbRepositoryAccessStatusCurrent, grant.RepositoryAccessStatus)

	noneProfile := kb.KnowledgeBaseRepositoryAccessProfileNone
	_, err = svc.UpdateKnowledgeBasePermissionRepositoryOverrides(ctx, "wsp_1", "owner", base.ID, grant.ID, []KBPermissionRepositoryOverrideMutation{{SourceID: source.ID, RepoRole: reposstore.RepoRoleInsights, RepositoryAccessProfile: &noneProfile}})
	require.NoError(t, err)

	overview, err := svc.GetKnowledgeBaseOverview(ctx, "wsp_1", "viewer", base.ID)
	require.NoError(t, err)
	require.Len(t, overview.Sources, 1)
	require.Equal(t, kb.KnowledgeBaseRepositoryAccessProfileRead, overview.Sources[0].SourceRepositoryAccessProfile)
	require.Equal(t, kbRepositoryAccessStatusCurrent, overview.Sources[0].SourceRepositoryAccessStatus)
	require.Equal(t, kb.KnowledgeBaseRepositoryAccessProfileNone, overview.Sources[0].InsightsRepositoryAccessProfile)
	require.Equal(t, kbRepositoryAccessStatusNotGranted, overview.Sources[0].InsightsRepositoryAccessStatus)
}

func TestKBServiceAnnotatesCreatedAndListedSourcesWithCurrentUserRepositoryAccess(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)

	created, err := svc.CreateSource(ctx, "wsp_1", "owner", base.ID, KBSourceMutation{SourceType: "codebase", Name: "TiDB", IndexStatus: "ready"})
	require.NoError(t, err)
	require.Equal(t, kb.KnowledgeBaseRepositoryAccessProfileWrite, created.SourceRepositoryAccessProfile)
	require.Equal(t, kbRepositoryAccessStatusCurrent, created.SourceRepositoryAccessStatus)
	require.Equal(t, kb.KnowledgeBaseRepositoryAccessProfileWrite, created.InsightsRepositoryAccessProfile)
	require.Equal(t, kbRepositoryAccessStatusCurrent, created.InsightsRepositoryAccessStatus)

	sources, err := svc.ListSources(ctx, "wsp_1", "owner", base.ID, 1, 20)
	require.NoError(t, err)
	require.Len(t, sources, 1)
	require.Equal(t, kb.KnowledgeBaseRepositoryAccessProfileWrite, sources[0].SourceRepositoryAccessProfile)
	require.Equal(t, kbRepositoryAccessStatusCurrent, sources[0].SourceRepositoryAccessStatus)
	require.Equal(t, kb.KnowledgeBaseRepositoryAccessProfileWrite, sources[0].InsightsRepositoryAccessProfile)
	require.Equal(t, kbRepositoryAccessStatusCurrent, sources[0].InsightsRepositoryAccessStatus)
}

func TestKBServiceAppliesWorkspaceMemberDefaultRoleAndMemberOverrides(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	editRole := "edit"
	defaultRepositoryProfile := kb.KnowledgeBaseRepositoryAccessProfileRead
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks", DefaultWorkspaceMemberRole: &editRole, DefaultRepositoryAccessProfile: &defaultRepositoryProfile})
	require.NoError(t, err)
	require.Equal(t, "edit", base.DefaultWorkspaceMemberRole)
	require.Equal(t, kb.KnowledgeBaseRepositoryAccessProfileRead, base.DefaultRepositoryAccessProfile)

	viewerBase, err := svc.GetKnowledgeBase(ctx, "wsp_1", "viewer", base.ID)
	require.NoError(t, err)
	require.Equal(t, "edit", viewerBase.Role)
	require.Equal(t, kb.KnowledgeBaseRepositoryAccessProfileRead, viewerBase.RepositoryAccessProfile)
	require.False(t, viewerBase.CanManage)
	_, err = svc.CreateSource(ctx, "wsp_1", "viewer", base.ID, KBSourceMutation{Name: "viewer.md", SourceType: "file_repository"})
	require.NoError(t, err)

	noneGrant, err := svc.UpsertKnowledgeBasePermission(ctx, "wsp_1", "owner", base.ID, KBPermissionGrantMutation{SubjectType: "user", SubjectID: "viewer", Role: "none", RoleSet: true})
	require.NoError(t, err)
	require.Equal(t, "none", noneGrant.Role)
	require.Equal(t, kb.KnowledgeBaseRepositoryAccessProfileNone, noneGrant.RepositoryAccessProfile)

	_, err = svc.GetKnowledgeBase(ctx, "wsp_1", "viewer", base.ID)
	require.ErrorIs(t, err, ErrForbidden)
	collection, err := svc.ListKnowledgeBases(ctx, "wsp_1", "viewer", "all", "", nil, 1, 20)
	require.NoError(t, err)
	require.Empty(t, collection.Items)
	require.Zero(t, collection.Total)

	manageRole := "manage"
	writeProfile := kb.KnowledgeBaseRepositoryAccessProfileWrite
	updated, err := svc.UpdateKnowledgeBase(ctx, "wsp_1", "owner", base.ID, KBMutation{Name: base.Name, Description: base.Description, DefaultWorkspaceMemberRole: &manageRole, DefaultRepositoryAccessProfile: &writeProfile})
	require.NoError(t, err)
	require.Equal(t, "manage", updated.DefaultWorkspaceMemberRole)
	require.Equal(t, kb.KnowledgeBaseRepositoryAccessProfileWrite, updated.DefaultRepositoryAccessProfile)

	require.NoError(t, svc.DeleteKnowledgeBasePermission(ctx, "wsp_1", "owner", base.ID, noneGrant.ID))
	managerBase, err := svc.GetKnowledgeBase(ctx, "wsp_1", "viewer", base.ID)
	require.NoError(t, err)
	require.Equal(t, "manage", managerBase.Role)
	require.Equal(t, kb.KnowledgeBaseRepositoryAccessProfileWrite, managerBase.RepositoryAccessProfile)
	require.True(t, managerBase.CanManage)

	noneRole := "none"
	_, err = svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Private Runbooks", DefaultWorkspaceMemberRole: &noneRole})
	require.ErrorIs(t, err, ErrInvalidArgument)
}

func TestOrganizationAdminImplicitlyManagesEveryKnowledgeBase(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	svc.workspaceGovernanceAuthorizer = fakeWorkspaceGovernanceAuthorizer{authorize: func(_ context.Context, workspaceID, principalID string) (bool, error) {
		return workspaceID == "wsp_1" && principalID == "org-admin", nil
	}}
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "other-owner", KBMutation{Name: "Runbooks", SetupMode: "create_draft"})
	require.NoError(t, err)
	_, err = svc.CreateKnowledgeBase(ctx, "wsp_1", "other-owner", KBMutation{Name: "Published runbooks"})
	require.NoError(t, err)
	_, err = svc.UpsertKnowledgeBasePermission(ctx, "wsp_1", "other-owner", base.ID, KBPermissionGrantMutation{
		SubjectType: "user", SubjectID: "org-admin", Role: "none", RoleSet: true,
	})
	require.NoError(t, err)

	adminView, err := svc.GetKnowledgeBase(ctx, "wsp_1", "org-admin", base.ID)
	require.NoError(t, err)
	require.Equal(t, "manage", adminView.Role)
	require.True(t, adminView.CanManage)
	require.Equal(t, kb.KnowledgeBaseRepositoryAccessProfileNone, adminView.RepositoryAccessProfile, "implicit KB management must not grant source repository access")

	collection, err := svc.ListKnowledgeBases(ctx, "wsp_1", "org-admin", "draft", "", nil, 1, 20)
	require.NoError(t, err)
	require.EqualValues(t, 1, collection.Total)
	require.Len(t, collection.Items, 1)
	require.Equal(t, "manage", collection.Items[0].Role)
	require.True(t, collection.Items[0].CanManage)
	require.Equal(t, base.ID, collection.Items[0].ID)

	normalMember, err := svc.ListKnowledgeBases(ctx, "wsp_1", "denied-member", "draft", "", nil, 1, 20)
	require.NoError(t, err)
	require.Empty(t, normalMember.Items, "drafts are hidden from ordinary workspace members")
	_, err = svc.GetKnowledgeBase(ctx, "wsp_1", "denied-member", base.ID)
	require.ErrorIs(t, err, ErrForbidden)
	viewGrant, err := svc.UpsertKnowledgeBasePermission(ctx, "wsp_1", "other-owner", base.ID, KBPermissionGrantMutation{
		SubjectType: "user", SubjectID: "denied-member", Role: "view", RoleSet: true,
	})
	require.NoError(t, err)
	normalMember, err = svc.ListKnowledgeBases(ctx, "wsp_1", "denied-member", "draft", "", nil, 1, 20)
	require.NoError(t, err)
	require.Empty(t, normalMember.Items, "view-only grants must not disclose drafts")
	_, err = svc.GetKnowledgeBase(ctx, "wsp_1", "denied-member", base.ID)
	require.ErrorIs(t, err, ErrForbidden)

	_, err = svc.UpsertKnowledgeBasePermission(ctx, "wsp_1", "other-owner", base.ID, KBPermissionGrantMutation{
		GrantID: viewGrant.ID, Role: "manage", RoleSet: true,
	})
	require.NoError(t, err)
	normalMember, err = svc.ListKnowledgeBases(ctx, "wsp_1", "denied-member", "draft", "", nil, 1, 20)
	require.NoError(t, err)
	require.Len(t, normalMember.Items, 1, "explicit maintainers collaborate on drafts")
	require.Equal(t, "manage", normalMember.Items[0].Role)
	maintainerView, err := svc.GetKnowledgeBase(ctx, "wsp_1", "denied-member", base.ID)
	require.NoError(t, err)
	require.Equal(t, "manage", maintainerView.Role)
	updatedByMaintainer, err := svc.UpdateKnowledgeBase(ctx, "wsp_1", "denied-member", base.ID, KBMutation{Name: "Maintained", Description: "Updated by maintainer"})
	require.NoError(t, err)
	require.Equal(t, "Maintained", updatedByMaintainer.Name)

	updated, err := svc.UpdateKnowledgeBase(ctx, "wsp_1", "org-admin", base.ID, KBMutation{Name: "Administered", Description: "Updated by organization admin"})
	require.NoError(t, err)
	require.Equal(t, "Administered", updated.Name)
}

func TestKBServiceProtectsOwnerRoleDuringRepositoryAccessEdits(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)

	permissions, err := svc.ListKnowledgeBasePermissions(ctx, "wsp_1", "owner", base.ID)
	require.NoError(t, err)
	require.NotNil(t, permissions.Owner)

	writeProfile := kb.KnowledgeBaseRepositoryAccessProfileWrite
	updated, err := svc.UpsertKnowledgeBasePermission(ctx, "wsp_1", "owner", base.ID, KBPermissionGrantMutation{GrantID: permissions.Owner.ID, RepositoryAccessProfile: &writeProfile})
	require.NoError(t, err)
	require.Equal(t, "owner", updated.Role)
	require.Equal(t, kb.KnowledgeBaseRepositoryAccessProfileWrite, updated.RepositoryAccessProfile)

	_, err = svc.UpsertKnowledgeBasePermission(ctx, "wsp_1", "owner", base.ID, KBPermissionGrantMutation{GrantID: permissions.Owner.ID, Role: "view", RoleSet: true})
	require.ErrorIs(t, err, ErrInvalidArgument)

	permissions, err = svc.ListKnowledgeBasePermissions(ctx, "wsp_1", "owner", base.ID)
	require.NoError(t, err)
	require.NotNil(t, permissions.Owner)
	require.Equal(t, "owner", permissions.Owner.Role)
	require.Equal(t, kb.KnowledgeBaseRepositoryAccessProfileWrite, permissions.Owner.RepositoryAccessProfile)
}

func TestKBServiceUpdatesRepositoryAccessProfileIndependently(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)

	readProfile := kb.KnowledgeBaseRepositoryAccessProfileRead
	grant, err := svc.UpsertKnowledgeBasePermission(ctx, "wsp_1", "owner", base.ID, KBPermissionGrantMutation{SubjectType: "user", SubjectID: "viewer", Role: "view", RepositoryAccessProfile: &readProfile})
	require.NoError(t, err)
	require.Equal(t, "view", grant.Role)
	require.Equal(t, kb.KnowledgeBaseRepositoryAccessProfileRead, grant.RepositoryAccessProfile)
	require.Equal(t, kbRepositoryAccessStatusNotGranted, grant.RepositoryAccessStatus)

	_, err = svc.repositoryStore.UpsertAccessGrant(ctx, reposstore.ResourceRepositoryAccessGrant{
		WorkspaceID:   "wsp_1",
		ResourceType:  reposstore.ResourceTypeKBSource,
		ResourceID:    "src_1",
		RepoRole:      reposstore.RepoRolePrimary,
		RepoFullName:  "owner/runbooks",
		SubjectType:   "user",
		SubjectID:     "viewer",
		Permission:    reposstore.AccessPermissionRead,
		IntentType:    reposstore.AccessIntentTypeKBGrant,
		IntentID:      grant.ID,
		IntentProfile: kb.KnowledgeBaseRepositoryAccessProfileRead,
		Status:        reposstore.AccessStatusBlocked,
		LastError:     "ags unavailable",
		CreatedBy:     "owner",
		UpdatedBy:     "owner",
		CreatedAt:     grant.CreatedAt,
		UpdatedAt:     grant.UpdatedAt,
	})
	require.NoError(t, err)
	permissions, err := svc.ListKnowledgeBasePermissions(ctx, "wsp_1", "owner", base.ID)
	require.NoError(t, err)
	require.Len(t, permissions.Members, 2)
	var viewer KBPermissionGrant
	for _, member := range permissions.Members {
		if member.SubjectID == "viewer" {
			viewer = member
		}
	}
	require.Equal(t, kbRepositoryAccessStatusBlocked, viewer.RepositoryAccessStatus)
	require.Equal(t, "ags unavailable", viewer.RepositoryAccessLastError)

	writeProfile := kb.KnowledgeBaseRepositoryAccessProfileWrite
	profileOnly, err := svc.UpsertKnowledgeBasePermission(ctx, "wsp_1", "owner", base.ID, KBPermissionGrantMutation{GrantID: grant.ID, RepositoryAccessProfile: &writeProfile})
	require.NoError(t, err)
	require.Equal(t, "view", profileOnly.Role)
	require.Equal(t, kb.KnowledgeBaseRepositoryAccessProfileWrite, profileOnly.RepositoryAccessProfile)

	roleOnly, err := svc.UpsertKnowledgeBasePermission(ctx, "wsp_1", "owner", base.ID, KBPermissionGrantMutation{GrantID: grant.ID, Role: "edit", RoleSet: true})
	require.NoError(t, err)
	require.Equal(t, "edit", roleOnly.Role)
	require.Equal(t, kb.KnowledgeBaseRepositoryAccessProfileWrite, roleOnly.RepositoryAccessProfile)

	_, err = svc.UpsertKnowledgeBasePermission(ctx, "wsp_1", "owner", base.ID, KBPermissionGrantMutation{SubjectType: "agent", SubjectID: "agt_reader", RepositoryAccessProfile: &readProfile})
	require.ErrorIs(t, err, ErrInvalidArgument)
}

func TestKBServiceCreateSourceReconcilesExistingRepositoryAccessIntents(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	wikiStore := svc.wikiStore.(*fakeAGSRepositoryStore)
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)
	readProfile := kb.KnowledgeBaseRepositoryAccessProfileRead
	viewerGrant, err := svc.UpsertKnowledgeBasePermission(ctx, "wsp_1", "owner", base.ID, KBPermissionGrantMutation{SubjectType: "user", SubjectID: "viewer", Role: "view", RepositoryAccessProfile: &readProfile})
	require.NoError(t, err)
	agentGrant, err := svc.UpsertKnowledgeBasePermission(ctx, "wsp_1", "owner", base.ID, KBPermissionGrantMutation{SubjectType: "agent", SubjectID: "agt_reader", Role: "view"})
	require.NoError(t, err)
	wikiStore.grants = nil

	source, err := svc.CreateSource(ctx, "wsp_1", "owner", base.ID, KBSourceMutation{SourceType: "codebase", Name: "TiDB", IndexStatus: "ready"})
	require.NoError(t, err)
	require.NotEmpty(t, source.ID)
	require.Len(t, wikiStore.grants, 6)

	viewerRepos := map[string]reposservice.ResourceRepositoryCollaboratorGrant{}
	agentRepos := map[string]reposservice.ResourceRepositoryCollaboratorGrant{}
	ownerRepos := map[string]reposservice.ResourceRepositoryCollaboratorGrant{}
	for _, grant := range wikiStore.grants {
		switch grant.Actor.Subject {
		case "owner":
			ownerRepos[grant.Repository.FullName] = grant
		case "viewer":
			viewerRepos[grant.Repository.FullName] = grant
		case "agt_reader":
			agentRepos[grant.Repository.FullName] = grant
		}
	}
	require.ElementsMatch(t, []string{"owner/tidb", "owner/tidb.kb"}, sortedGrantRepoNames(ownerRepos))
	for _, grant := range ownerRepos {
		require.Equal(t, reposstore.AccessPermissionWrite, grant.Permission)
		require.True(t, grant.AllowExistingDirectUpdate)
	}
	require.ElementsMatch(t, []string{"owner/tidb", "owner/tidb.kb"}, sortedGrantRepoNames(viewerRepos))
	for _, grant := range viewerRepos {
		require.Equal(t, reposstore.AccessPermissionRead, grant.Permission)
	}
	require.ElementsMatch(t, []string{"owner/tidb", "owner/tidb.kb"}, sortedGrantRepoNames(agentRepos))

	viewerRows, err := svc.repositoryStore.ListAccessGrantsForIntent(ctx, "wsp_1", reposstore.AccessIntentTypeKBGrant, viewerGrant.ID)
	require.NoError(t, err)
	require.Len(t, viewerRows, 2)
	for _, row := range viewerRows {
		require.Equal(t, reposstore.AccessStatusCurrent, row.Status)
	}
	agentRows, err := svc.repositoryStore.ListAccessGrantsForIntent(ctx, "wsp_1", reposstore.AccessIntentTypeKBGrant, agentGrant.ID)
	require.NoError(t, err)
	require.Len(t, agentRows, 2)
}

func TestKBServiceCreateSourceGrantsCreatorRepositoryAccessIntent(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	wikiStore := svc.wikiStore.(*fakeAGSRepositoryStore)
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)
	wikiStore.grants = nil

	_, err = svc.CreateSource(ctx, "wsp_1", "owner", base.ID, KBSourceMutation{SourceType: "codebase", Name: "TiDB", IndexStatus: "ready"})
	require.NoError(t, err)

	permissions, err := svc.ListKnowledgeBasePermissions(ctx, "wsp_1", "owner", base.ID)
	require.NoError(t, err)
	require.NotNil(t, permissions.Owner)
	require.Equal(t, kb.KnowledgeBaseRepositoryAccessProfileWrite, permissions.Owner.RepositoryAccessProfile)
	require.Equal(t, kbRepositoryAccessStatusCurrent, permissions.Owner.RepositoryAccessStatus)

	ownerRows, err := svc.repositoryStore.ListAccessGrantsForIntent(ctx, "wsp_1", reposstore.AccessIntentTypeKBGrant, permissions.Owner.ID)
	require.NoError(t, err)
	require.Len(t, ownerRows, 2)
	for _, row := range ownerRows {
		require.Equal(t, "owner", row.SubjectID)
		require.Equal(t, reposstore.AccessPermissionWrite, row.Permission)
		require.Equal(t, kb.KnowledgeBaseRepositoryAccessProfileWrite, row.IntentProfile)
		require.Equal(t, reposstore.AccessStatusCurrent, row.Status)
	}

	ownerRepos := map[string]reposservice.ResourceRepositoryCollaboratorGrant{}
	for _, grant := range wikiStore.grants {
		if grant.Actor.Subject == "owner" {
			ownerRepos[grant.Repository.FullName] = grant
		}
	}
	require.ElementsMatch(t, []string{"owner/tidb", "owner/tidb.kb"}, sortedGrantRepoNames(ownerRepos))
	for _, grant := range ownerRepos {
		require.Equal(t, reposstore.AccessPermissionWrite, grant.Permission)
		require.True(t, grant.AllowExistingDirectUpdate)
	}
}

func TestKBServiceReconcilesBlockedRepositoryAccessGrant(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	wikiStore := svc.wikiStore.(*fakeAGSRepositoryStore)
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)
	_, err = svc.CreateSource(ctx, "wsp_1", "owner", base.ID, KBSourceMutation{SourceType: "codebase", Name: "TiDB", IndexStatus: "ready"})
	require.NoError(t, err)
	readProfile := kb.KnowledgeBaseRepositoryAccessProfileRead
	wikiStore.grantErr = errors.New("ags unavailable")

	grant, err := svc.UpsertKnowledgeBasePermission(ctx, "wsp_1", "owner", base.ID, KBPermissionGrantMutation{SubjectType: "user", SubjectID: "viewer", Role: "view", RepositoryAccessProfile: &readProfile})
	require.NoError(t, err)
	require.Equal(t, kbRepositoryAccessStatusBlocked, grant.RepositoryAccessStatus)
	require.Equal(t, "ags unavailable", grant.RepositoryAccessLastError)

	wikiStore.grantErr = nil
	repaired, err := svc.ReconcileKnowledgeBasePermissionRepositoryAccess(ctx, "wsp_1", "owner", base.ID, grant.ID)
	require.NoError(t, err)
	require.Equal(t, kbRepositoryAccessStatusCurrent, repaired.RepositoryAccessStatus)
	require.Empty(t, repaired.RepositoryAccessLastError)
	events, err := svc.ListAuditEvents(ctx, "wsp_1", "owner", base.ID, 1, 50)
	require.NoError(t, err)
	var repairEvent kb.AuditEvent
	for _, event := range events {
		if event.Action == "kb.repository_access.repair" {
			repairEvent = event
			break
		}
	}
	require.NotEmpty(t, repairEvent.ID)
	require.Contains(t, repairEvent.Details, `"status":"current"`)
}

func TestKBServiceReconcilesBlockedAgentRepositoryAccessGrant(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	wikiStore := svc.wikiStore.(*fakeAGSRepositoryStore)
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)
	_, err = svc.CreateSource(ctx, "wsp_1", "owner", base.ID, KBSourceMutation{SourceType: "codebase", Name: "TiDB", IndexStatus: "ready"})
	require.NoError(t, err)
	wikiStore.grantErr = errors.New("ags unavailable")

	grant, err := svc.UpsertKnowledgeBasePermission(ctx, "wsp_1", "owner", base.ID, KBPermissionGrantMutation{SubjectType: "agent", SubjectID: "agt_reader", Role: "view"})
	require.NoError(t, err)
	require.Equal(t, kbRepositoryAccessStatusBlocked, grant.RepositoryAccessStatus)
	require.Equal(t, "ags unavailable", grant.RepositoryAccessLastError)

	wikiStore.grantErr = nil
	repaired, err := svc.ReconcileKnowledgeBasePermissionRepositoryAccess(ctx, "wsp_1", "owner", base.ID, grant.ID)
	require.NoError(t, err)
	require.Equal(t, kb.KnowledgeBaseRepositoryAccessProfileRead, repaired.RepositoryAccessProfile)
	require.Equal(t, kbRepositoryAccessStatusCurrent, repaired.RepositoryAccessStatus)
	require.Empty(t, repaired.RepositoryAccessLastError)
}

func TestKBServiceRevokesRepositoryAccessOnlyWhenNoOtherIntentNeedsIt(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	wikiStore := svc.wikiStore.(*fakeAGSRepositoryStore)
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)
	_, err = svc.CreateSource(ctx, "wsp_1", "owner", base.ID, KBSourceMutation{SourceType: "codebase", Name: "TiDB", IndexStatus: "ready"})
	require.NoError(t, err)
	readProfile := kb.KnowledgeBaseRepositoryAccessProfileRead
	grant, err := svc.UpsertKnowledgeBasePermission(ctx, "wsp_1", "owner", base.ID, KBPermissionGrantMutation{SubjectType: "user", SubjectID: "viewer", Role: "view", RepositoryAccessProfile: &readProfile})
	require.NoError(t, err)
	rows, err := svc.repositoryStore.ListAccessGrantsForIntent(ctx, "wsp_1", reposstore.AccessIntentTypeKBGrant, grant.ID)
	require.NoError(t, err)
	require.Len(t, rows, 2)
	upsertSiblingRepositoryAccessIntent(t, ctx, svc, rows, "kbg_other_intent", reposstore.AccessPermissionRead, true)
	wikiStore.revokes = nil
	noneProfile := kb.KnowledgeBaseRepositoryAccessProfileNone

	updated, err := svc.UpsertKnowledgeBasePermission(ctx, "wsp_1", "owner", base.ID, KBPermissionGrantMutation{GrantID: grant.ID, RepositoryAccessProfile: &noneProfile})
	require.NoError(t, err)
	require.Equal(t, kbRepositoryAccessStatusNotGranted, updated.RepositoryAccessStatus)
	require.Empty(t, wikiStore.revokes)

	rows, err = svc.repositoryStore.ListAccessGrantsForIntent(ctx, "wsp_1", reposstore.AccessIntentTypeKBGrant, grant.ID)
	require.NoError(t, err)
	require.Len(t, rows, 2)
	for _, row := range rows {
		require.Equal(t, reposstore.AccessStatusRevoked, row.Status)
	}
}

func TestKBServiceDoesNotRevokeNonMaterializedRepositoryAccess(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	wikiStore := svc.wikiStore.(*fakeAGSRepositoryStore)
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)
	_, err = svc.CreateSource(ctx, "wsp_1", "owner", base.ID, KBSourceMutation{SourceType: "codebase", Name: "TiDB", IndexStatus: "ready"})
	require.NoError(t, err)
	readProfile := kb.KnowledgeBaseRepositoryAccessProfileRead
	grant, err := svc.UpsertKnowledgeBasePermission(ctx, "wsp_1", "owner", base.ID, KBPermissionGrantMutation{SubjectType: "user", SubjectID: "viewer", Role: "view", RepositoryAccessProfile: &readProfile})
	require.NoError(t, err)
	rows, err := svc.repositoryStore.ListAccessGrantsForIntent(ctx, "wsp_1", reposstore.AccessIntentTypeKBGrant, grant.ID)
	require.NoError(t, err)
	require.Len(t, rows, 2)
	for _, row := range rows {
		row.Materialized = false
		_, err = svc.repositoryStore.UpsertAccessGrant(ctx, row)
		require.NoError(t, err)
	}
	wikiStore.revokes = nil
	noneProfile := kb.KnowledgeBaseRepositoryAccessProfileNone

	updated, err := svc.UpsertKnowledgeBasePermission(ctx, "wsp_1", "owner", base.ID, KBPermissionGrantMutation{GrantID: grant.ID, RepositoryAccessProfile: &noneProfile})
	require.NoError(t, err)
	require.Equal(t, kbRepositoryAccessStatusNotGranted, updated.RepositoryAccessStatus)
	require.Empty(t, wikiStore.revokes)
}

func TestKBServiceRevokesMaterializedRepositoryAccessWhenOnlyNonMaterializedIntentRemains(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	wikiStore := svc.wikiStore.(*fakeAGSRepositoryStore)
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)
	_, err = svc.CreateSource(ctx, "wsp_1", "owner", base.ID, KBSourceMutation{SourceType: "codebase", Name: "TiDB", IndexStatus: "ready"})
	require.NoError(t, err)
	readProfile := kb.KnowledgeBaseRepositoryAccessProfileRead
	grant, err := svc.UpsertKnowledgeBasePermission(ctx, "wsp_1", "owner", base.ID, KBPermissionGrantMutation{SubjectType: "user", SubjectID: "viewer", Role: "view", RepositoryAccessProfile: &readProfile})
	require.NoError(t, err)
	rows, err := svc.repositoryStore.ListAccessGrantsForIntent(ctx, "wsp_1", reposstore.AccessIntentTypeKBGrant, grant.ID)
	require.NoError(t, err)
	require.Len(t, rows, 2)
	upsertSiblingRepositoryAccessIntent(t, ctx, svc, rows, "kbg_independent_intent", reposstore.AccessPermissionRead, false)
	wikiStore.revokes = nil
	noneProfile := kb.KnowledgeBaseRepositoryAccessProfileNone

	_, err = svc.UpsertKnowledgeBasePermission(ctx, "wsp_1", "owner", base.ID, KBPermissionGrantMutation{GrantID: grant.ID, RepositoryAccessProfile: &noneProfile})
	require.NoError(t, err)
	require.Len(t, wikiStore.revokes, 2)
	var revoked []string
	for _, revoke := range wikiStore.revokes {
		revoked = append(revoked, revoke.Repository.FullName)
	}
	require.ElementsMatch(t, []string{"owner/tidb", "owner/tidb.kb"}, revoked)
}

func TestKBServiceDeletePermissionKeepsGrantWhenRepositoryRevokeBlocked(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	wikiStore := svc.wikiStore.(*fakeAGSRepositoryStore)
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)
	_, err = svc.CreateSource(ctx, "wsp_1", "owner", base.ID, KBSourceMutation{SourceType: "codebase", Name: "TiDB", IndexStatus: "ready"})
	require.NoError(t, err)
	readProfile := kb.KnowledgeBaseRepositoryAccessProfileRead
	grant, err := svc.UpsertKnowledgeBasePermission(ctx, "wsp_1", "owner", base.ID, KBPermissionGrantMutation{SubjectType: "user", SubjectID: "viewer", Role: "view", RepositoryAccessProfile: &readProfile})
	require.NoError(t, err)
	wikiStore.revokeErr = errors.New("ags unavailable")

	err = svc.DeleteKnowledgeBasePermission(ctx, "wsp_1", "owner", base.ID, grant.ID)

	require.ErrorIs(t, err, ErrConflict)
	_, err = svc.store.GetKnowledgeBaseGrantByID(ctx, "wsp_1", base.ID, grant.ID)
	require.NoError(t, err)
	rows, err := svc.repositoryStore.ListAccessGrantsForIntent(ctx, "wsp_1", reposstore.AccessIntentTypeKBGrant, grant.ID)
	require.NoError(t, err)
	require.NotEmpty(t, rows)
	for _, row := range rows {
		require.Equal(t, reposstore.AccessStatusBlocked, row.Status)
		require.Equal(t, "ags unavailable", row.LastError)
	}
	permissions, err := svc.ListKnowledgeBasePermissions(ctx, "wsp_1", "owner", base.ID)
	require.NoError(t, err)
	var viewer KBPermissionGrant
	for _, member := range permissions.Members {
		if member.SubjectID == "viewer" {
			viewer = member
		}
	}
	require.Equal(t, kbRepositoryAccessStatusBlocked, viewer.RepositoryAccessStatus)
	require.Equal(t, "ags unavailable", viewer.RepositoryAccessLastError)
}

func ptrTo[T any](value T) *T { return &value }

func upsertSiblingRepositoryAccessIntent(t *testing.T, ctx context.Context, svc *KBService, rows []reposstore.ResourceRepositoryAccessGrant, intentID, permission string, materialized bool) {
	t.Helper()
	for _, row := range rows {
		_, err := svc.repositoryStore.UpsertAccessGrant(ctx, reposstore.ResourceRepositoryAccessGrant{
			WorkspaceID:   row.WorkspaceID,
			ResourceType:  row.ResourceType,
			ResourceID:    row.ResourceID,
			RepoRole:      row.RepoRole,
			RepoFullName:  row.RepoFullName,
			AGSRepoID:     row.AGSRepoID,
			SubjectType:   row.SubjectType,
			SubjectID:     row.SubjectID,
			Permission:    permission,
			IntentType:    reposstore.AccessIntentTypeKBGrant,
			IntentID:      intentID,
			IntentProfile: kb.KnowledgeBaseRepositoryAccessProfileRead,
			Materialized:  materialized,
			Status:        reposstore.AccessStatusCurrent,
			CreatedBy:     "owner",
			UpdatedBy:     "owner",
			CreatedAt:     row.CreatedAt,
			UpdatedAt:     row.UpdatedAt,
		})
		require.NoError(t, err)
	}
}

func sortedGrantRepoNames(grants map[string]reposservice.ResourceRepositoryCollaboratorGrant) []string {
	names := make([]string, 0, len(grants))
	for name := range grants {
		names = append(names, name)
	}
	sort.Strings(names)
	return names
}

func TestKBServiceSourceProvisionsInsightsMappings(t *testing.T) {
	ctx := context.Background()
	svc := newKBServiceForTest(t)
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", KBMutation{Name: "Runbooks"})
	require.NoError(t, err)

	source, err := svc.CreateSource(ctx, "wsp_1", "owner", base.ID, KBSourceMutation{
		SourceType: "database_schema",
		Name:       "Production Schema",
		Config:     `{"databaseType":"mysql"}`,
	})
	require.NoError(t, err)

	repos, err := svc.repositoryStore.ListForResource(ctx, "wsp_1", reposstore.ResourceTypeKBSource, source.ID)
	require.NoError(t, err)
	require.Len(t, repos, 2)
	byRole := map[string]string{}
	for _, repo := range repos {
		byRole[repo.RepoRole] = repo.RepoFullName
	}
	require.Equal(t, "owner/production-schema", byRole[reposstore.RepoRolePrimary])
	require.Equal(t, "owner/production-schema.kb", byRole[reposstore.RepoRoleInsights])
}
