package kb

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

	"github.com/stretchr/testify/require"
	"gorm.io/gorm"

	"github.com/tidbcloud/AgentTeam/internal/service/memory"
	"github.com/tidbcloud/AgentTeam/internal/store/kb"
	"github.com/tidbcloud/AgentTeam/internal/store/resource/activity"
	"github.com/tidbcloud/AgentTeam/internal/store/resource/repos"
)

type recordingKBWikiReader struct {
	searchRequests []KBWikiSearchRequest
	readRequests   []KBWikiReadRequest
	treeRequests   []KBWikiTreeRequest
	searchByRepo   map[string]KBWikiSearchResponse
	pageByRepoSlug map[string]KBWikiPage
	pageErrBySlug  map[string]error
	treeByRepo     map[string][]KBWikiTreeEntry
	treeByRepoPath map[string][]KBWikiTreeEntry
}

func (reader *recordingKBWikiReader) SearchWiki(ctx context.Context, request KBWikiSearchRequest) (KBWikiSearchResponse, error) {
	reader.searchRequests = append(reader.searchRequests, request)
	return reader.searchByRepo[request.Owner+"/"+request.Repo], nil
}

func (reader *recordingKBWikiReader) ReadWikiPage(ctx context.Context, request KBWikiReadRequest) (KBWikiPage, error) {
	reader.readRequests = append(reader.readRequests, request)
	key := request.Owner + "/" + request.Repo + ":" + request.Slug
	if err := reader.pageErrBySlug[key]; err != nil {
		return KBWikiPage{}, err
	}
	return reader.pageByRepoSlug[key], nil
}

func (reader *recordingKBWikiReader) ListWikiTree(ctx context.Context, request KBWikiTreeRequest) ([]KBWikiTreeEntry, error) {
	reader.treeRequests = append(reader.treeRequests, request)
	if entries, ok := reader.treeByRepoPath[request.Owner+"/"+request.Repo+":"+request.Path]; ok {
		return entries, nil
	}
	return reader.treeByRepo[request.Owner+"/"+request.Repo], nil
}

func newRetrievalKBService(t *testing.T, wiki KBWikiReader) *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{}, &repos.ResourceRepository{}, &repos.ResourceRepositoryLifecycleEvent{}, &repos.ResourceRepositoryAccessGrant{}, &activity.ResourceActivityEvent{}, &activity.ResourceActivityLatestCommit{}))
	now := func() time.Time { return time.Date(2026, 6, 6, 12, 0, 0, 0, time.UTC) }
	return NewKBServiceWithOptions(kb.New(db), now, KBServiceOptions{
		WikiReader:           wiki,
		WikiStore:            &fakeAGSRepositoryStore{},
		RepositoryStore:      repos.New(db),
		AgentOwnershipSource: fakeAgentOwnershipSource{agents: []memory.AgentOwnership{{ID: "agt_reader"}}},
	})
}

func seedRetrievalKB(t *testing.T, svc *KBService) string {
	t.Helper()
	base, err := svc.CreateKnowledgeBase(context.Background(), "wsp_1", "usr_1", KBMutation{Name: "Spec KB"})
	require.NoError(t, err)
	_, err = svc.CreateSource(context.Background(), "wsp_1", "usr_1", base.ID, KBSourceMutation{
		SourceType:  "codebase",
		Name:        "AgentTeam",
		IndexStatus: "ready",
	})
	require.NoError(t, err)
	_, err = svc.CreateSource(context.Background(), "wsp_1", "usr_1", base.ID, KBSourceMutation{
		SourceType:  "codebase",
		Name:        "Pending",
		IndexStatus: "pending",
	})
	require.NoError(t, err)
	_, err = svc.GrantAgentKnowledgeBaseAccess(context.Background(), "wsp_1", "usr_1", "agt_reader", base.ID, "read")
	require.NoError(t, err)
	return base.ID
}

func TestProjectRetrievalSourcesKeepsQueryCountConstantForBoundedBatch(t *testing.T) {
	svc := newRetrievalKBService(t, &recordingKBWikiReader{})
	db := svc.store.DB()
	now := time.Date(2026, 8, 21, 8, 0, 0, 0, time.UTC)
	sources := make([]kb.Source, 20)
	revisions := make([]kb.SourceRevision, 20)
	repositories := make([]repos.ResourceRepository, 20)
	for index := range sources {
		sourceID := fmt.Sprintf("src_batch_%02d", index)
		sources[index] = kb.Source{ID: sourceID, WorkspaceID: "wsp_batch", KnowledgeBaseID: "kb_batch", SourceType: "codebase", Status: "ready", IndexStatus: "ready", CreatedAt: now, UpdatedAt: now}
		revisions[index] = kb.SourceRevision{ID: "rev_" + sourceID, WorkspaceID: "wsp_batch", KnowledgeBaseID: "kb_batch", SourceID: sourceID, PublishedWikiRevision: "wiki-rev", IsCurrent: true, CreatedAt: now, UpdatedAt: now}
		repositories[index] = repos.ResourceRepository{ID: "repo_" + sourceID, WorkspaceID: "wsp_batch", ResourceType: repos.ResourceTypeKBSource, ResourceID: sourceID, RepoRole: repos.RepoRoleInsights, RepoFullName: "org/" + sourceID, CreatedAt: now, UpdatedAt: now}
	}
	require.NoError(t, db.Create(&revisions).Error)
	require.NoError(t, db.Create(&repositories).Error)
	var queryCount atomic.Int64
	require.NoError(t, db.Callback().Query().Before("gorm:query").Register("test:kb_retrieval_source_batch", func(*gorm.DB) { queryCount.Add(1) }))
	queryCount.Store(0)
	_, err := svc.projectRetrievalSources(t.Context(), "wsp_batch", sources[:1])
	require.NoError(t, err)
	oneSourceQueries := queryCount.Load()
	queryCount.Store(0)
	projected, err := svc.projectRetrievalSources(t.Context(), "wsp_batch", sources)
	require.NoError(t, err)
	require.Len(t, projected, len(sources))
	require.Equal(t, oneSourceQueries, queryCount.Load(), "retrieval source hydration must remain one bounded batch per owning store")
}

func retrievalSourceID(t *testing.T, svc *KBService, kbID, repoFullName string) string {
	t.Helper()
	contextResult, err := svc.DescribeKnowledgeContext(context.Background(), "wsp_1", "usr_1", KBRetrievalContextRequest{AgentID: "agt_reader", KnowledgeBaseIDs: []string{kbID}})
	require.NoError(t, err)
	require.Len(t, contextResult.KnowledgeBases, 1)
	for _, source := range contextResult.KnowledgeBases[0].Sources {
		if source.RepoFullName == repoFullName {
			return source.SourceID
		}
	}
	require.Failf(t, "missing retrieval source", "repo %s not found", repoFullName)
	return ""
}

func TestKBServiceSearchesReadySourcesAndReturnsStableCitations(t *testing.T) {
	wiki := &recordingKBWikiReader{searchByRepo: map[string]KBWikiSearchResponse{
		"usr_1/agentteam.kb": {Results: []KBWikiSearchHit{{Slug: "wiki-mcp", Title: "Wiki MCP", Snippet: "AGS wiki", Score: 3.0}}},
	}}
	svc := newRetrievalKBService(t, wiki)
	kbID := seedRetrievalKB(t, svc)

	result, err := svc.SearchKnowledge(context.Background(), "wsp_1", "usr_1", KBSearchRequest{AgentID: "agt_reader", KnowledgeBaseIDs: []string{kbID}, Query: "mcp", Limit: 10, AuthorizationHeader: "Bearer user"})
	require.NoError(t, err)
	require.Len(t, wiki.searchRequests, 1)
	require.Equal(t, "usr_1", wiki.searchRequests[0].Owner)
	require.Len(t, result.Hits, 1)
	require.Equal(t, "Wiki MCP", result.Hits[0].Title)
	require.Contains(t, result.Hits[0].CitationURI, "kb://"+kbID+"/sources/")
	require.Contains(t, result.Hits[0].CitationURI, "/wiki/wiki-mcp")
	require.Equal(t, "insights_repo", result.Hits[0].SourceKind)
	require.Equal(t, "usr_1/agentteam.kb", result.Hits[0].RepoFullName)
}

func TestKBServiceSearchClampsMaxResultsAndReturnsOpaqueCitations(t *testing.T) {
	wiki := &recordingKBWikiReader{searchByRepo: map[string]KBWikiSearchResponse{
		"usr_1/agentteam.kb": {Results: []KBWikiSearchHit{
			{Slug: "first", Title: "First", Snippet: "one", Score: 3},
			{Slug: "second", Title: "Second", Snippet: "two", Score: 2},
		}},
	}}
	svc := newRetrievalKBService(t, wiki)
	kbID := seedRetrievalKB(t, svc)

	result, err := svc.SearchKnowledge(context.Background(), "wsp_1", "usr_1", KBSearchRequest{AgentID: "agt_reader", KnowledgeBaseIDs: []string{kbID}, Query: "mcp", Limit: 1})

	require.NoError(t, err)
	require.Len(t, result.Hits, 1)
	require.Equal(t, 2, result.Total)
	require.True(t, result.Truncated)
	require.True(t, result.HasMore)
	require.Contains(t, result.Warnings, "results truncated; refine your query for more specific results (no pagination)")
	require.NotEmpty(t, result.Hits[0].CitationID)
	require.NotContains(t, result.Hits[0].CitationID, "kb://")

	_, err = svc.SearchKnowledge(context.Background(), "wsp_1", "usr_1", KBSearchRequest{AgentID: "agt_reader", KnowledgeBaseIDs: []string{kbID}, Query: "mcp", Limit: 51})
	require.NoError(t, err)
	require.Equal(t, 50, wiki.searchRequests[len(wiki.searchRequests)-1].Limit)
}

func TestKBServiceFetchUsesCitationIDsAndReturnsStructuredItemErrors(t *testing.T) {
	wiki := &recordingKBWikiReader{
		searchByRepo: map[string]KBWikiSearchResponse{
			"usr_1/agentteam.kb": {Results: []KBWikiSearchHit{{Slug: "overview", Title: "Overview", Snippet: "snippet", SHA: "sha-fetch", Score: 4}}},
		},
		pageByRepoSlug: map[string]KBWikiPage{
			"usr_1/agentteam.kb:overview": {Slug: "overview", Title: "Overview", Body: "abcdef", SHA: "sha-fetch"},
		},
	}
	svc := newRetrievalKBService(t, wiki)
	kbID := seedRetrievalKB(t, svc)
	search, err := svc.SearchKnowledge(context.Background(), "wsp_1", "usr_1", KBSearchRequest{AgentID: "agt_reader", KnowledgeBaseIDs: []string{kbID}, Query: "overview"})
	require.NoError(t, err)
	require.Len(t, search.Hits, 1)

	result, err := svc.FetchKnowledge(context.Background(), "wsp_1", "usr_1", KBFetchRequest{AgentID: "agt_reader", CitationIDs: []string{search.Hits[0].CitationID, "cit_invalid"}, MaxCharsPerItem: 3})

	require.NoError(t, err)
	require.Len(t, result.Items, 2)
	require.Equal(t, "Overview", result.Items[0].Title)
	require.Equal(t, "abc", result.Items[0].Content)
	require.True(t, result.Items[0].Truncated)
	require.Contains(t, result.Warnings, "content truncated; refine kb_search and fetch a more specific citation")
	require.NotNil(t, result.Items[1].Error)
	require.Equal(t, "stale", result.Items[1].Error.Code)
	require.Contains(t, result.Items[1].Error.Message, "run kb_search again")
}

func TestKBServiceRetrievalTargetsInsightsRepoAndFiltersSources(t *testing.T) {
	wiki := &recordingKBWikiReader{
		searchByRepo: map[string]KBWikiSearchResponse{
			"usr_1/agentteam.kb": {Results: []KBWikiSearchHit{{Slug: "overview", Title: "Insights Overview", Snippet: "insights", SHA: "wiki-sha", Score: 4.0}}},
		},
		pageByRepoSlug: map[string]KBWikiPage{
			"usr_1/agentteam.kb:overview": {Slug: "overview", Title: "Insights Overview", Body: "insights content", SHA: "wiki-sha"},
		},
	}
	svc := newRetrievalKBService(t, wiki)
	kbID := seedRetrievalKB(t, svc)
	sourceID := retrievalSourceID(t, svc, kbID, "usr_1/agentteam.kb")
	now := time.Date(2026, 6, 6, 12, 30, 0, 0, time.UTC)
	_, err := svc.store.CreateSourceRevision(context.Background(), kb.SourceRevision{
		ID:                    "rev_agentteam",
		WorkspaceID:           "wsp_1",
		KnowledgeBaseID:       kbID,
		SourceID:              sourceID,
		InsightsCommitSHA:     "insights-sha",
		PublishedWikiRevision: "wiki-rev",
		FreshnessStatus:       "stale",
		IsCurrent:             true,
		CreatedAt:             now,
		UpdatedAt:             now,
	})
	require.NoError(t, err)

	contextResult, err := svc.DescribeKnowledgeContext(context.Background(), "wsp_1", "usr_1", KBRetrievalContextRequest{AgentID: "agt_reader", KnowledgeBaseIDs: []string{kbID}, SourceIDs: []string{sourceID}})
	require.NoError(t, err)
	require.Len(t, contextResult.KnowledgeBases[0].Sources, 1)
	require.Equal(t, "insights_repo", contextResult.KnowledgeBases[0].Sources[0].SourceKind)
	require.Equal(t, "usr_1/agentteam.kb", contextResult.KnowledgeBases[0].Sources[0].RepoFullName)
	require.Equal(t, "wiki-rev", contextResult.KnowledgeBases[0].Sources[0].Revision)
	require.Equal(t, "stale", contextResult.KnowledgeBases[0].Sources[0].FreshnessStatus)

	search, err := svc.SearchKnowledge(context.Background(), "wsp_1", "usr_1", KBSearchRequest{AgentID: "agt_reader", KnowledgeBaseIDs: []string{kbID}, SourceIDs: []string{sourceID}, Query: "overview"})
	require.NoError(t, err)
	require.Len(t, search.Hits, 1)
	require.Equal(t, "insights_repo", search.Hits[0].SourceKind)
	require.Equal(t, "insights", wiki.searchRequests[0].RepoRole)
	require.Equal(t, "wiki-rev", wiki.searchRequests[0].Revision)
	require.Equal(t, "usr_1/agentteam.kb", search.Hits[0].RepoFullName)
	require.Equal(t, "wiki-sha", search.Hits[0].Route.Revision)

	read, err := svc.ReadKnowledge(context.Background(), "wsp_1", "usr_1", KBReadRequest{AgentID: "agt_reader", Route: search.Hits[0].Route})
	require.NoError(t, err)
	require.Equal(t, "insights content", read.Body)
	require.Equal(t, "usr_1", wiki.readRequests[0].Owner)
	require.Equal(t, "agentteam.kb", wiki.readRequests[0].Repo)
	require.Equal(t, "wiki-sha", wiki.readRequests[0].Revision)
}

func TestKBServiceFallsBackToMirroredSourceFilesBeforeInsightsExist(t *testing.T) {
	wiki := &recordingKBWikiReader{
		searchByRepo: map[string]KBWikiSearchResponse{
			"usr_1/agentteam": {Results: []KBWikiSearchHit{{Slug: "docs/architecture.md", Title: "docs/architecture.md", Snippet: "AgentTeam server and web layers", SHA: "source-commit", Score: 4.0}}},
		},
		pageByRepoSlug: map[string]KBWikiPage{
			"usr_1/agentteam:docs/architecture.md": {Slug: "docs/architecture.md", Title: "docs/architecture.md", Body: "# Architecture\nAgentTeam server and web layers", SHA: "source-commit"},
		},
	}
	svc := newRetrievalKBService(t, wiki)
	kbID := seedRetrievalKB(t, svc)
	sourceID := retrievalSourceID(t, svc, kbID, "usr_1/agentteam.kb")
	now := time.Date(2026, 6, 6, 12, 30, 0, 0, time.UTC)
	lastSuccessAt := now.Add(-5 * time.Minute)
	_, err := svc.repositoryStore.Upsert(context.Background(), repos.ResourceRepository{
		ID:           "rr_agentteam_mirror",
		WorkspaceID:  "wsp_1",
		ResourceType: repos.ResourceTypeKBSource,
		ResourceID:   sourceID,
		RepoRole:     repos.RepoRoleMirror,
		OwnerKind:    "personal",
		RepoOwner:    "usr_1",
		RepoName:     "agentteam",
		RepoFullName: "usr_1/agentteam",
		AGSRepoID:    "repo-agentteam",
		DefaultRef:   "main",
		Visibility:   "private",
		UserVisible:  true,
		CreatedBy:    "usr_1",
		UpdatedBy:    "usr_1",
		CreatedAt:    now,
		UpdatedAt:    now,
	})
	require.NoError(t, err)
	_, err = svc.store.CreateSourceMirror(context.Background(), kb.SourceMirror{
		SourceID:           sourceID,
		WorkspaceID:        "wsp_1",
		KnowledgeBaseID:    kbID,
		UpstreamType:       kb.SourceMirrorUpstreamTypeGenericGit,
		RemoteURL:          "https://git.example/agentteam.git",
		DefaultRef:         "main",
		Enabled:            true,
		SyncStatus:         kb.SourceMirrorSyncStatusRunning,
		LastSuccessAt:      &lastSuccessAt,
		LastMirroredCommit: "source-commit",
		CreatedBy:          "usr_1",
		UpdatedBy:          "usr_1",
		CreatedAt:          now,
		UpdatedAt:          now,
	})
	require.NoError(t, err)

	contextResult, err := svc.DescribeKnowledgeContext(context.Background(), "wsp_1", "usr_1", KBRetrievalContextRequest{AgentID: "agt_reader", KnowledgeBaseIDs: []string{kbID}, SourceIDs: []string{sourceID}})
	require.NoError(t, err)
	require.Len(t, contextResult.KnowledgeBases[0].Sources, 1)
	contextSource := contextResult.KnowledgeBases[0].Sources[0]
	require.Equal(t, "source_repo", contextSource.SourceKind)
	require.Equal(t, repos.RepoRoleMirror, contextSource.RepoRole)
	require.Equal(t, "usr_1/agentteam", contextSource.RepoFullName)
	require.Equal(t, "source-commit", contextSource.Revision)
	require.Equal(t, "stale", contextSource.FreshnessStatus)

	search, err := svc.SearchKnowledge(context.Background(), "wsp_1", "usr_1", KBSearchRequest{AgentID: "agt_reader", KnowledgeBaseIDs: []string{kbID}, SourceIDs: []string{sourceID}, Query: "architecture", AuthorizationHeader: "Bearer user"})
	require.NoError(t, err)
	require.Len(t, search.Hits, 1)
	require.Equal(t, "source_repo", search.Hits[0].SourceKind)
	require.Equal(t, repos.RepoRoleMirror, wiki.searchRequests[0].RepoRole)
	require.Equal(t, "source-commit", wiki.searchRequests[0].Revision)
	require.Equal(t, "usr_1/agentteam", search.Hits[0].RepoFullName)
	require.Equal(t, "source-commit", search.Hits[0].Route.Revision)

	read, err := svc.ReadKnowledge(context.Background(), "wsp_1", "usr_1", KBReadRequest{AgentID: "agt_reader", Route: search.Hits[0].Route, AuthorizationHeader: "Bearer user"})
	require.NoError(t, err)
	require.Equal(t, "# Architecture\nAgentTeam server and web layers", read.Body)
	require.Equal(t, "usr_1", wiki.readRequests[0].Owner)
	require.Equal(t, "agentteam", wiki.readRequests[0].Repo)
	require.Equal(t, repos.RepoRoleMirror, wiki.readRequests[0].RepoRole)
	require.Equal(t, "source-commit", wiki.readRequests[0].Revision)
}

func TestKBServiceDescribesRetrievalContextForAgentSelection(t *testing.T) {
	svc := newRetrievalKBService(t, nil)
	kbID := seedRetrievalKB(t, svc)

	contextResult, err := svc.DescribeKnowledgeContext(context.Background(), "wsp_1", "usr_1", KBRetrievalContextRequest{AgentID: "agt_reader", KnowledgeBaseIDs: []string{kbID}})
	require.NoError(t, err)
	require.Len(t, contextResult.KnowledgeBases, 1)
	base := contextResult.KnowledgeBases[0]
	require.Equal(t, kbID, base.ID)
	require.Equal(t, "Spec KB", base.Name)
	require.Len(t, base.Sources, 2)
	require.False(t, base.HasMoreSources)

	sourcesByRepo := map[string]KBRetrievalContextSource{}
	for _, source := range base.Sources {
		sourcesByRepo[source.RepoFullName] = source
	}
	require.Equal(t, "insights_repo", sourcesByRepo["usr_1/agentteam.kb"].SourceKind)
	require.Equal(t, "ready", sourcesByRepo["usr_1/agentteam.kb"].IndexStatus)
	require.Equal(t, "insights_repo", sourcesByRepo["usr_1/pending.kb"].SourceKind)
	require.Equal(t, "pending", sourcesByRepo["usr_1/pending.kb"].IndexStatus)
}

func TestKBServiceOverviewAndSourceDetailExposeHealthActivityAndRepos(t *testing.T) {
	svc := newRetrievalKBService(t, nil)
	kbID := seedRetrievalKB(t, svc)
	sourceID := retrievalSourceID(t, svc, kbID, "usr_1/agentteam.kb")
	now := time.Date(2026, 6, 6, 14, 0, 0, 0, time.UTC)
	_, err := svc.store.CreateSourceRevision(context.Background(), kb.SourceRevision{
		ID:                    "rev_overview",
		WorkspaceID:           "wsp_1",
		KnowledgeBaseID:       kbID,
		SourceID:              sourceID,
		SourceCommitSHA:       "src-sha",
		InsightsCommitSHA:     "ins-sha",
		PublishedWikiRevision: "wiki-rev",
		FreshnessStatus:       "current",
		IsCurrent:             true,
		CreatedAt:             now,
		UpdatedAt:             now,
	})
	require.NoError(t, err)
	_, err = svc.store.CreateAnalysisRun(context.Background(), kb.AnalysisRun{
		ID:                    "run_overview",
		WorkspaceID:           "wsp_1",
		KnowledgeBaseID:       kbID,
		SourceID:              sourceID,
		Trigger:               "manual",
		Status:                "succeeded",
		InputKind:             "source_repo",
		OutputRepoRole:        "insights",
		OutputCommitSHA:       "ins-sha",
		PublishedWikiRevision: "wiki-rev",
		Progress:              "{}",
		CreatedBy:             "usr_1",
		CreatedAt:             now,
		UpdatedAt:             now,
	})
	require.NoError(t, err)
	_, err = svc.store.CreateJob(context.Background(), kb.Job{ID: "job_overview", WorkspaceID: "wsp_1", KnowledgeBaseID: kbID, SourceID: sourceID, JobType: KBJobTypeGenerateRepoWikiFromCode, Status: KBJobStatusSucceeded, Progress: "{}", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)

	overview, err := svc.GetKnowledgeBaseOverview(context.Background(), "wsp_1", "usr_1", kbID)
	require.NoError(t, err)
	require.Equal(t, kbID, overview.KnowledgeBaseID)
	require.Equal(t, 2, overview.Health.TotalSources)
	require.NotEmpty(t, overview.Activity)
	require.Contains(t, overview.Sources[0].Revision+overview.Sources[1].Revision, "wiki-rev")

	detail, err := svc.GetKnowledgeBaseSourceDetail(context.Background(), "wsp_1", "usr_1", kbID, sourceID)
	require.NoError(t, err)
	require.Equal(t, sourceID, detail.Source.ID)
	require.NotNil(t, detail.CurrentRevision)
	require.Equal(t, "wiki-rev", detail.CurrentRevision.PublishedWikiRevision)
	require.NotNil(t, detail.Repositories.Insights)
	require.Equal(t, "usr_1/agentteam.kb", detail.Repositories.Insights.RepoFullName)
	require.Len(t, detail.AnalysisRuns, 1)
	require.Len(t, detail.Jobs, 1)
	require.NotEmpty(t, detail.RecommendedPaths)
}

func TestKBServiceDerivesSourceImportProgressFromActiveJobs(t *testing.T) {
	svc := newRetrievalKBService(t, nil)
	kbID := seedRetrievalKB(t, svc)
	sourceID := retrievalSourceID(t, svc, kbID, "usr_1/agentteam.kb")
	now := time.Date(2026, 6, 6, 14, 0, 0, 0, time.UTC)
	_, err := svc.store.CreateSourceMirror(context.Background(), kb.SourceMirror{SourceID: sourceID, WorkspaceID: "wsp_1", KnowledgeBaseID: kbID, UpstreamType: kb.SourceMirrorUpstreamTypeGenericGit, RemoteURL: "https://93.184.216.34/org/repo.git", DefaultRef: "main", IntervalSeconds: 60, Enabled: true, SyncStatus: kb.SourceMirrorSyncStatusIdle, CreatedBy: "usr_1", UpdatedBy: "usr_1", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	_, err = svc.store.CreateJob(context.Background(), kb.Job{
		ID:              "job_progress",
		WorkspaceID:     "wsp_1",
		KnowledgeBaseID: kbID,
		SourceID:        sourceID,
		JobType:         KBJobTypeSyncSourceMirror,
		Status:          KBJobStatusRunning,
		Progress:        `{"stage":"syncing_ags_repo","message":"snapshot /tmp/agentteam/source token=secret Authorization: Bearer auth-secret credentialRef: secret://kb/source-token access_token: ghp_secret"}`,
		StartedAt:       &now,
		CreatedAt:       now,
		UpdatedAt:       now.Add(time.Minute),
	})
	require.NoError(t, err)

	detail, err := svc.GetKnowledgeBaseSourceDetail(context.Background(), "wsp_1", "usr_1", kbID, sourceID)
	require.NoError(t, err)
	require.NotNil(t, detail.Source.ImportProgress)
	require.Equal(t, KBSourceImportProgressStateRunning, detail.Source.ImportProgress.State)
	require.Equal(t, KBSourceImportProgressSeverityInfo, detail.Source.ImportProgress.Severity)
	require.Equal(t, KBSourceImportProgressPhaseMirrorSync, detail.Source.ImportProgress.Phase)
	require.Equal(t, KBSourceMirrorStageSyncingAGS, detail.Source.ImportProgress.Stage)
	require.Equal(t, "job_progress", detail.Source.ImportProgress.CurrentJobID)
	require.NotNil(t, detail.Source.ImportProgress.Percent)
	require.Equal(t, 55, *detail.Source.ImportProgress.Percent)
	require.Contains(t, detail.Source.ImportProgress.Message, "[local path]")
	require.Contains(t, detail.Source.ImportProgress.Message, "Authorization: Bearer [redacted]")
	require.NotContains(t, detail.Source.ImportProgress.Message, "auth-secret")
	require.NotContains(t, detail.Source.ImportProgress.Message, "secret://")
	require.NotContains(t, detail.Source.ImportProgress.Message, "ghp_secret")
	steps := importProgressStepsByID(detail.Source.ImportProgress.Steps)
	require.Equal(t, KBSourceImportProgressStepStatusSucceeded, steps[KBSourceImportProgressStepSourceCreated].Status)
	require.Equal(t, KBSourceImportProgressStepStatusRunning, steps[KBSourceImportProgressStepMirrorSynced].Status)

	overview, err := svc.GetKnowledgeBaseOverview(context.Background(), "wsp_1", "usr_1", kbID)
	require.NoError(t, err)
	progressBySource := map[string]*kb.SourceImportProgress{}
	for _, source := range overview.Sources {
		progressBySource[source.SourceID] = source.ImportProgress
	}
	require.Equal(t, KBSourceImportProgressStateRunning, progressBySource[sourceID].State)
}

func TestKBServiceDerivesSourceImportProgressFromFailedMirrorSync(t *testing.T) {
	svc := newRetrievalKBService(t, nil)
	kbID := seedRetrievalKB(t, svc)
	sourceID := retrievalSourceID(t, svc, kbID, "usr_1/agentteam.kb")
	now := time.Date(2026, 6, 6, 15, 0, 0, 0, time.UTC)
	finished := now.Add(time.Minute)
	_, err := svc.store.CreateSourceMirror(context.Background(), kb.SourceMirror{SourceID: sourceID, WorkspaceID: "wsp_1", KnowledgeBaseID: kbID, UpstreamType: kb.SourceMirrorUpstreamTypeGenericGit, RemoteURL: "https://93.184.216.34/org/repo.git", DefaultRef: "main", IntervalSeconds: 60, Enabled: true, SyncStatus: kb.SourceMirrorSyncStatusIdle, CreatedBy: "usr_1", UpdatedBy: "usr_1", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	_, err = svc.store.CreateJob(context.Background(), kb.Job{
		ID:              "job_failed",
		WorkspaceID:     "wsp_1",
		KnowledgeBaseID: kbID,
		SourceID:        sourceID,
		JobType:         KBJobTypeSyncSourceMirror,
		Status:          KBJobStatusFailed,
		Progress:        `{"stage":"failed","message":"mirror sync failed token=secret"}`,
		Error:           "mirror sync failed Authorization: Bearer auth-secret",
		StartedAt:       &now,
		FinishedAt:      &finished,
		CreatedAt:       now,
		UpdatedAt:       finished,
	})
	require.NoError(t, err)

	detail, err := svc.GetKnowledgeBaseSourceDetail(context.Background(), "wsp_1", "usr_1", kbID, sourceID)
	require.NoError(t, err)
	require.NotNil(t, detail.Source.ImportProgress)
	require.Equal(t, KBSourceImportProgressStateFailed, detail.Source.ImportProgress.State)
	require.Equal(t, "job_failed", detail.Source.ImportProgress.CurrentJobID)

	overview, err := svc.GetKnowledgeBaseOverview(context.Background(), "wsp_1", "usr_1", kbID)
	require.NoError(t, err)
	var failedActivity KBOverviewActivity
	for _, activity := range overview.Activity {
		if activity.ID == "job_failed" {
			failedActivity = activity
			break
		}
	}
	require.Equal(t, "kb.source.mirror_sync.failed", failedActivity.Action)
	require.Equal(t, "source", failedActivity.ResourceType)
	require.Equal(t, sourceID, failedActivity.ResourceID)
	require.Contains(t, failedActivity.Summary, "Authorization: Bearer [redacted]")
	require.NotContains(t, failedActivity.Summary, "auth-secret")
}

func importProgressStepsByID(steps []kb.SourceImportProgressStep) map[string]kb.SourceImportProgressStep {
	out := map[string]kb.SourceImportProgressStep{}
	for _, step := range steps {
		out[step.ID] = step
	}
	return out
}

func TestKBServiceReadRejectsCitationOutsideSelectedKB(t *testing.T) {
	wiki := &recordingKBWikiReader{}
	svc := newRetrievalKBService(t, wiki)
	kbID := seedRetrievalKB(t, svc)

	_, err := svc.ReadKnowledge(context.Background(), "wsp_1", "usr_1", KBReadRequest{AgentID: "agt_reader", KnowledgeBaseIDs: []string{kbID}, CitationURI: "kb://other/sources/source/wiki/page", AuthorizationHeader: "Bearer user"})
	require.Error(t, err)
}

func TestKBServiceReadPinsRevisionFromAGS(t *testing.T) {
	wiki := &recordingKBWikiReader{pageByRepoSlug: map[string]KBWikiPage{"usr_1/agentteam.kb:agent-kb": {Slug: "agent-kb", Title: "Agent KB", Body: "content", SHA: "sha123"}}}
	svc := newRetrievalKBService(t, wiki)
	kbID := seedRetrievalKB(t, svc)
	sourceID := retrievalSourceID(t, svc, kbID, "usr_1/agentteam.kb")

	page, err := svc.ReadKnowledge(context.Background(), "wsp_1", "usr_1", KBReadRequest{AgentID: "agt_reader", KnowledgeBaseIDs: []string{kbID}, SourceID: sourceID, Slug: "agent-kb", AuthorizationHeader: "Bearer user"})
	require.NoError(t, err)
	require.Equal(t, "sha123", page.SHA)
	require.Contains(t, page.CitationURI, "rev=sha123")
	require.Equal(t, "content", page.Body)
}

func TestKBServiceReadRejectsSourceOutsideSelectedScope(t *testing.T) {
	wiki := &recordingKBWikiReader{pageByRepoSlug: map[string]KBWikiPage{"usr_1/agentteam.kb:agent-kb": {Slug: "agent-kb", Title: "Agent KB", Body: "content", SHA: "sha123"}}}
	svc := newRetrievalKBService(t, wiki)
	kbID := seedRetrievalKB(t, svc)
	sourceID := retrievalSourceID(t, svc, kbID, "usr_1/agentteam.kb")
	pendingSourceID := retrievalSourceID(t, svc, kbID, "usr_1/pending.kb")

	_, err := svc.ReadKnowledge(context.Background(), "wsp_1", "usr_1", KBReadRequest{AgentID: "agt_reader", KnowledgeBaseIDs: []string{kbID}, SourceIDs: []string{pendingSourceID}, SourceID: sourceID, Slug: "agent-kb", AuthorizationHeader: "Bearer user"})

	require.Error(t, err)
	require.Empty(t, wiki.readRequests)
}

func TestKBServiceReadRejectsPendingSource(t *testing.T) {
	wiki := &recordingKBWikiReader{pageByRepoSlug: map[string]KBWikiPage{"usr_1/pending.kb:agent-kb": {Slug: "agent-kb", Title: "Agent KB", Body: "content", SHA: "sha123"}}}
	svc := newRetrievalKBService(t, wiki)
	kbID := seedRetrievalKB(t, svc)
	pendingSourceID := retrievalSourceID(t, svc, kbID, "usr_1/pending.kb")

	_, err := svc.ReadKnowledge(context.Background(), "wsp_1", "usr_1", KBReadRequest{AgentID: "agt_reader", KnowledgeBaseIDs: []string{kbID}, SourceID: pendingSourceID, Slug: "agent-kb", AuthorizationHeader: "Bearer user"})

	require.Error(t, err)
	require.Empty(t, wiki.readRequests)
}

func TestKBServiceListsWikiTreeForSelectedSource(t *testing.T) {
	wiki := &recordingKBWikiReader{treeByRepo: map[string][]KBWikiTreeEntry{
		"usr_1/agentteam.kb": {{Path: "docs/agent-kb.md", Name: "agent-kb.md", Kind: "page", Slug: "agent-kb", Title: "Agent KB", SHA: "sha-tree", Size: 42}},
	}}
	svc := newRetrievalKBService(t, wiki)
	kbID := seedRetrievalKB(t, svc)
	sourceID := retrievalSourceID(t, svc, kbID, "usr_1/agentteam.kb")

	tree, err := svc.ListKnowledgeTree(context.Background(), "wsp_1", "usr_1", KBTreeRequest{AgentID: "agt_reader", KnowledgeBaseIDs: []string{kbID}, SourceID: sourceID, Path: "docs", AuthorizationHeader: "Bearer user"})
	require.NoError(t, err)
	require.Len(t, wiki.treeRequests, 1)
	require.Equal(t, "usr_1", wiki.treeRequests[0].Owner)
	require.Equal(t, "agentteam.kb", wiki.treeRequests[0].Repo)
	require.Equal(t, "docs", wiki.treeRequests[0].Path)
	require.Len(t, tree.Entries, 1)
	require.Equal(t, "Agent KB", tree.Entries[0].Title)
	require.Equal(t, "sha-tree", tree.Entries[0].SHA)
	require.Contains(t, tree.Entries[0].CitationURI, "/wiki/agent-kb")
}

func TestKBServiceReadsAndTreesStructuredRepoRoute(t *testing.T) {
	wiki := &recordingKBWikiReader{
		pageByRepoSlug: map[string]KBWikiPage{
			"usr_1/agentteam.kb:docs/agent-kb": {Slug: "docs/agent-kb", Title: "Agent KB", Body: "route content", SHA: "sha-read"},
		},
		treeByRepo: map[string][]KBWikiTreeEntry{
			"usr_1/agentteam.kb": {{Path: "docs/agent-kb.md", Name: "agent-kb.md", Kind: "page", Slug: "docs/agent-kb", Title: "Agent KB", SHA: "sha-tree", Size: 42}},
		},
	}
	svc := newRetrievalKBService(t, wiki)
	kbID := seedRetrievalKB(t, svc)

	read, err := svc.ReadKnowledge(context.Background(), "wsp_1", "usr_1", KBReadRequest{
		AgentID:          "agt_reader",
		KnowledgeBaseIDs: []string{kbID},
		RepoID:           "repo-insights",
		RepoRole:         "insights",
		Path:             "docs/agent-kb.md",
		Revision:         "main",
	})
	require.NoError(t, err)
	require.Equal(t, "route content", read.Body)
	require.Equal(t, "docs/agent-kb", wiki.readRequests[0].Slug)
	require.Equal(t, "insights", wiki.readRequests[0].RepoRole)
	require.Equal(t, "main", wiki.readRequests[0].Revision)

	tree, err := svc.ListKnowledgeTree(context.Background(), "wsp_1", "usr_1", KBTreeRequest{
		AgentID:          "agt_reader",
		KnowledgeBaseIDs: []string{kbID},
		RepoID:           "repo-insights",
		RepoRole:         "insights",
		Path:             "docs",
		Revision:         "main",
	})
	require.NoError(t, err)
	require.Len(t, tree.Entries, 1)
	require.Equal(t, "docs", wiki.treeRequests[0].Path)
	require.Equal(t, "main", wiki.treeRequests[0].Revision)
}

func TestKBServiceTreeAcceptsCitationURI(t *testing.T) {
	wiki := &recordingKBWikiReader{treeByRepo: map[string][]KBWikiTreeEntry{
		"usr_1/agentteam.kb": {{Path: "docs/agent-kb.md", Name: "agent-kb.md", Kind: "page", Slug: "agent-kb", Title: "Agent KB", SHA: "sha-tree", Size: 42}},
	}}
	svc := newRetrievalKBService(t, wiki)
	kbID := seedRetrievalKB(t, svc)
	sourceID := retrievalSourceID(t, svc, kbID, "usr_1/agentteam.kb")

	tree, err := svc.ListKnowledgeTree(context.Background(), "wsp_1", "usr_1", KBTreeRequest{AgentID: "agt_reader", CitationURI: "kb://" + kbID + "/sources/" + sourceID + "/wiki/agent-kb?rev=sha-citation", AuthorizationHeader: "Bearer user"})
	require.NoError(t, err)
	require.Len(t, tree.Entries, 1)
	require.Equal(t, "sha-citation", wiki.treeRequests[0].Revision)
}

func TestKBServiceTreeSupportsDepthPaginationAndFilters(t *testing.T) {
	includePages := true
	includeDirs := false
	wiki := &recordingKBWikiReader{treeByRepoPath: map[string][]KBWikiTreeEntry{
		"usr_1/agentteam.kb:": {
			{Path: "architecture", Name: "architecture", Kind: "dir", IsDir: true},
			{Path: "overview.md", Name: "overview.md", Kind: "page", Slug: "overview", Title: "Overview", SHA: "sha-overview"},
		},
		"usr_1/agentteam.kb:architecture": {
			{Path: "architecture/billing.md", Name: "billing.md", Kind: "page", Slug: "architecture/billing", Title: "Billing", SHA: "sha-billing"},
			{Path: "architecture/runtime.md", Name: "runtime.md", Kind: "page", Slug: "architecture/runtime", Title: "Runtime", SHA: "sha-runtime"},
		},
	}}
	svc := newRetrievalKBService(t, wiki)
	kbID := seedRetrievalKB(t, svc)
	sourceID := retrievalSourceID(t, svc, kbID, "usr_1/agentteam.kb")

	tree, err := svc.ListKnowledgeTree(context.Background(), "wsp_1", "usr_1", KBTreeRequest{AgentID: "agt_reader", KnowledgeBaseIDs: []string{kbID}, SourceID: sourceID, Depth: 2, Limit: 2, IncludePages: &includePages, IncludeDirs: &includeDirs})

	require.NoError(t, err)
	require.Len(t, wiki.treeRequests, 2)
	require.Equal(t, "", wiki.treeRequests[0].Path)
	require.Equal(t, "architecture", wiki.treeRequests[1].Path)
	require.Len(t, tree.Entries, 2)
	require.Equal(t, 3, tree.Total)
	require.True(t, tree.HasMore)
	require.Equal(t, "2", tree.NextCursor)
	require.Equal(t, "overview", tree.Entries[0].Slug)
	require.Equal(t, "architecture/billing", tree.Entries[1].Slug)

	next, err := svc.ListKnowledgeTree(context.Background(), "wsp_1", "usr_1", KBTreeRequest{AgentID: "agt_reader", KnowledgeBaseIDs: []string{kbID}, SourceID: sourceID, Depth: 2, Limit: 2, Cursor: tree.NextCursor, IncludePages: &includePages, IncludeDirs: &includeDirs})
	require.NoError(t, err)
	require.Len(t, next.Entries, 1)
	require.Equal(t, "architecture/runtime", next.Entries[0].Slug)
	require.False(t, next.HasMore)
}

func TestKBServiceReadShapesSectionsRangesAndSummary(t *testing.T) {
	body := "# Overview\nintro paragraph\n\n## Billing\nbilling details\n\n### Plans\nplan details\n\n## Runtime\nruntime details"
	wiki := &recordingKBWikiReader{pageByRepoSlug: map[string]KBWikiPage{"usr_1/agentteam.kb:overview": {Slug: "overview", Title: "Overview", Body: body, SHA: "sha123"}}}
	svc := newRetrievalKBService(t, wiki)
	kbID := seedRetrievalKB(t, svc)
	sourceID := retrievalSourceID(t, svc, kbID, "usr_1/agentteam.kb")

	section, err := svc.ReadKnowledge(context.Background(), "wsp_1", "usr_1", KBReadRequest{AgentID: "agt_reader", KnowledgeBaseIDs: []string{kbID}, SourceID: sourceID, Slug: "overview", Section: "Billing", Format: "plain", MaxBytes: 40})

	require.NoError(t, err)
	require.Equal(t, "plain", section.Format)
	require.Contains(t, section.Body, "Billing")
	require.Contains(t, section.Body, "billing details")
	require.NotContains(t, section.Body, "Runtime")
	require.True(t, section.Truncated)
	require.Equal(t, len([]byte(body)), section.OriginalBytes)

	headings, err := svc.ReadKnowledge(context.Background(), "wsp_1", "usr_1", KBReadRequest{AgentID: "agt_reader", KnowledgeBaseIDs: []string{kbID}, SourceID: sourceID, Slug: "overview", HeadingsOnly: true})
	require.NoError(t, err)
	require.Equal(t, "headings", headings.Format)
	require.Contains(t, headings.Body, "## Billing")
	require.NotContains(t, headings.Body, "intro paragraph")
}

func TestKBServiceCapabilitiesManifestStatAndBatch(t *testing.T) {
	wiki := &recordingKBWikiReader{
		pageByRepoSlug: map[string]KBWikiPage{
			"usr_1/agentteam.kb:overview": {Slug: "overview", Title: "Overview", Body: "# Overview", SHA: "sha-overview"},
		},
		pageErrBySlug: map[string]error{
			"usr_1/agentteam.kb:missing": codedNotFound("not_found", "missing page"),
		},
		treeByRepoPath: map[string][]KBWikiTreeEntry{
			"usr_1/agentteam.kb:": {{Path: "overview.md", Name: "overview.md", Kind: "page", Slug: "overview", Title: "Overview", SHA: "sha-overview"}},
		},
	}
	svc := newRetrievalKBService(t, wiki)
	kbID := seedRetrievalKB(t, svc)
	sourceID := retrievalSourceID(t, svc, kbID, "usr_1/agentteam.kb")

	caps, err := svc.DescribeKnowledgeCapabilities(context.Background(), "wsp_1", "usr_1", KBCapabilitiesRequest{AgentID: "agt_reader", KnowledgeBaseIDs: []string{kbID}})
	require.NoError(t, err)
	require.True(t, caps.KnowledgeBases[0].Operations["read"].Supported)
	require.True(t, caps.KnowledgeBases[0].Operations["read"].MCPToolExposed)
	require.False(t, caps.KnowledgeBases[0].Operations["create"].Supported)
	require.False(t, caps.KnowledgeBases[0].Operations["create"].Writable)
	require.True(t, caps.KnowledgeBases[0].Operations["create"].BackendWritable)
	require.False(t, caps.KnowledgeBases[0].Operations["create"].MCPToolExposed)

	manifest, err := svc.BuildKnowledgeManifest(context.Background(), "wsp_1", "usr_1", KBManifestRequest{AgentID: "agt_reader", KnowledgeBaseIDs: []string{kbID}, SourceIDs: []string{sourceID}, Limit: 10})
	require.NoError(t, err)
	require.Equal(t, 1, manifest.TotalSources)
	require.Equal(t, 1, manifest.TotalPages)
	require.Equal(t, "overview", manifest.KnowledgeBases[0].Sources[0].Entries[0].Slug)
	require.Empty(t, manifest.KnowledgeBases[0].Sources[0].Entries[0].KnowledgeBaseName)
	fullEntries := false
	fullManifest, err := svc.BuildKnowledgeManifest(context.Background(), "wsp_1", "usr_1", KBManifestRequest{AgentID: "agt_reader", KnowledgeBaseIDs: []string{kbID}, SourceIDs: []string{sourceID}, Limit: 10, Compact: &fullEntries})
	require.NoError(t, err)
	require.Equal(t, "Spec KB", fullManifest.KnowledgeBases[0].Sources[0].Entries[0].KnowledgeBaseName)

	stat, err := svc.StatKnowledge(context.Background(), "wsp_1", "usr_1", KBStatRequest{AgentID: "agt_reader", KnowledgeBaseIDs: []string{kbID}, SourceID: sourceID, Slug: "overview"})
	require.NoError(t, err)
	require.True(t, stat.Exists)
	require.Equal(t, "Overview", stat.Title)

	missing, err := svc.StatKnowledge(context.Background(), "wsp_1", "usr_1", KBStatRequest{AgentID: "agt_reader", KnowledgeBaseIDs: []string{kbID}, SourceID: sourceID, Slug: "missing"})
	require.NoError(t, err)
	require.False(t, missing.Exists)
	require.Equal(t, "not_found", missing.Error.Code)

	batch, err := svc.BatchReadKnowledge(context.Background(), "wsp_1", "usr_1", KBBatchReadRequest{AgentID: "agt_reader", KnowledgeBaseIDs: []string{kbID}, SourceIDs: []string{sourceID}, Items: []KBReadRequest{{Slug: "overview", HeadingsOnly: true}, {Slug: "missing"}}})
	require.NoError(t, err)
	require.Len(t, batch.Results, 2)
	require.NotNil(t, batch.Results[0].Page)
	require.Equal(t, "not_found", batch.Results[1].Error.Code)

	treeBatch, err := svc.BatchKnowledgeTree(context.Background(), "wsp_1", "usr_1", KBBatchTreeRequest{AgentID: "agt_reader", KnowledgeBaseIDs: []string{kbID}, Items: []KBTreeRequest{{SourceID: sourceID, Limit: 1}}})
	require.NoError(t, err)
	require.Len(t, treeBatch.Results, 1)
	require.Len(t, treeBatch.Results[0].Tree.Entries, 1)

	require.True(t, errors.Is(codedNotFound("not_found", "x"), ErrNotFound))
}

func TestKBServiceTreeReturnsNotFoundForMissingNonRootPath(t *testing.T) {
	wiki := &recordingKBWikiReader{treeByRepoPath: map[string][]KBWikiTreeEntry{
		"usr_1/agentteam.kb:does-not-exist": {},
	}}
	svc := newRetrievalKBService(t, wiki)
	kbID := seedRetrievalKB(t, svc)
	sourceID := retrievalSourceID(t, svc, kbID, "usr_1/agentteam.kb")

	_, err := svc.ListKnowledgeTree(context.Background(), "wsp_1", "usr_1", KBTreeRequest{AgentID: "agt_reader", KnowledgeBaseIDs: []string{kbID}, SourceID: sourceID, Path: "does-not-exist"})

	require.ErrorIs(t, err, ErrNotFound)
	require.Len(t, wiki.treeRequests, 1)
}

func TestNewKBServiceWithOptionsWiresKBWikiReader(t *testing.T) {
	wiki := &recordingKBWikiReader{}

	svc := NewKBServiceWithOptions(nil, nil, KBServiceOptions{WikiReader: wiki})

	require.Same(t, wiki, svc.wikiReader)
}

func TestKBServiceRetrievalUsesAgentAccessNotActingUser(t *testing.T) {
	wiki := &recordingKBWikiReader{searchByRepo: map[string]KBWikiSearchResponse{
		"usr_1/agentteam.kb": {Results: []KBWikiSearchHit{{Slug: "wiki-mcp", Title: "Wiki MCP", Score: 2}}},
	}}
	svc := newRetrievalKBService(t, wiki)
	kbID := seedRetrievalKB(t, svc)

	result, err := svc.SearchKnowledge(context.Background(), "wsp_1", "different-acting-user", KBSearchRequest{AgentID: "agt_reader", KnowledgeBaseIDs: []string{kbID}, Query: "mcp"})

	require.NoError(t, err)
	require.Len(t, result.Hits, 1)
	require.Len(t, wiki.searchRequests, 1)
}

func TestKBServiceSearchReturnsNotFoundForExplicitMissingSource(t *testing.T) {
	wiki := &recordingKBWikiReader{searchByRepo: map[string]KBWikiSearchResponse{
		"usr_1/agentteam.kb": {Results: []KBWikiSearchHit{{Slug: "wiki-mcp", Title: "Wiki MCP", Score: 2}}},
	}}
	svc := newRetrievalKBService(t, wiki)
	kbID := seedRetrievalKB(t, svc)

	_, err := svc.SearchKnowledge(context.Background(), "wsp_1", "usr_1", KBSearchRequest{AgentID: "agt_reader", KnowledgeBaseIDs: []string{kbID}, SourceIDs: []string{"src_missing"}, Query: "mcp"})

	require.ErrorIs(t, err, ErrNotFound)
	require.Empty(t, wiki.searchRequests)
}

func TestKBServiceAgentRetrievalDoesNotFallBackToActingUserAccess(t *testing.T) {
	wiki := &recordingKBWikiReader{searchByRepo: map[string]KBWikiSearchResponse{
		"usr_1/agentteam.kb": {Results: []KBWikiSearchHit{{Slug: "wiki-mcp", Title: "Wiki MCP", Score: 2}}},
	}}
	svc := newRetrievalKBService(t, wiki)
	base, err := svc.CreateKnowledgeBase(context.Background(), "wsp_1", "usr_1", KBMutation{Name: "Spec KB"})
	require.NoError(t, err)
	_, err = svc.CreateSource(context.Background(), "wsp_1", "usr_1", base.ID, KBSourceMutation{
		SourceType:  "codebase",
		Name:        "AgentTeam",
		IndexStatus: "ready",
	})
	require.NoError(t, err)
	readProfile := kb.KnowledgeBaseRepositoryAccessProfileRead
	_, err = svc.UpsertKnowledgeBasePermission(context.Background(), "wsp_1", "usr_1", base.ID, KBPermissionGrantMutation{SubjectType: "user", SubjectID: "usr_viewer", Role: "view", RepositoryAccessProfile: &readProfile})
	require.NoError(t, err)

	contextResult, err := svc.DescribeKnowledgeContext(context.Background(), "wsp_1", "agt_reader", KBRetrievalContextRequest{AgentID: "agt_without_grant", ActingUserID: "usr_viewer"})

	require.NoError(t, err)
	require.Empty(t, contextResult.KnowledgeBases)

	result, err := svc.SearchKnowledge(context.Background(), "wsp_1", "agt_reader", KBSearchRequest{AgentID: "agt_without_grant", ActingUserID: "usr_viewer", Query: "mcp", AuthorizationHeader: "Bearer agent", ActingUserAuthorizationHeader: "Bearer viewer"})
	require.NoError(t, err)
	require.Empty(t, result.Hits)
	require.Empty(t, wiki.searchRequests)

	_, err = svc.DescribeKnowledgeContext(context.Background(), "wsp_1", "agt_reader", KBRetrievalContextRequest{AgentID: "agt_without_grant", ActingUserID: "usr_viewer", KnowledgeBaseIDs: []string{base.ID}})
	require.ErrorIs(t, err, ErrForbidden)
}

func TestKBServiceRetrievalSupportsCurrentUserWithoutAgentPrincipal(t *testing.T) {
	wiki := &recordingKBWikiReader{searchByRepo: map[string]KBWikiSearchResponse{
		"usr_1/agentteam.kb": {Results: []KBWikiSearchHit{{Slug: "wiki-mcp", Title: "Wiki MCP", Score: 2}}},
	}}
	svc := newRetrievalKBService(t, wiki)
	base, err := svc.CreateKnowledgeBase(context.Background(), "wsp_1", "usr_1", KBMutation{Name: "Spec KB"})
	require.NoError(t, err)
	_, err = svc.CreateSource(context.Background(), "wsp_1", "usr_1", base.ID, KBSourceMutation{SourceType: "codebase", Name: "AgentTeam", IndexStatus: "ready"})
	require.NoError(t, err)
	readProfile := kb.KnowledgeBaseRepositoryAccessProfileRead
	_, err = svc.UpsertKnowledgeBasePermission(context.Background(), "wsp_1", "usr_1", base.ID, KBPermissionGrantMutation{
		SubjectType: "user", SubjectID: "usr_viewer", Role: "view", RepositoryAccessProfile: &readProfile,
	})
	require.NoError(t, err)

	contextResult, err := svc.DescribeKnowledgeContext(context.Background(), "wsp_1", "usr_viewer", KBRetrievalContextRequest{
		ActingUserID: "usr_viewer", KnowledgeBaseIDs: []string{base.ID},
	})
	require.NoError(t, err)
	require.Len(t, contextResult.KnowledgeBases, 1)

	result, err := svc.SearchKnowledge(context.Background(), "wsp_1", "usr_viewer", KBSearchRequest{
		ActingUserID: "usr_viewer", KnowledgeBaseIDs: []string{base.ID}, Query: "mcp",
		ActingUserAuthorizationHeader: "Bearer viewer",
	})
	require.NoError(t, err)
	require.Len(t, result.Hits, 1)
	require.Equal(t, "Bearer viewer", wiki.searchRequests[0].AuthorizationHeader)

	_, err = svc.DescribeKnowledgeContext(context.Background(), "wsp_1", "usr_viewer", KBRetrievalContextRequest{
		ActingUserID: "usr_viewer", KnowledgeBaseIDs: []string{"kb_forbidden"},
	})
	require.ErrorIs(t, err, ErrForbidden)
}

func TestKBServiceRetrievalRejectsSelectedKBOutsideAgentAccess(t *testing.T) {
	wiki := &recordingKBWikiReader{}
	svc := newRetrievalKBService(t, wiki)
	kbID := seedRetrievalKB(t, svc)

	_, err := svc.DescribeKnowledgeContext(context.Background(), "wsp_1", "usr_1", KBRetrievalContextRequest{AgentID: "agt_other", KnowledgeBaseIDs: []string{kbID}})

	require.Error(t, err)
}

func TestKBServiceRetrievalDefaultsToAgentAccessibleKBsWhenSelectionOmitted(t *testing.T) {
	svc := newRetrievalKBService(t, nil)
	kbID := seedRetrievalKB(t, svc)

	contextResult, err := svc.DescribeKnowledgeContext(context.Background(), "wsp_1", "usr_1", KBRetrievalContextRequest{AgentID: "agt_reader"})

	require.NoError(t, err)
	require.Len(t, contextResult.KnowledgeBases, 1)
	require.Equal(t, kbID, contextResult.KnowledgeBases[0].ID)
}

func TestKBServiceRetrievalRestrictsSelectedDraftsToCollaborators(t *testing.T) {
	wiki := &recordingKBWikiReader{searchByRepo: map[string]KBWikiSearchResponse{
		"owner/agentteam.kb": {Results: []KBWikiSearchHit{{Slug: "draft", Title: "Draft", Score: 2}}},
	}}
	svc := newRetrievalKBService(t, wiki)
	base, _, err := svc.CreateKnowledgeBaseSetup(context.Background(), "wsp_1", "owner", KBMutation{Name: "Draft KB"})
	require.NoError(t, err)
	source, err := svc.CreateSource(context.Background(), "wsp_1", "owner", base.ID, KBSourceMutation{SourceType: "codebase", Name: "AgentTeam", IndexStatus: "ready"})
	require.NoError(t, err)
	now := svc.now()
	_, err = svc.store.CreateSourceRevision(context.Background(), kb.SourceRevision{
		ID: "rev_draft", WorkspaceID: "wsp_1", KnowledgeBaseID: base.ID, SourceID: source.ID,
		SourceCommitSHA: "source-commit", PublishedWikiRevision: "wiki-commit", IsCurrent: true,
		FreshnessStatus: "current", CreatedAt: now, UpdatedAt: now,
	})
	require.NoError(t, err)
	readProfile := kb.KnowledgeBaseRepositoryAccessProfileRead
	_, err = svc.UpsertKnowledgeBasePermission(context.Background(), "wsp_1", "owner", base.ID, KBPermissionGrantMutation{
		SubjectType: "user", SubjectID: "viewer", Role: "view", RepositoryAccessProfile: &readProfile,
	})
	require.NoError(t, err)
	grant, err := svc.store.GetKnowledgeBaseGrant(context.Background(), "wsp_1", base.ID, "user", "viewer")
	require.NoError(t, err)
	_, err = svc.repositoryStore.UpsertAccessGrant(context.Background(), repos.ResourceRepositoryAccessGrant{
		WorkspaceID: "wsp_1", ResourceType: repos.ResourceTypeKBSource, ResourceID: source.ID,
		RepoRole: repos.RepoRoleInsights, RepoFullName: "owner/agentteam.kb",
		SubjectType: "user", SubjectID: "viewer", Permission: repos.AccessPermissionRead,
		IntentType: repos.AccessIntentTypeKBGrant, IntentID: grant.ID,
		IntentProfile: kb.KnowledgeBaseRepositoryAccessProfileRead, Materialized: true,
		Status: repos.AccessStatusCurrent, CreatedBy: "owner", UpdatedBy: "owner",
		CreatedAt: now, UpdatedAt: now,
	})
	require.NoError(t, err)
	_, err = svc.GrantAgentKnowledgeBaseAccess(context.Background(), "wsp_1", "owner", "agt_reader", base.ID, "read")
	require.NoError(t, err)

	_, err = svc.DescribeKnowledgeContext(context.Background(), "wsp_1", "viewer", KBRetrievalContextRequest{ActingUserID: "viewer", KnowledgeBaseIDs: []string{base.ID}})
	require.ErrorIs(t, err, ErrForbidden)
	_, err = svc.DescribeKnowledgeContext(context.Background(), "wsp_1", "viewer", KBRetrievalContextRequest{AgentID: "agt_reader", ActingUserID: "viewer", KnowledgeBaseIDs: []string{base.ID}})
	require.ErrorIs(t, err, ErrForbidden)

	_, err = svc.UpsertKnowledgeBasePermission(context.Background(), "wsp_1", "owner", base.ID, KBPermissionGrantMutation{
		GrantID: grant.ID, Role: "edit", RoleSet: true,
	})
	require.NoError(t, err)
	maintainerContext, err := svc.DescribeKnowledgeContext(context.Background(), "wsp_1", "viewer", KBRetrievalContextRequest{ActingUserID: "viewer", KnowledgeBaseIDs: []string{base.ID}})
	require.NoError(t, err)
	require.Len(t, maintainerContext.KnowledgeBases, 1)
	maintainerAgentContext, err := svc.DescribeKnowledgeContext(context.Background(), "wsp_1", "viewer", KBRetrievalContextRequest{AgentID: "agt_reader", ActingUserID: "viewer", KnowledgeBaseIDs: []string{base.ID}})
	require.NoError(t, err)
	require.Len(t, maintainerAgentContext.KnowledgeBases, 1)

	ownerContext, err := svc.DescribeKnowledgeContext(context.Background(), "wsp_1", "owner", KBRetrievalContextRequest{AgentID: "agt_reader", ActingUserID: "owner", KnowledgeBaseIDs: []string{base.ID}})
	require.NoError(t, err)
	require.Len(t, ownerContext.KnowledgeBases, 1)
}
