package kb_test

import (
	"bytes"
	"context"
	"encoding/base64"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/AgentTeam/internal/agsbridge/wiki"
	apikb "github.com/tidbcloud/AgentTeam/internal/api/kb"
	"github.com/tidbcloud/AgentTeam/internal/identity/trusted"
	kbservice "github.com/tidbcloud/AgentTeam/internal/service/kb"
	"github.com/tidbcloud/AgentTeam/internal/service/memory"
	"github.com/tidbcloud/AgentTeam/internal/service/resource/repos"
	"github.com/tidbcloud/AgentTeam/internal/store/kb"
	"github.com/tidbcloud/AgentTeam/internal/store/resource/activity"
	reposstore "github.com/tidbcloud/AgentTeam/internal/store/resource/repos"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

func newKBTestMux(t *testing.T) http.Handler {
	mux, _ := newKBTestMuxAndServiceWithWiki(t, nil)
	return mux
}

func newKBTestMuxWithWiki(t *testing.T, wiki kbservice.KBWikiReader) http.Handler {
	mux, _ := newKBTestMuxAndServiceWithWiki(t, wiki)
	return mux
}

func newKBTestMuxAndServiceWithWiki(t *testing.T, wiki kbservice.KBWikiReader) (http.Handler, *kbservice.KBService) {
	mux, svc, _ := newKBTestMuxServiceStoreWithWiki(t, wiki)
	return mux, svc
}

func newKBTestMuxServiceStoreWithWiki(t *testing.T, wiki kbservice.KBWikiReader) (http.Handler, *kbservice.KBService, *kb.Store) {
	return newKBTestMuxServiceStoreWithOptions(t, wiki, nil)
}

func newKBTestMuxServiceStoreWithOptions(t *testing.T, wiki kbservice.KBWikiReader, configure func(*kbservice.KBServiceOptions)) (http.Handler, *kbservice.KBService, *kb.Store) {
	t.Helper()
	db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(
		&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{},
		&activity.ResourceActivityEvent{},
		&activity.ResourceActivityLatestCommit{},
		&activity.ResourceActivityProjectorCheckpoint{},
	))
	now := func() time.Time { return time.Date(2026, 6, 1, 2, 3, 4, 0, time.UTC) }
	store := kb.New(db)
	activityStore := activity.New(db)
	options := kbservice.KBServiceOptions{
		WikiReader:           wiki,
		WikiStore:            &apiFakeAGSRepositoryStore{},
		RepositoryStore:      reposstore.New(db),
		AgentOwnershipSource: apiFakeAgentOwnershipSource{agents: []memory.AgentOwnership{{ID: "agt_reader"}, {ID: "agt_other"}}},
		ActivityStore:        activityStore,
	}
	if configure != nil {
		configure(&options)
	}
	svc := kbservice.NewKBServiceWithOptions(store, now, options)
	mux := http.NewServeMux()
	mw := trusted.Middleware(trusted.Options{Enabled: true, Header: "Authorization"})
	apikb.RegisterRoutes(mux, apikb.NewKBHandler(svc), mw)
	return mux, svc, store
}

type apiFakeAgentOwnershipSource struct {
	agents []memory.AgentOwnership
	err    error
}

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

type apiFakeAGSRepositoryStore struct{}

func (apiFakeAGSRepositoryStore) ResolveRepositorySnapshot(context.Context, repos.RepositoryIdentity) (string, error) {
	return "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", nil
}

func (apiFakeAGSRepositoryStore) EnsureRepository(_ context.Context, request repos.AGSRepositoryEnsureRequest) (repos.RepositoryIdentity, error) {
	return repos.RepositoryIdentity{ID: "repo-" + request.RepoRole, Owner: request.Owner, Name: request.Name, FullName: request.Owner + "/" + request.Name, DefaultBranch: request.DefaultBranch}, nil
}

func (apiFakeAGSRepositoryStore) RepositoryExists(_ context.Context, _ repos.RepositoryIdentity) (bool, error) {
	return false, nil
}

func (apiFakeAGSRepositoryStore) GrantRepositoryCollaborator(context.Context, repos.ResourceRepositoryCollaboratorGrant) (repos.ResourceRepositoryCollaboratorGrantResult, error) {
	return repos.ResourceRepositoryCollaboratorGrantResult{Materialized: true}, nil
}

func (apiFakeAGSRepositoryStore) RevokeRepositoryCollaborator(context.Context, repos.ResourceRepositoryCollaboratorRevoke) error {
	return nil
}

func (apiFakeAGSRepositoryStore) PutPage(context.Context, wiki.KBWikiPageWrite) error {
	return nil
}

func (apiFakeAGSRepositoryStore) PrunePages(context.Context, wiki.KBWikiPrune) error {
	return nil
}

func (apiFakeAGSRepositoryStore) Reconcile(_ context.Context, repository repos.RepositoryIdentity) (wiki.KBWikiReconcileResult, error) {
	return wiki.KBWikiReconcileResult{RepositoryID: repository.ID, Reconciled: true}, nil
}

type apiRecordingKBWikiReader struct {
	searchRequests []kbservice.KBWikiSearchRequest
	readRequests   []kbservice.KBWikiReadRequest
	treeRequests   []kbservice.KBWikiTreeRequest
	searchErr      error
	readErr        error
	treeErr        error
}

func (reader *apiRecordingKBWikiReader) SearchWiki(_ context.Context, request kbservice.KBWikiSearchRequest) (kbservice.KBWikiSearchResponse, error) {
	reader.searchRequests = append(reader.searchRequests, request)
	if reader.searchErr != nil {
		return kbservice.KBWikiSearchResponse{}, reader.searchErr
	}
	return kbservice.KBWikiSearchResponse{Results: []kbservice.KBWikiSearchHit{{Slug: "wiki-mcp", Title: "Wiki MCP", Snippet: "AGS wiki", Score: 1.25}}}, nil
}

func (reader *apiRecordingKBWikiReader) ReadWikiPage(_ context.Context, request kbservice.KBWikiReadRequest) (kbservice.KBWikiPage, error) {
	reader.readRequests = append(reader.readRequests, request)
	if reader.readErr != nil {
		return kbservice.KBWikiPage{}, reader.readErr
	}
	return kbservice.KBWikiPage{Slug: request.Slug, Title: "Wiki MCP", Body: "content", SHA: "sha123"}, nil
}

func (reader *apiRecordingKBWikiReader) ListWikiTree(_ context.Context, request kbservice.KBWikiTreeRequest) ([]kbservice.KBWikiTreeEntry, error) {
	reader.treeRequests = append(reader.treeRequests, request)
	if reader.treeErr != nil {
		return nil, reader.treeErr
	}
	return []kbservice.KBWikiTreeEntry{{Path: "docs/wiki-mcp.md", Name: "wiki-mcp.md", Kind: "page", Slug: "wiki-mcp", Title: "Wiki MCP", SHA: "sha-tree", Size: 42}}, nil
}

func TestKBHandlerReturnsKnowledgeBasePermissionMetadata(t *testing.T) {
	mux, svc := newKBTestMuxAndServiceWithWiki(t, nil)
	ctx := context.Background()
	defaultRole := "view"
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", kbservice.KBMutation{Name: "Runbooks", DefaultWorkspaceMemberRole: &defaultRole})
	require.NoError(t, err)
	_, err = svc.UpsertKnowledgeBasePermission(ctx, "wsp_1", "owner", base.ID, kbservice.KBPermissionGrantMutation{SubjectType: "user", SubjectID: "manager", Role: "manage"})
	require.NoError(t, err)
	_, err = svc.UpsertKnowledgeBasePermission(ctx, "wsp_1", "owner", base.ID, kbservice.KBPermissionGrantMutation{SubjectType: "user", SubjectID: "editor", Role: "edit"})
	require.NoError(t, err)
	_, err = svc.UpsertKnowledgeBasePermission(ctx, "wsp_1", "owner", base.ID, kbservice.KBPermissionGrantMutation{SubjectType: "user", SubjectID: "viewer", Role: "view"})
	require.NoError(t, err)

	detailReq := httptest.NewRequest(http.MethodGet, "/api/v1/kb/"+base.ID, nil)
	detailReq.Header.Set("Authorization", "Bearer "+jwt(map[string]any{"sub": "manager"}))
	detailReq.Header.Set("X-Workspace-Id", "wsp_1")
	detailRec := httptest.NewRecorder()
	mux.ServeHTTP(detailRec, detailReq)
	require.Equal(t, http.StatusOK, detailRec.Code)
	var detail struct {
		Role      string `json:"role"`
		CanManage bool   `json:"canManage"`
	}
	require.NoError(t, json.Unmarshal(detailRec.Body.Bytes(), &detail))
	require.Equal(t, "manage", detail.Role)
	require.True(t, detail.CanManage)

	listReq := httptest.NewRequest(http.MethodGet, "/api/v1/kb", nil)
	listReq.Header.Set("Authorization", "Bearer "+jwt(map[string]any{"sub": "editor"}))
	listReq.Header.Set("X-Workspace-Id", "wsp_1")
	listRec := httptest.NewRecorder()
	mux.ServeHTTP(listRec, listReq)
	require.Equal(t, http.StatusOK, listRec.Code)
	var list struct {
		Items []struct {
			ID        string `json:"id"`
			Role      string `json:"role"`
			CanManage bool   `json:"canManage"`
		} `json:"items"`
	}
	require.NoError(t, json.Unmarshal(listRec.Body.Bytes(), &list))
	require.Len(t, list.Items, 1)
	require.Equal(t, base.ID, list.Items[0].ID)
	require.Equal(t, "edit", list.Items[0].Role)
	require.False(t, list.Items[0].CanManage)

	viewerReq := httptest.NewRequest(http.MethodGet, "/api/v1/kb/"+base.ID, nil)
	viewerReq.Header.Set("Authorization", "Bearer "+jwt(map[string]any{"sub": "viewer"}))
	viewerReq.Header.Set("X-Workspace-Id", "wsp_1")
	viewerRec := httptest.NewRecorder()
	mux.ServeHTTP(viewerRec, viewerReq)
	require.Equal(t, http.StatusOK, viewerRec.Code)
	var viewerDetail struct {
		Role      string `json:"role"`
		CanManage bool   `json:"canManage"`
	}
	require.NoError(t, json.Unmarshal(viewerRec.Body.Bytes(), &viewerDetail))
	require.Equal(t, "view", viewerDetail.Role)
	require.False(t, viewerDetail.CanManage)

	memberListReq := httptest.NewRequest(http.MethodGet, "/api/v1/kb", nil)
	memberListReq.Header.Set("Authorization", "Bearer "+jwt(map[string]any{"sub": "member"}))
	memberListReq.Header.Set("X-Workspace-Id", "wsp_1")
	memberListRec := httptest.NewRecorder()
	mux.ServeHTTP(memberListRec, memberListReq)
	require.Equal(t, http.StatusOK, memberListRec.Code)
	var memberList struct {
		Items []struct {
			ID        string `json:"id"`
			Role      string `json:"role"`
			CanManage bool   `json:"canManage"`
		} `json:"items"`
	}
	require.NoError(t, json.Unmarshal(memberListRec.Body.Bytes(), &memberList))
	require.Len(t, memberList.Items, 1)
	require.Equal(t, base.ID, memberList.Items[0].ID)
	require.Equal(t, "view", memberList.Items[0].Role)
	require.False(t, memberList.Items[0].CanManage)

	memberDetailReq := httptest.NewRequest(http.MethodGet, "/api/v1/kb/"+base.ID, nil)
	memberDetailReq.Header.Set("Authorization", "Bearer "+jwt(map[string]any{"sub": "member"}))
	memberDetailReq.Header.Set("X-Workspace-Id", "wsp_1")
	memberDetailRec := httptest.NewRecorder()
	mux.ServeHTTP(memberDetailRec, memberDetailReq)
	require.Equal(t, http.StatusOK, memberDetailRec.Code)
	var memberDetail struct {
		Role      string `json:"role"`
		CanManage bool   `json:"canManage"`
	}
	require.NoError(t, json.Unmarshal(memberDetailRec.Body.Bytes(), &memberDetail))
	require.Equal(t, "view", memberDetail.Role)
	require.False(t, memberDetail.CanManage)
}

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

	patch := httptest.NewRequest(http.MethodPatch, "/api/v1/kb/"+base.ID+"/permissions/grants/"+grant.ID+"/repository-overrides", bytes.NewBufferString(`{
		"items":[{"sourceId":"`+source.ID+`","repoRole":"canonical","repositoryAccessProfile":"manage"}]
	}`))
	patch.Header.Set("Authorization", "Bearer "+jwt(map[string]any{"sub": "owner"}))
	patch.Header.Set("X-Workspace-Id", "wsp_1")
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, patch)
	require.Equal(t, http.StatusOK, rec.Code)

	var body struct {
		RepositoryAccessProfile string `json:"repositoryAccessProfile"`
		RepositoryAccessStatus  string `json:"repositoryAccessStatus"`
		SourceRepositoryAccess  []struct {
			SourceID                         string `json:"sourceId"`
			RepoRole                         string `json:"repoRole"`
			InheritedRepositoryAccessProfile string `json:"inheritedRepositoryAccessProfile"`
			OverrideRepositoryAccessProfile  string `json:"overrideRepositoryAccessProfile"`
			EffectiveRepositoryAccessProfile string `json:"effectiveRepositoryAccessProfile"`
			RepositoryAccessStatus           string `json:"repositoryAccessStatus"`
		} `json:"sourceRepositoryAccess"`
	}
	require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &body))
	require.Equal(t, "read", body.RepositoryAccessProfile)
	require.Equal(t, "current", body.RepositoryAccessStatus)
	require.Len(t, body.SourceRepositoryAccess, 2)
	accessByRole := make(map[string]struct {
		SourceID                         string `json:"sourceId"`
		RepoRole                         string `json:"repoRole"`
		InheritedRepositoryAccessProfile string `json:"inheritedRepositoryAccessProfile"`
		OverrideRepositoryAccessProfile  string `json:"overrideRepositoryAccessProfile"`
		EffectiveRepositoryAccessProfile string `json:"effectiveRepositoryAccessProfile"`
		RepositoryAccessStatus           string `json:"repositoryAccessStatus"`
	}, len(body.SourceRepositoryAccess))
	for _, row := range body.SourceRepositoryAccess {
		accessByRole[row.RepoRole] = row
		require.Equal(t, source.ID, row.SourceID)
		require.Equal(t, "read", row.InheritedRepositoryAccessProfile)
		require.Equal(t, "current", row.RepositoryAccessStatus)
	}
	require.Equal(t, "manage", accessByRole["canonical"].OverrideRepositoryAccessProfile)
	require.Equal(t, "manage", accessByRole["canonical"].EffectiveRepositoryAccessProfile)
	require.Empty(t, accessByRole["insights"].OverrideRepositoryAccessProfile)
	require.Equal(t, "read", accessByRole["insights"].EffectiveRepositoryAccessProfile)
}

func TestKBHandlerAllowsOnlyManagersToUpdateKnowledgeQASourceAccessPolicy(t *testing.T) {
	mux, svc := newKBTestMuxAndServiceWithWiki(t, nil)
	base, err := svc.CreateKnowledgeBase(context.Background(), "wsp_1", "owner", kbservice.KBMutation{Name: "Runbooks"})
	require.NoError(t, err)
	_, err = svc.UpsertKnowledgeBasePermission(context.Background(), "wsp_1", "owner", base.ID, kbservice.KBPermissionGrantMutation{
		SubjectType: "user", SubjectID: "viewer", Role: "view", RoleSet: true,
	})
	require.NoError(t, err)

	update := httptest.NewRequest(http.MethodPatch, "/api/v1/kb/"+base.ID, bytes.NewBufferString(`{
		"name":"Runbooks",
		"description":"",
		"allowKnowledgeQAWithoutSourceAccess":true
	}`))
	update.Header.Set("Authorization", "Bearer "+jwt(map[string]any{"sub": "owner"}))
	update.Header.Set("X-Workspace-Id", "wsp_1")
	update.Header.Set("Content-Type", "application/json")
	updateRec := httptest.NewRecorder()
	mux.ServeHTTP(updateRec, update)
	require.Equal(t, http.StatusOK, updateRec.Code)
	var updated struct {
		AllowKnowledgeQAWithoutSourceAccess bool `json:"allowKnowledgeQAWithoutSourceAccess"`
	}
	require.NoError(t, json.Unmarshal(updateRec.Body.Bytes(), &updated))
	require.True(t, updated.AllowKnowledgeQAWithoutSourceAccess)

	denied := httptest.NewRequest(http.MethodPatch, "/api/v1/kb/"+base.ID, bytes.NewBufferString(`{
		"name":"Runbooks",
		"description":"",
		"allowKnowledgeQAWithoutSourceAccess":false
	}`))
	denied.Header.Set("Authorization", "Bearer "+jwt(map[string]any{"sub": "viewer"}))
	denied.Header.Set("X-Workspace-Id", "wsp_1")
	denied.Header.Set("Content-Type", "application/json")
	deniedRec := httptest.NewRecorder()
	mux.ServeHTTP(deniedRec, denied)
	require.Equal(t, http.StatusForbidden, deniedRec.Code)
}

func TestKBHandlerCreatesUpdatesAndQueuesSourceMirrorWithoutCredentialLeak(t *testing.T) {
	mux, svc, store := newKBTestMuxServiceStoreWithWiki(t, nil)
	authHeader := "Bearer " + jwt(map[string]any{"sub": "owner"})

	createKB := httptest.NewRequest(http.MethodPost, "/api/v1/kb", bytes.NewBufferString(`{"name":"Runbooks"}`))
	createKB.Header.Set("Authorization", authHeader)
	createKB.Header.Set("X-Workspace-Id", "wsp_1")
	createKBRec := httptest.NewRecorder()
	mux.ServeHTTP(createKBRec, createKB)
	require.Equal(t, http.StatusCreated, createKBRec.Code)
	var base struct {
		ID string `json:"id"`
	}
	require.NoError(t, json.Unmarshal(createKBRec.Body.Bytes(), &base))

	createSource := httptest.NewRequest(http.MethodPost, "/api/v1/kb/"+base.ID+"/sources", bytes.NewBufferString(`{
		"sourceType":"codebase",
		"name":"External agentteam",
		"repoMode":"mirror",
		"defaultRef":"main",
		"mirror":{
			"upstreamType":"github",
			"remoteUrl":"https://github.com/pingcap/agentteam.git",
			"credentialRef":"automation-secret://GITHUB_TOKEN",
			"defaultRef":"main",
			"intervalSeconds":300,
			"enabled":true
		}
	}`))
	createSource.Header.Set("Authorization", authHeader)
	createSource.Header.Set("X-Workspace-Id", "wsp_1")
	createSourceRec := httptest.NewRecorder()
	mux.ServeHTTP(createSourceRec, createSource)
	require.Equal(t, http.StatusCreated, createSourceRec.Code)
	require.NotContains(t, createSourceRec.Body.String(), "secret://")
	var source struct {
		ID     string `json:"id"`
		Mirror struct {
			RemoteURL       string `json:"remoteUrl"`
			UpstreamType    string `json:"upstreamType"`
			IntervalSeconds int    `json:"intervalSeconds"`
			SyncStatus      string `json:"syncStatus"`
		} `json:"mirror"`
	}
	require.NoError(t, json.Unmarshal(createSourceRec.Body.Bytes(), &source))
	require.NotEmpty(t, source.ID)
	require.Equal(t, "https://github.com/pingcap/agentteam.git", source.Mirror.RemoteURL)
	require.Equal(t, "github", source.Mirror.UpstreamType)
	require.Equal(t, 300, source.Mirror.IntervalSeconds)
	require.Equal(t, kb.SourceMirrorSyncStatusIdle, source.Mirror.SyncStatus)

	loadedMirror, err := svc.ListSourceMirrorSyncs(context.Background(), "wsp_1", "owner", base.ID, source.ID, 1, 20)
	require.NoError(t, err)
	require.Equal(t, "automation-secret://GITHUB_TOKEN", loadedMirror.Mirror.CredentialRef)

	updateMirror := httptest.NewRequest(http.MethodPatch, "/api/v1/kb/"+base.ID+"/sources/"+source.ID+"/mirror", bytes.NewBufferString(`{
		"upstreamType":"gitlab",
		"remoteUrl":"https://93.184.216.34/group/project.git",
		"defaultRef":"trunk",
		"intervalSeconds":0,
		"enabled":false
	}`))
	updateMirror.Header.Set("Authorization", authHeader)
	updateMirror.Header.Set("X-Workspace-Id", "wsp_1")
	updateMirrorRec := httptest.NewRecorder()
	mux.ServeHTTP(updateMirrorRec, updateMirror)
	require.Equal(t, http.StatusOK, updateMirrorRec.Code)
	require.NotContains(t, updateMirrorRec.Body.String(), "secret://")
	require.Contains(t, updateMirrorRec.Body.String(), `"upstreamType":"gitlab"`)
	require.Contains(t, updateMirrorRec.Body.String(), `"enabled":false`)

	reenable, err := svc.UpdateSourceMirror(context.Background(), "wsp_1", "owner", base.ID, source.ID, kbservice.KBSourceMirrorMutation{
		UpstreamType:    "gitlab",
		RemoteURL:       "https://93.184.216.34/group/project.git",
		DefaultRef:      "trunk",
		IntervalSeconds: ptrTo(300),
		Enabled:         boolPtr(true),
	})
	require.NoError(t, err)
	require.True(t, reenable.Enabled)
	require.Equal(t, 300, reenable.IntervalSeconds)
	require.NotNil(t, reenable.NextSyncAt)

	patchURLOnly := httptest.NewRequest(http.MethodPatch, "/api/v1/kb/"+base.ID+"/sources/"+source.ID+"/mirror", bytes.NewBufferString(`{
		"remoteUrl":"https://93.184.216.34/group/project-renamed.git"
	}`))
	patchURLOnly.Header.Set("Authorization", authHeader)
	patchURLOnly.Header.Set("X-Workspace-Id", "wsp_1")
	patchURLOnlyRec := httptest.NewRecorder()
	mux.ServeHTTP(patchURLOnlyRec, patchURLOnly)
	require.Equal(t, http.StatusOK, patchURLOnlyRec.Code)
	var patchedMirror struct {
		RemoteURL       string `json:"remoteUrl"`
		IntervalSeconds int    `json:"intervalSeconds"`
		NextSyncAt      string `json:"nextSyncAt"`
	}
	require.NoError(t, json.Unmarshal(patchURLOnlyRec.Body.Bytes(), &patchedMirror))
	require.Equal(t, "https://93.184.216.34/group/project-renamed.git", patchedMirror.RemoteURL)
	require.Equal(t, 300, patchedMirror.IntervalSeconds)
	require.NotEmpty(t, patchedMirror.NextSyncAt)
	loadedAfterPatch, err := svc.ListSourceMirrorSyncs(context.Background(), "wsp_1", "owner", base.ID, source.ID, 1, 20)
	require.NoError(t, err)
	require.Equal(t, 300, loadedAfterPatch.Mirror.IntervalSeconds)
	require.NotNil(t, loadedAfterPatch.Mirror.NextSyncAt)
	mirrorWithError := loadedAfterPatch.Mirror
	mirrorWithError.LastError = "mirror failed Authorization: Bearer mirror-secret credentialRef: secret://kb/source-token"
	_, err = store.UpdateSourceMirror(context.Background(), mirrorWithError)
	require.NoError(t, err)
	sourceDetail := httptest.NewRequest(http.MethodGet, "/api/v1/kb/"+base.ID+"/sources/"+source.ID+"/detail", nil)
	sourceDetail.Header.Set("Authorization", authHeader)
	sourceDetail.Header.Set("X-Workspace-Id", "wsp_1")
	sourceDetailRec := httptest.NewRecorder()
	mux.ServeHTTP(sourceDetailRec, sourceDetail)
	require.Equal(t, http.StatusOK, sourceDetailRec.Code)
	require.Contains(t, sourceDetailRec.Body.String(), `"lastError":"mirror failed Authorization: Bearer [redacted] credentialRef: [redacted]"`)
	require.NotContains(t, sourceDetailRec.Body.String(), "mirror-secret")
	require.NotContains(t, sourceDetailRec.Body.String(), "secret://")

	syncReq := httptest.NewRequest(http.MethodPost, "/api/v1/kb/"+base.ID+"/sources/"+source.ID+"/mirror-syncs", bytes.NewBufferString(`{"ref":"trunk"}`))
	syncReq.Header.Set("Authorization", authHeader)
	syncReq.Header.Set("X-Workspace-Id", "wsp_1")
	syncRec := httptest.NewRecorder()
	mux.ServeHTTP(syncRec, syncReq)
	require.Equal(t, http.StatusAccepted, syncRec.Code)
	require.NotContains(t, syncRec.Body.String(), "secret://")
	require.Contains(t, syncRec.Body.String(), `"status":"queued"`)
	require.Contains(t, syncRec.Body.String(), `"stage":"queued"`)
	require.Contains(t, syncRec.Body.String(), `"ref":"trunk"`)
	require.Contains(t, syncRec.Body.String(), `"trigger":"manual"`)
	require.Contains(t, syncRec.Body.String(), `"requestedBy":"owner"`)
	require.Contains(t, syncRec.Body.String(), `"traceId":"trace_`)

	listReq := httptest.NewRequest(http.MethodGet, "/api/v1/kb/"+base.ID+"/sources/"+source.ID+"/mirror-syncs", nil)
	listReq.Header.Set("Authorization", authHeader)
	listReq.Header.Set("X-Workspace-Id", "wsp_1")
	listRec := httptest.NewRecorder()
	mux.ServeHTTP(listRec, listReq)
	require.Equal(t, http.StatusOK, listRec.Code)
	require.NotContains(t, listRec.Body.String(), "secret://")
	require.Contains(t, listRec.Body.String(), `"items":[`)
	require.Contains(t, listRec.Body.String(), `"trigger":"manual"`)
}

func boolPtr(value bool) *bool { return &value }

func TestKBHandlerRunsRetrievalSearchReadAndTree(t *testing.T) {
	wiki := &apiRecordingKBWikiReader{}
	mux, svc, store := newKBTestMuxServiceStoreWithWiki(t, wiki)
	authHeader := "Bearer " + jwt(map[string]any{"sub": "user-1"})
	createKB := httptest.NewRequest(http.MethodPost, "/api/v1/kb", bytes.NewBufferString(`{"name":"Runbooks"}`))
	createKB.Header.Set("Authorization", authHeader)
	createKB.Header.Set("X-Workspace-Id", "wsp_1")
	createKBRec := httptest.NewRecorder()
	mux.ServeHTTP(createKBRec, createKB)
	require.Equal(t, http.StatusCreated, createKBRec.Code)
	var created struct {
		ID string `json:"id"`
	}
	require.NoError(t, json.Unmarshal(createKBRec.Body.Bytes(), &created))
	_, err := svc.CreateSource(context.Background(), "wsp_1", "user-1", created.ID, kbservice.KBSourceMutation{SourceType: "codebase", Name: "AgentTeam", IndexStatus: "ready"})
	require.NoError(t, err)

	_, err = svc.GrantAgentKnowledgeBaseAccess(context.Background(), "wsp_1", "user-1", "agt_reader", created.ID, "read")
	require.NoError(t, err)

	contextReq := httptest.NewRequest(http.MethodPost, "/api/v1/kb/retrieval/context", bytes.NewBufferString(`{"agentId":"agt_reader","kbIds":["`+created.ID+`"]}`))
	contextReq.Header.Set("Authorization", authHeader)
	contextReq.Header.Set("X-Workspace-Id", "wsp_1")
	contextRec := httptest.NewRecorder()
	mux.ServeHTTP(contextRec, contextReq)
	require.Equal(t, http.StatusOK, contextRec.Code)
	require.Contains(t, contextRec.Body.String(), `"knowledgeBases":[`)
	require.Contains(t, contextRec.Body.String(), `"name":"Runbooks"`)
	require.Contains(t, contextRec.Body.String(), `"repoFullName":"user-1/agentteam.kb"`)
	var contextResponse struct {
		KnowledgeBases []struct {
			Sources []struct {
				SourceID string `json:"sourceId"`
			} `json:"sources"`
		} `json:"knowledgeBases"`
	}
	require.NoError(t, json.Unmarshal(contextRec.Body.Bytes(), &contextResponse))
	require.Len(t, contextResponse.KnowledgeBases, 1)
	require.NotEmpty(t, contextResponse.KnowledgeBases[0].Sources)
	sourceID := contextResponse.KnowledgeBases[0].Sources[0].SourceID
	started := time.Date(2026, 6, 1, 3, 0, 0, 0, time.UTC)
	_, err = store.CreateSourceMirror(context.Background(), kb.SourceMirror{SourceID: sourceID, WorkspaceID: "wsp_1", KnowledgeBaseID: created.ID, UpstreamType: kb.SourceMirrorUpstreamTypeGenericGit, RemoteURL: "https://93.184.216.34/org/repo.git", DefaultRef: "main", IntervalSeconds: 60, Enabled: true, SyncStatus: kb.SourceMirrorSyncStatusIdle, CreatedBy: "user-1", UpdatedBy: "user-1", CreatedAt: started, UpdatedAt: started})
	require.NoError(t, err)
	_, err = store.CreateJob(context.Background(), kb.Job{ID: "job_mirror", WorkspaceID: "wsp_1", KnowledgeBaseID: created.ID, SourceID: sourceID, JobType: kbservice.KBJobTypeSyncSourceMirror, Status: kbservice.KBJobStatusQueued, Progress: "{}", CreatedAt: started, UpdatedAt: started})
	require.NoError(t, err)
	var mirrorJob kb.Job
	require.NoError(t, store.DB().Where("workspace_id = ? AND knowledge_base_id = ? AND source_id = ? AND job_type = ?", "wsp_1", created.ID, sourceID, kbservice.KBJobTypeSyncSourceMirror).First(&mirrorJob).Error)
	mirrorJob.Status = kbservice.KBJobStatusRunning
	mirrorJob.StartedAt = &started
	mirrorJob.Progress = `{"stage":"syncing_ags_repo","message":"reading /tmp/agentteam/private token=secret"}`
	mirrorJob.UpdatedAt = started.Add(time.Minute)
	_, err = store.UpdateJob(context.Background(), mirrorJob)
	require.NoError(t, err)
	source, err := store.GetSource(context.Background(), "wsp_1", created.ID, sourceID)
	require.NoError(t, err)
	source.Status = kbservice.KBSourceStatusReady
	source.SyncStatus = kbservice.KBSourceStatusReady
	source.IndexStatus = kbservice.KBSourceStatusReady
	source.AnalysisStatus = "succeeded"
	source.LastError = "source failed Authorization: Bearer source-secret credentialRef: secret://kb/source-token access_token: source-ghp_secret"
	source.UpdatedAt = started.Add(time.Minute)
	_, err = store.UpdateSource(context.Background(), source)
	require.NoError(t, err)

	overview := httptest.NewRequest(http.MethodGet, "/api/v1/kb/"+created.ID+"/overview", nil)
	overview.Header.Set("Authorization", authHeader)
	overview.Header.Set("X-Workspace-Id", "wsp_1")
	overviewRec := httptest.NewRecorder()
	mux.ServeHTTP(overviewRec, overview)
	require.Equal(t, http.StatusOK, overviewRec.Code)
	require.Contains(t, overviewRec.Body.String(), `"health"`)
	require.Contains(t, overviewRec.Body.String(), `"recommendedPaths"`)
	require.Contains(t, overviewRec.Body.String(), `"importProgress"`)

	sourceDetail := httptest.NewRequest(http.MethodGet, "/api/v1/kb/"+created.ID+"/sources/"+sourceID+"/detail", nil)
	sourceDetail.Header.Set("Authorization", authHeader)
	sourceDetail.Header.Set("X-Workspace-Id", "wsp_1")
	sourceDetailRec := httptest.NewRecorder()
	mux.ServeHTTP(sourceDetailRec, sourceDetail)
	require.Equal(t, http.StatusOK, sourceDetailRec.Code)
	require.Contains(t, sourceDetailRec.Body.String(), `"source"`)
	require.Contains(t, sourceDetailRec.Body.String(), `"repositories"`)
	require.Contains(t, sourceDetailRec.Body.String(), `"analysisRuns"`)
	require.Contains(t, sourceDetailRec.Body.String(), `"importProgress":{"state":"running","severity":"info","phase":"mirror_sync","stage":"syncing_ags_repo"`)
	require.Contains(t, sourceDetailRec.Body.String(), `"message":"reading [local path] token=[redacted]"`)
	require.Contains(t, sourceDetailRec.Body.String(), `"lastError":"source failed Authorization: Bearer [redacted] credentialRef: [redacted] access_token: [redacted]"`)
	require.NotContains(t, sourceDetailRec.Body.String(), "secret")

	search := httptest.NewRequest(http.MethodPost, "/api/v1/kb/retrieval/search", bytes.NewBufferString(`{"agentId":"agt_reader","kbIds":["`+created.ID+`"],"query":"mcp","limit":5}`))
	search.Header.Set("Authorization", authHeader)
	search.Header.Set("X-Workspace-Id", "wsp_1")
	searchRec := httptest.NewRecorder()
	mux.ServeHTTP(searchRec, search)
	require.Equal(t, http.StatusOK, searchRec.Code)
	require.Contains(t, searchRec.Body.String(), `"title":"Wiki MCP"`)
	require.Contains(t, searchRec.Body.String(), `"citations"`)
	require.Contains(t, searchRec.Body.String(), `"citation_id":"cit_`)
	require.NotContains(t, searchRec.Body.String(), "citationUri")
	require.NotContains(t, searchRec.Body.String(), "sourceId")
	require.Len(t, wiki.searchRequests, 1)
	require.Equal(t, authHeader, wiki.searchRequests[0].AuthorizationHeader)

	read := httptest.NewRequest(http.MethodPost, "/api/v1/kb/retrieval/read", bytes.NewBufferString(`{"agentId":"agt_reader","kbIds":["`+created.ID+`"],"sourceId":"`+sourceID+`","slug":"wiki-mcp"}`))
	read.Header.Set("Authorization", authHeader)
	read.Header.Set("X-Workspace-Id", "wsp_1")
	readRec := httptest.NewRecorder()
	mux.ServeHTTP(readRec, read)
	require.Equal(t, http.StatusOK, readRec.Code)
	require.Contains(t, readRec.Body.String(), `"body":"content"`)
	require.Contains(t, readRec.Body.String(), `rev=sha123`)

	tree := httptest.NewRequest(http.MethodPost, "/api/v1/kb/retrieval/tree", bytes.NewBufferString(`{"agentId":"agt_reader","kbIds":["`+created.ID+`"],"sourceId":"`+sourceID+`","path":"docs"}`))
	tree.Header.Set("Authorization", authHeader)
	tree.Header.Set("X-Workspace-Id", "wsp_1")
	treeRec := httptest.NewRecorder()
	mux.ServeHTTP(treeRec, tree)
	require.Equal(t, http.StatusOK, treeRec.Code)
	require.Contains(t, treeRec.Body.String(), `"path":"docs/wiki-mcp.md"`)
	require.Len(t, wiki.treeRequests, 1)
	require.Equal(t, "docs", wiki.treeRequests[0].Path)
}

func TestKBHandlerRunsRetrievalEfficiencyOperations(t *testing.T) {
	wiki := &apiRecordingKBWikiReader{}
	mux, svc := newKBTestMuxAndServiceWithWiki(t, wiki)
	authHeader := "Bearer " + jwt(map[string]any{"sub": "user-1"})

	created := createKBKnowledgeKB(t, svc, "wsp_1", "user-1", "Runbooks")
	_, err := svc.GrantAgentKnowledgeBaseAccess(context.Background(), "wsp_1", "user-1", "agt_reader", created.ID, "read")
	require.NoError(t, err)
	contextResult, err := svc.DescribeKnowledgeContext(context.Background(), "wsp_1", "user-1", kbservice.KBRetrievalContextRequest{AgentID: "agt_reader", KnowledgeBaseIDs: []string{created.ID}})
	require.NoError(t, err)
	require.Len(t, contextResult.KnowledgeBases, 1)
	require.NotEmpty(t, contextResult.KnowledgeBases[0].Sources)
	sourceID := contextResult.KnowledgeBases[0].Sources[0].SourceID

	post := func(path, body string) *httptest.ResponseRecorder {
		req := httptest.NewRequest(http.MethodPost, path, bytes.NewBufferString(body))
		req.Header.Set("Authorization", authHeader)
		req.Header.Set("X-Workspace-Id", "wsp_1")
		rec := httptest.NewRecorder()
		mux.ServeHTTP(rec, req)
		return rec
	}

	capabilities := post("/api/v1/kb/retrieval/capabilities", `{"agentId":"agt_reader","kbIds":["`+created.ID+`"]}`)
	require.Equal(t, http.StatusOK, capabilities.Code)
	require.Contains(t, capabilities.Body.String(), `"operations"`)
	require.Contains(t, capabilities.Body.String(), `"backendWritable":true`)

	manifest := post("/api/v1/kb/retrieval/manifest", `{"agentId":"agt_reader","kbIds":["`+created.ID+`"],"sourceIds":["`+sourceID+`"],"limit":5}`)
	require.Equal(t, http.StatusOK, manifest.Code)
	require.Contains(t, manifest.Body.String(), `"totalPages":1`)
	require.Contains(t, manifest.Body.String(), `"slug":"wiki-mcp"`)

	stat := post("/api/v1/kb/retrieval/stat", `{"agentId":"agt_reader","kbIds":["`+created.ID+`"],"sourceId":"`+sourceID+`","wikiSlug":"wiki-mcp"}`)
	require.Equal(t, http.StatusOK, stat.Code)
	require.Contains(t, stat.Body.String(), `"exists":true`)

	batchRead := post("/api/v1/kb/retrieval/batch-read", `{"agentId":"agt_reader","kbIds":["`+created.ID+`"],"sourceIds":["`+sourceID+`"],"items":[{"wikiSlug":"wiki-mcp","headingsOnly":true}]}`)
	require.Equal(t, http.StatusOK, batchRead.Code)
	require.Contains(t, batchRead.Body.String(), `"results"`)
	require.Contains(t, batchRead.Body.String(), `"page"`)

	batchTree := post("/api/v1/kb/retrieval/batch-tree", `{"agentId":"agt_reader","kbIds":["`+created.ID+`"],"items":[{"sourceId":"`+sourceID+`","limit":1}]}`)
	require.Equal(t, http.StatusOK, batchTree.Code)
	require.Contains(t, batchTree.Body.String(), `"tree"`)
}

func TestKBHandlerKBKnowledgeUsesAgentTeamAttestation(t *testing.T) {
	wiki := &apiRecordingKBWikiReader{}
	mux, svc := newKBTestMuxAndServiceWithWiki(t, wiki)
	authHeader := "Bearer " + jwt(map[string]any{"sub": "user-1"})

	createKB := httptest.NewRequest(http.MethodPost, "/api/v1/kb", bytes.NewBufferString(`{"name":"Runbooks"}`))
	createKB.Header.Set("Authorization", authHeader)
	createKB.Header.Set("X-Workspace-Id", "wsp_1")
	createKBRec := httptest.NewRecorder()
	mux.ServeHTTP(createKBRec, createKB)
	require.Equal(t, http.StatusCreated, createKBRec.Code)
	var created struct {
		ID string `json:"id"`
	}
	require.NoError(t, json.Unmarshal(createKBRec.Body.Bytes(), &created))
	_, err := svc.CreateSource(context.Background(), "wsp_1", "user-1", created.ID, kbservice.KBSourceMutation{SourceType: "codebase", Name: "AgentTeam", IndexStatus: "ready"})
	require.NoError(t, err)

	_, err = svc.GrantAgentKnowledgeBaseAccess(context.Background(), "wsp_1", "user-1", "agt_reader", created.ID, "read")
	require.NoError(t, err)
	attestation := base64.RawURLEncoding.EncodeToString([]byte(`{"workspaceId":"wsp_1","threadChannelId":"thr_kb","agentId":"agt_reader"}`))

	search := httptest.NewRequest(http.MethodPost, "/api/v1/kb/retrieval/knowledge/search", bytes.NewBufferString(`{"threadChannelId":"thr_kb","query":"mcp","limit":5,"kbIds":["`+created.ID+`"]}`))
	search.Header.Set("Authorization", authHeader)
	search.Header.Set("X-Workspace-Id", "wsp_1")
	search.Header.Set("X-AgentTeam-Agent-Thread-Attestation", attestation)
	searchRec := httptest.NewRecorder()
	mux.ServeHTTP(searchRec, search)

	require.Equal(t, http.StatusOK, searchRec.Code)
	require.Contains(t, searchRec.Body.String(), `"title":"Wiki MCP"`)
	require.Len(t, wiki.searchRequests, 1)
	require.Contains(t, searchRec.Body.String(), `"citations"`)
	require.Contains(t, searchRec.Body.String(), `"citation_id":"cit_`)
	require.NotContains(t, searchRec.Body.String(), "citationUri")
	require.NotContains(t, searchRec.Body.String(), "sourceId")
}

func TestKBHandlerKBKnowledgeSupportsEfficiencyOperations(t *testing.T) {
	wiki := &apiRecordingKBWikiReader{}
	mux, svc := newKBTestMuxAndServiceWithWiki(t, wiki)
	authHeader := "Bearer " + jwt(map[string]any{"sub": "user-1"})

	created := createKBKnowledgeKB(t, svc, "wsp_1", "user-1", "Runbooks")
	_, err := svc.GrantAgentKnowledgeBaseAccess(context.Background(), "wsp_1", "user-1", "agt_reader", created.ID, "read")
	require.NoError(t, err)
	contextResult, err := svc.DescribeKnowledgeContext(context.Background(), "wsp_1", "user-1", kbservice.KBRetrievalContextRequest{AgentID: "agt_reader", KnowledgeBaseIDs: []string{created.ID}})
	require.NoError(t, err)
	require.Len(t, contextResult.KnowledgeBases, 1)
	require.NotEmpty(t, contextResult.KnowledgeBases[0].Sources)
	sourceID := contextResult.KnowledgeBases[0].Sources[0].SourceID
	attestation := encodeAgentThreadAttestation(t, map[string]any{"workspaceId": "wsp_1", "threadChannelId": "thr_kb", "agentId": "agt_reader"})

	post := func(operation, body string) *httptest.ResponseRecorder {
		req := httptest.NewRequest(http.MethodPost, "/api/v1/kb/retrieval/knowledge/"+operation, bytes.NewBufferString(body))
		req.Header.Set("Authorization", authHeader)
		req.Header.Set("X-Workspace-Id", "wsp_1")
		req.Header.Set("X-AgentTeam-Agent-Thread-Attestation", attestation)
		rec := httptest.NewRecorder()
		mux.ServeHTTP(rec, req)
		return rec
	}

	capabilities := post("capabilities", `{"threadChannelId":"thr_kb","kbIds":["`+created.ID+`"]}`)
	require.Equal(t, http.StatusOK, capabilities.Code)
	require.Contains(t, capabilities.Body.String(), `"operations"`)
	require.Contains(t, capabilities.Body.String(), `"create":{"supported":false`)
	require.Contains(t, capabilities.Body.String(), `"backendWritable":true`)
	require.Contains(t, capabilities.Body.String(), `"mcpToolExposed":false`)

	manifest := post("manifest", `{"threadChannelId":"thr_kb","kbIds":["`+created.ID+`"],"sourceIds":["`+sourceID+`"],"limit":5}`)
	require.Equal(t, http.StatusOK, manifest.Code)
	require.Contains(t, manifest.Body.String(), `"totalPages":1`)
	require.Contains(t, manifest.Body.String(), `"slug":"wiki-mcp"`)

	read := post("read", `{"threadChannelId":"thr_kb","kbIds":["`+created.ID+`"],"sourceId":"`+sourceID+`","wikiSlug":"wiki-mcp","format":"plain","maxBytes":3}`)
	require.Equal(t, http.StatusOK, read.Code)
	require.Contains(t, read.Body.String(), `"body":"con"`)
	require.Contains(t, read.Body.String(), `"truncated":true`)

	tree := post("tree", `{"threadChannelId":"thr_kb","kbIds":["`+created.ID+`"],"sourceId":"`+sourceID+`","limit":1,"depth":1}`)
	require.Equal(t, http.StatusOK, tree.Code)
	require.Contains(t, tree.Body.String(), `"total":1`)

	stat := post("stat", `{"threadChannelId":"thr_kb","kbIds":["`+created.ID+`"],"sourceId":"`+sourceID+`","wikiSlug":"wiki-mcp"}`)
	require.Equal(t, http.StatusOK, stat.Code)
	require.Contains(t, stat.Body.String(), `"exists":true`)

	batchRead := post("batch_read", `{"threadChannelId":"thr_kb","kbIds":["`+created.ID+`"],"sourceIds":["`+sourceID+`"],"items":[{"wikiSlug":"wiki-mcp","headingsOnly":true}]}`)
	require.Equal(t, http.StatusOK, batchRead.Code)
	require.Contains(t, batchRead.Body.String(), `"results"`)
	require.Contains(t, batchRead.Body.String(), `"page"`)

	batchTree := post("batch_tree", `{"threadChannelId":"thr_kb","kbIds":["`+created.ID+`"],"items":[{"sourceId":"`+sourceID+`","limit":1}]}`)
	require.Equal(t, http.StatusOK, batchTree.Code)
	require.Contains(t, batchTree.Body.String(), `"tree"`)
}

func TestKBHandlerKBKnowledgeSingleOperationsReturnStructuredErrors(t *testing.T) {
	wiki := &apiRecordingKBWikiReader{searchErr: kbservice.ErrNotFound, readErr: kbservice.ErrNotFound, treeErr: kbservice.ErrNotFound}
	mux, svc := newKBTestMuxAndServiceWithWiki(t, wiki)
	authHeader := "Bearer " + jwt(map[string]any{"sub": "user-1"})

	created := createKBKnowledgeKB(t, svc, "wsp_1", "user-1", "Runbooks")
	_, err := svc.GrantAgentKnowledgeBaseAccess(context.Background(), "wsp_1", "user-1", "agt_reader", created.ID, "read")
	require.NoError(t, err)
	contextResult, err := svc.DescribeKnowledgeContext(context.Background(), "wsp_1", "user-1", kbservice.KBRetrievalContextRequest{AgentID: "agt_reader", KnowledgeBaseIDs: []string{created.ID}})
	require.NoError(t, err)
	require.Len(t, contextResult.KnowledgeBases, 1)
	require.NotEmpty(t, contextResult.KnowledgeBases[0].Sources)
	sourceID := contextResult.KnowledgeBases[0].Sources[0].SourceID
	attestation := encodeAgentThreadAttestation(t, map[string]any{"workspaceId": "wsp_1", "threadChannelId": "thr_kb", "agentId": "agt_reader"})

	post := func(operation, body string) *httptest.ResponseRecorder {
		req := httptest.NewRequest(http.MethodPost, "/api/v1/kb/retrieval/knowledge/"+operation, bytes.NewBufferString(body))
		req.Header.Set("Authorization", authHeader)
		req.Header.Set("X-Workspace-Id", "wsp_1")
		req.Header.Set("X-AgentTeam-Agent-Thread-Attestation", attestation)
		rec := httptest.NewRecorder()
		mux.ServeHTTP(rec, req)
		return rec
	}

	search := post("search", `{"threadChannelId":"thr_kb","kbIds":["`+created.ID+`"],"query":"mcp"}`)
	require.Equal(t, http.StatusOK, search.Code)
	require.Contains(t, search.Body.String(), `"error":{"code":"not_found"`)
	require.Contains(t, search.Body.String(), `"status":404`)

	read := post("read", `{"threadChannelId":"thr_kb","kbIds":["`+created.ID+`"],"sourceId":"`+sourceID+`","wikiSlug":"missing"}`)
	require.Equal(t, http.StatusOK, read.Code)
	require.Contains(t, read.Body.String(), `"error":{"code":"not_found"`)
	require.Contains(t, read.Body.String(), `"status":404`)

	tree := post("tree", `{"threadChannelId":"thr_kb","kbIds":["`+created.ID+`"],"sourceId":"`+sourceID+`"}`)
	require.Equal(t, http.StatusOK, tree.Code)
	require.Contains(t, tree.Body.String(), `"error":{"code":"not_found"`)
	require.Contains(t, tree.Body.String(), `"status":404`)
}

func TestKBHandlerKBKnowledgeAcceptsSnakeCaseAgentTeamAttestationAndRequestBody(t *testing.T) {
	wiki := &apiRecordingKBWikiReader{}
	mux, svc := newKBTestMuxAndServiceWithWiki(t, wiki)
	authHeader := "Bearer " + jwt(map[string]any{"sub": "user-1"})

	created := createKBKnowledgeKB(t, svc, "wsp_1", "user-1", "Runbooks")
	_, err := svc.GrantAgentKnowledgeBaseAccess(context.Background(), "wsp_1", "user-1", "agt_reader", created.ID, "read")
	require.NoError(t, err)

	attestation := encodeAgentThreadAttestation(t, map[string]any{
		"workspace_id":      "wsp_1",
		"thread_channel_id": "thr_kb",
		"agent_id":          "agt_reader",
	})
	search := httptest.NewRequest(http.MethodPost, "/api/v1/kb/retrieval/knowledge/search", bytes.NewBufferString(`{"thread_channel_id":"thr_kb","query":"mcp","limit":5,"kb_ids":["`+created.ID+`"],"source_ids":["src_missing"]}`))
	search.Header.Set("Authorization", authHeader)
	search.Header.Set("X-Workspace-Id", "wsp_1")
	search.Header.Set("X-AgentTeam-Agent-Thread-Attestation", attestation)
	searchRec := httptest.NewRecorder()
	mux.ServeHTTP(searchRec, search)

	require.Equal(t, http.StatusOK, searchRec.Code)
	require.Contains(t, searchRec.Body.String(), `"error":{"code":"not_found"`)
	require.NotContains(t, searchRec.Body.String(), `"title":"Wiki MCP"`)
	require.Empty(t, wiki.searchRequests)
}

func TestKBHandlerKBKnowledgeUsesAgentScopedAuthorizationForWikiReader(t *testing.T) {
	wiki := &apiRecordingKBWikiReader{}
	mux, svc := newKBTestMuxAndServiceWithWiki(t, wiki)
	adminAuth := "Bearer " + jwt(map[string]any{"sub": "admin-user"})

	created := createKBKnowledgeKB(t, svc, "wsp_1", "admin-user", "Runbooks")
	_, err := svc.GrantAgentKnowledgeBaseAccess(context.Background(), "wsp_1", "admin-user", "agt_reader", created.ID, "read")
	require.NoError(t, err)

	agentAuth := "Bearer " + jwt(map[string]any{"sub": "agt_reader", "preferred_username": "mem-agent"})
	attestation := encodeAgentThreadAttestation(t, map[string]any{
		"workspaceId":         "wsp_1",
		"threadChannelId":     "thr_kb",
		"agentId":             "agt_reader",
		"acting_user_id":      "usr_human",
		"acting_display_name": "Human Reviewer",
	})
	search := httptest.NewRequest(http.MethodPost, "/api/v1/kb/retrieval/knowledge/search", bytes.NewBufferString(`{"threadChannelId":"thr_kb","query":"mcp","kbIds":["`+created.ID+`"]}`))
	search.Header.Set("Authorization", agentAuth)
	search.Header.Set("X-Workspace-Id", "wsp_1")
	search.Header.Set("X-AgentTeam-Agent-Thread-Attestation", attestation)
	searchRec := httptest.NewRecorder()
	mux.ServeHTTP(searchRec, search)

	require.Equal(t, http.StatusOK, searchRec.Code)
	require.Len(t, wiki.searchRequests, 1)
	require.Equal(t, agentAuth, wiki.searchRequests[0].AuthorizationHeader)
	require.NotContains(t, wiki.searchRequests[0].AuthorizationHeader, "usr_human")
	require.NotEqual(t, adminAuth, wiki.searchRequests[0].AuthorizationHeader)
}

func TestKBHandlerKBKnowledgeRejectsUnauthorizedAgentScope(t *testing.T) {
	wiki := &apiRecordingKBWikiReader{}
	mux, svc := newKBTestMuxAndServiceWithWiki(t, wiki)
	authHeader := "Bearer " + jwt(map[string]any{"sub": "user-1"})

	allowed := createKBKnowledgeKB(t, svc, "wsp_1", "user-1", "Allowed")
	blocked := createKBKnowledgeKB(t, svc, "wsp_1", "user-1", "Blocked")
	_, err := svc.GrantAgentKnowledgeBaseAccess(context.Background(), "wsp_1", "user-1", "agt_reader", allowed.ID, "read")
	require.NoError(t, err)

	attestation := encodeAgentThreadAttestation(t, map[string]any{
		"workspaceId":     "wsp_1",
		"threadChannelId": "thr_kb",
		"agentId":         "agt_reader",
	})
	search := httptest.NewRequest(http.MethodPost, "/api/v1/kb/retrieval/knowledge/search", bytes.NewBufferString(`{"threadChannelId":"thr_kb","query":"mcp","kbIds":["`+blocked.ID+`"]}`))
	search.Header.Set("Authorization", authHeader)
	search.Header.Set("X-Workspace-Id", "wsp_1")
	search.Header.Set("X-AgentTeam-Agent-Thread-Attestation", attestation)
	searchRec := httptest.NewRecorder()
	mux.ServeHTTP(searchRec, search)

	require.Equal(t, http.StatusForbidden, searchRec.Code)
	require.Contains(t, searchRec.Body.String(), "knowledge base is not accessible to the selected agent")
	require.Empty(t, wiki.searchRequests)
}

func TestKBHandlerKBKnowledgeRejectsUnauthorizedRequestFilter(t *testing.T) {
	wiki := &apiRecordingKBWikiReader{}
	mux, svc := newKBTestMuxAndServiceWithWiki(t, wiki)
	authHeader := "Bearer " + jwt(map[string]any{"sub": "user-1"})

	allowed := createKBKnowledgeKB(t, svc, "wsp_1", "user-1", "Allowed")
	blocked := createKBKnowledgeKB(t, svc, "wsp_1", "user-1", "Blocked")
	_, err := svc.GrantAgentKnowledgeBaseAccess(context.Background(), "wsp_1", "user-1", "agt_reader", allowed.ID, "read")
	require.NoError(t, err)

	attestation := encodeAgentThreadAttestation(t, map[string]any{
		"workspaceId":     "wsp_1",
		"threadChannelId": "thr_kb",
		"agentId":         "agt_reader",
	})
	search := httptest.NewRequest(http.MethodPost, "/api/v1/kb/retrieval/knowledge/search", bytes.NewBufferString(`{"threadChannelId":"thr_kb","query":"mcp","kbIds":["`+blocked.ID+`"]}`))
	search.Header.Set("Authorization", authHeader)
	search.Header.Set("X-Workspace-Id", "wsp_1")
	search.Header.Set("X-AgentTeam-Agent-Thread-Attestation", attestation)
	searchRec := httptest.NewRecorder()
	mux.ServeHTTP(searchRec, search)

	require.Equal(t, http.StatusForbidden, searchRec.Code)
	require.Contains(t, searchRec.Body.String(), "knowledge base is not accessible to the selected agent")
	require.Empty(t, wiki.searchRequests)
}

func createKBKnowledgeKB(t *testing.T, svc *kbservice.KBService, workspaceID, subject, name string) kb.KnowledgeBase {
	t.Helper()
	base, err := svc.CreateKnowledgeBase(context.Background(), workspaceID, subject, kbservice.KBMutation{Name: name})
	require.NoError(t, err)
	_, err = svc.CreateSource(context.Background(), workspaceID, subject, base.ID, kbservice.KBSourceMutation{SourceType: "codebase", Name: name + " Source", IndexStatus: "ready"})
	require.NoError(t, err)
	return base
}

func encodeAgentThreadAttestation(t *testing.T, payload map[string]any) string {
	t.Helper()
	body, err := json.Marshal(payload)
	require.NoError(t, err)
	return base64.RawURLEncoding.EncodeToString(body)
}

func TestKBHandlerDoesNotRegisterRemovedRefOrRepoCandidateRoutes(t *testing.T) {
	mux := newKBTestMux(t)
	createKB := httptest.NewRequest(http.MethodPost, "/api/v1/kb", bytes.NewBufferString(`{"name":"Runbooks"}`))
	createKB.Header.Set("Authorization", "Bearer "+jwt(map[string]any{"sub": "user-1"}))
	createKB.Header.Set("X-Workspace-Id", "wsp_1")
	createKBRec := httptest.NewRecorder()
	mux.ServeHTTP(createKBRec, createKB)
	require.Equal(t, http.StatusCreated, createKBRec.Code)
	var created struct {
		ID string `json:"id"`
	}
	require.NoError(t, json.Unmarshal(createKBRec.Body.Bytes(), &created))
	require.NotEmpty(t, created.ID)

	for _, req := range []*http.Request{
		httptest.NewRequest(http.MethodGet, "/api/v1/kb/"+created.ID+"/refs", nil),
		httptest.NewRequest(http.MethodPost, "/api/v1/kb/"+created.ID+"/refs", bytes.NewBufferString(`{}`)),
		httptest.NewRequest(http.MethodGet, "/api/v1/kb/"+created.ID+"/repo-candidates", nil),
	} {
		req.Header.Set("Authorization", "Bearer "+jwt(map[string]any{"sub": "user-1"}))
		req.Header.Set("X-Workspace-Id", "wsp_1")
		rec := httptest.NewRecorder()
		mux.ServeHTTP(rec, req)
		require.Equal(t, http.StatusNotFound, rec.Code)
	}
}

func TestKBHandlerCreatesRepositoryBackedSourceAndRejectsRemovedFileRoutes(t *testing.T) {
	mux := newKBTestMux(t)
	create := httptest.NewRequest(http.MethodPost, "/api/v1/kb", bytes.NewBufferString(`{"name":"Runbooks","description":"Ops"}`))
	create.Header.Set("Authorization", "Bearer "+jwt(map[string]any{"sub": "user-1"}))
	create.Header.Set("X-Workspace-Id", "wsp_1")
	createRec := httptest.NewRecorder()
	mux.ServeHTTP(createRec, create)
	require.Equal(t, http.StatusCreated, createRec.Code)
	var created struct {
		ID string `json:"id"`
	}
	require.NoError(t, json.Unmarshal(createRec.Body.Bytes(), &created))
	require.NotEmpty(t, created.ID)

	createSource := httptest.NewRequest(http.MethodPost, "/api/v1/kb/"+created.ID+"/sources", bytes.NewBufferString(`{"name":"Incidents","sourceType":"file_repository","defaultRef":"main","indexingMode":"repo_wiki_and_files"}`))
	createSource.Header.Set("Authorization", "Bearer "+jwt(map[string]any{"sub": "user-1"}))
	createSource.Header.Set("X-Workspace-Id", "wsp_1")
	createSourceRec := httptest.NewRecorder()
	mux.ServeHTTP(createSourceRec, createSource)
	require.Equal(t, http.StatusCreated, createSourceRec.Code)
	var createdSource struct {
		ID         string `json:"id"`
		SourceType string `json:"sourceType"`
	}
	require.NoError(t, json.Unmarshal(createSourceRec.Body.Bytes(), &createdSource))
	require.NotEmpty(t, createdSource.ID)
	require.Equal(t, "file_repository", createdSource.SourceType)
	detail := httptest.NewRequest(http.MethodGet, "/api/v1/kb/"+created.ID+"/sources/"+createdSource.ID+"/detail", nil)
	detail.Header.Set("Authorization", "Bearer "+jwt(map[string]any{"sub": "user-1"}))
	detail.Header.Set("X-Workspace-Id", "wsp_1")
	detailRec := httptest.NewRecorder()
	mux.ServeHTTP(detailRec, detail)
	require.Equal(t, http.StatusOK, detailRec.Code)
	require.Contains(t, detailRec.Body.String(), `"canonical"`)
	require.Contains(t, detailRec.Body.String(), `"repoFullName":"user-1/incidents.kb"`)

	get := httptest.NewRequest(http.MethodGet, "/api/v1/kb/"+created.ID, nil)
	get.Header.Set("Authorization", "Bearer "+jwt(map[string]any{"sub": "user-1"}))
	get.Header.Set("X-Workspace-Id", "wsp_1")
	getRec := httptest.NewRecorder()
	mux.ServeHTTP(getRec, get)
	require.Equal(t, http.StatusOK, getRec.Code)
	require.Contains(t, getRec.Body.String(), `"sources":[`)
	require.NotContains(t, getRec.Body.String(), `"materials"`)
	require.NotContains(t, getRec.Body.String(), `"connector_id"`)

	tree := httptest.NewRequest(http.MethodGet, "/api/v1/kb/"+created.ID+"/sources/"+createdSource.ID+"/tree", nil)
	tree.Header.Set("Authorization", "Bearer "+jwt(map[string]any{"sub": "user-1"}))
	tree.Header.Set("X-Workspace-Id", "wsp_1")
	treeRec := httptest.NewRecorder()
	mux.ServeHTTP(treeRec, tree)
	require.Equal(t, http.StatusNotFound, treeRec.Code)

	read := httptest.NewRequest(http.MethodPost, "/api/v1/kb/"+created.ID+"/sources/"+createdSource.ID+"/read", bytes.NewBufferString(`{"fileName":"incident.md"}`))
	read.Header.Set("Authorization", "Bearer "+jwt(map[string]any{"sub": "user-1"}))
	read.Header.Set("X-Workspace-Id", "wsp_1")
	readRec := httptest.NewRecorder()
	mux.ServeHTTP(readRec, read)
	require.Equal(t, http.StatusNotFound, readRec.Code)
}

func TestKBHandlerListsPagedCollectionsAndPinned(t *testing.T) {
	mux := newKBTestMux(t)
	createKB := func(subject string, body string) string {
		req := httptest.NewRequest(http.MethodPost, "/api/v1/kb", bytes.NewBufferString(body))
		req.Header.Set("Authorization", "Bearer "+jwt(map[string]any{"sub": subject}))
		req.Header.Set("X-Workspace-Id", "wsp_1")
		rec := httptest.NewRecorder()
		mux.ServeHTTP(rec, req)
		require.Equal(t, http.StatusCreated, rec.Code)
		var created struct {
			ID string `json:"id"`
		}
		require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &created))
		require.NotEmpty(t, created.ID)
		return created.ID
	}
	secondID := createKB("user-1", `{"name":"Beta"}`)
	createKB("user-1", `{"name":"Alpha"}`)
	createKB("user-2", `{"name":"Shared","defaultWorkspaceMemberRole":"view"}`)

	pin := httptest.NewRequest(http.MethodPut, "/api/v1/kb/"+secondID+"/collection-pin", nil)
	pin.Header.Set("Authorization", "Bearer "+jwt(map[string]any{"sub": "user-1"}))
	pin.Header.Set("X-Workspace-Id", "wsp_1")
	pinRec := httptest.NewRecorder()
	mux.ServeHTTP(pinRec, pin)
	require.Equal(t, http.StatusOK, pinRec.Code)

	list := httptest.NewRequest(http.MethodGet, "/api/v1/kb?view=all&page=1&per_page=1", nil)
	list.Header.Set("Authorization", "Bearer "+jwt(map[string]any{"sub": "user-1"}))
	list.Header.Set("X-Workspace-Id", "wsp_1")
	listRec := httptest.NewRecorder()
	mux.ServeHTTP(listRec, list)
	require.Equal(t, http.StatusOK, listRec.Code)
	var page struct {
		Items       []map[string]any `json:"items"`
		GroupCounts []struct {
			GroupID          string `json:"groupId"`
			PublicationState string `json:"publicationState"`
			Count            int    `json:"count"`
		} `json:"groupCounts"`
		Page    int  `json:"page"`
		PerPage int  `json:"perPage"`
		Total   int  `json:"total"`
		HasMore bool `json:"hasMore"`
	}
	require.NoError(t, json.Unmarshal(listRec.Body.Bytes(), &page))
	require.Len(t, page.Items, 1)
	require.Equal(t, 1, page.Page)
	require.Equal(t, 1, page.PerPage)
	require.Equal(t, 3, page.Total)
	require.True(t, page.HasMore)
	require.Len(t, page.GroupCounts, 1)
	require.NotEmpty(t, page.GroupCounts[0].GroupID)
	require.Equal(t, "official", page.GroupCounts[0].PublicationState)
	require.Equal(t, 3, page.GroupCounts[0].Count, "group count must cover the full filtered collection, not only the page")

	owned := httptest.NewRequest(http.MethodGet, "/api/v1/kb?view=owned", nil)
	owned.Header.Set("Authorization", "Bearer "+jwt(map[string]any{"sub": "user-1"}))
	owned.Header.Set("X-Workspace-Id", "wsp_1")
	ownedRec := httptest.NewRecorder()
	mux.ServeHTTP(ownedRec, owned)
	require.Equal(t, http.StatusOK, ownedRec.Code)
	var ownedPage struct {
		Items []struct {
			ID        string `json:"id"`
			CreatedBy string `json:"createdBy"`
		} `json:"items"`
		Total int `json:"total"`
	}
	require.NoError(t, json.Unmarshal(ownedRec.Body.Bytes(), &ownedPage))
	require.Len(t, ownedPage.Items, 2)
	require.Equal(t, 2, ownedPage.Total)
	for _, item := range ownedPage.Items {
		require.Equal(t, "user-1", item.CreatedBy)
	}

	pinned := httptest.NewRequest(http.MethodGet, "/api/v1/kb?view=pinned", nil)
	pinned.Header.Set("Authorization", "Bearer "+jwt(map[string]any{"sub": "user-1"}))
	pinned.Header.Set("X-Workspace-Id", "wsp_1")
	pinnedRec := httptest.NewRecorder()
	mux.ServeHTTP(pinnedRec, pinned)
	require.Equal(t, http.StatusOK, pinnedRec.Code)
	var pinnedPage struct {
		Items []struct {
			ID     string `json:"id"`
			Pinned bool   `json:"pinned"`
		} `json:"items"`
	}
	require.NoError(t, json.Unmarshal(pinnedRec.Body.Bytes(), &pinnedPage))
	require.Len(t, pinnedPage.Items, 1)
	require.Equal(t, secondID, pinnedPage.Items[0].ID)
	require.True(t, pinnedPage.Items[0].Pinned)

}

func TestKBHandlerManagesAndFiltersKnowledgeBaseTaxonomy(t *testing.T) {
	mux := newKBTestMux(t)
	send := func(method, path string, payload any) *httptest.ResponseRecorder {
		t.Helper()
		body := bytes.NewReader(nil)
		if payload != nil {
			raw, err := json.Marshal(payload)
			require.NoError(t, err)
			body = bytes.NewReader(raw)
		}
		req := httptest.NewRequest(method, path, body)
		req.Header.Set("Authorization", "Bearer "+jwt(map[string]any{"sub": "owner"}))
		req.Header.Set("X-Workspace-Id", "wsp_1")
		rec := httptest.NewRecorder()
		mux.ServeHTTP(rec, req)
		return rec
	}

	taxonomyRec := send(http.MethodGet, "/api/v1/kb/collection/taxonomy", nil)
	require.Equal(t, http.StatusOK, taxonomyRec.Code)
	var initialTaxonomy struct {
		Groups []struct {
			ID        string `json:"id"`
			IsDefault bool   `json:"isDefault"`
		} `json:"groups"`
	}
	require.NoError(t, json.Unmarshal(taxonomyRec.Body.Bytes(), &initialTaxonomy))
	require.Len(t, initialTaxonomy.Groups, 1)
	require.True(t, initialTaxonomy.Groups[0].IsDefault)

	groupRec := send(http.MethodPost, "/api/v1/kb/collection/groups", map[string]any{"name": "Operations", "color": "#123ABC"})
	require.Equal(t, http.StatusCreated, groupRec.Code)
	var group struct {
		ID   string `json:"id"`
		Name string `json:"name"`
	}
	require.NoError(t, json.Unmarshal(groupRec.Body.Bytes(), &group))
	require.NotEmpty(t, group.ID)

	createTag := func(name string) string {
		rec := send(http.MethodPost, "/api/v1/kb/collection/tags", map[string]any{"name": name})
		require.Equal(t, http.StatusCreated, rec.Code)
		var tag struct {
			ID string `json:"id"`
		}
		require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &tag))
		require.NotEmpty(t, tag.ID)
		return tag.ID
	}
	productionID := createTag("Production")
	runbookID := createTag("Runbook")

	createBase := func(name string, tagIDs []string) string {
		rec := send(http.MethodPost, "/api/v1/kb", map[string]any{"name": name, "groupId": group.ID, "tagIds": tagIDs})
		require.Equal(t, http.StatusCreated, rec.Code, rec.Body.String())
		var base struct {
			ID          string `json:"id"`
			GroupID     string `json:"groupId"`
			SourceCount int64  `json:"sourceCount"`
			Tags        []struct {
				ID string `json:"id"`
			} `json:"tags"`
		}
		require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &base))
		require.Equal(t, group.ID, base.GroupID)
		require.Len(t, base.Tags, len(tagIDs))
		require.Zero(t, base.SourceCount)
		return base.ID
	}
	bothID := createBase("Incident response", []string{productionID, runbookID})
	createBase("Deployments", []string{productionID})

	listRec := send(http.MethodGet, "/api/v1/kb?view=all&tag="+productionID+"&tag="+runbookID+"&q=runbook", nil)
	require.Equal(t, http.StatusOK, listRec.Code, listRec.Body.String())
	var page struct {
		Items []struct {
			ID      string `json:"id"`
			GroupID string `json:"groupId"`
			Group   struct {
				Name string `json:"name"`
			} `json:"group"`
			Tags []struct {
				ID string `json:"id"`
			} `json:"tags"`
		} `json:"items"`
	}
	require.NoError(t, json.Unmarshal(listRec.Body.Bytes(), &page))
	require.Len(t, page.Items, 1)
	require.Equal(t, bothID, page.Items[0].ID)
	require.Equal(t, group.ID, page.Items[0].GroupID)
	require.Equal(t, "Operations", page.Items[0].Group.Name)
	require.Len(t, page.Items[0].Tags, 2)
}

func TestKBHandlerReturnsFriendlySourceMirrorPreflightError(t *testing.T) {
	remote := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		http.Error(w, "authentication required", http.StatusUnauthorized)
	}))
	t.Cleanup(remote.Close)

	mux, _, _ := newKBTestMuxServiceStoreWithOptions(t, nil, func(options *kbservice.KBServiceOptions) {
		options.SourceMirrorPreflighter = kbservice.AGSHTTPSourceMirrorSyncer{}
		options.AllowPrivateSourceMirrorHosts = true
	})
	authHeader := "Bearer " + jwt(map[string]any{"sub": "owner"})
	createKB := httptest.NewRequest(http.MethodPost, "/api/v1/kb", bytes.NewBufferString(`{"name":"Runbooks"}`))
	createKB.Header.Set("Authorization", authHeader)
	createKB.Header.Set("X-Workspace-Id", "wsp_1")
	createKBRec := httptest.NewRecorder()
	mux.ServeHTTP(createKBRec, createKB)
	require.Equal(t, http.StatusCreated, createKBRec.Code, createKBRec.Body.String())
	var base struct {
		ID string `json:"id"`
	}
	require.NoError(t, json.Unmarshal(createKBRec.Body.Bytes(), &base))

	createSource := httptest.NewRequest(http.MethodPost, "/api/v1/kb/"+base.ID+"/sources", bytes.NewBufferString(`{
		"sourceType":"codebase",
		"name":"External agentteam",
		"repoMode":"mirror",
		"mirror":{
			"upstreamType":"generic_git",
			"remoteUrl":"`+remote.URL+`/pingcap/agentteam.git"
		}
	}`))
	createSource.Header.Set("Authorization", authHeader)
	createSource.Header.Set("X-Workspace-Id", "wsp_1")
	createSourceRec := httptest.NewRecorder()
	mux.ServeHTTP(createSourceRec, createSource)

	require.Equal(t, http.StatusBadRequest, createSourceRec.Code, createSourceRec.Body.String())
	require.JSONEq(t, `{
		"error":"source_mirror_authentication_failed",
		"message":"Could not access the repository. Check the URL and provide an access token with read permission."
	}`, createSourceRec.Body.String())
}

func TestKBHandlerMapsSourceRepositoryNameOverrideReservedSuffix(t *testing.T) {
	mux := newKBTestMux(t)
	authHeader := "Bearer " + jwt(map[string]any{"sub": "user-1"})
	createKB := httptest.NewRequest(http.MethodPost, "/api/v1/kb", bytes.NewBufferString(`{"name":"Runbooks"}`))
	createKB.Header.Set("Authorization", authHeader)
	createKB.Header.Set("X-Workspace-Id", "wsp_1")
	createKBRec := httptest.NewRecorder()
	mux.ServeHTTP(createKBRec, createKB)
	require.Equal(t, http.StatusCreated, createKBRec.Code, createKBRec.Body.String())
	var created struct {
		ID string `json:"id"`
	}
	require.NoError(t, json.Unmarshal(createKBRec.Body.Bytes(), &created))

	createSource := httptest.NewRequest(http.MethodPost, "/api/v1/kb/"+created.ID+"/sources", bytes.NewBufferString(`{"name":"AgentTeam","sourceType":"codebase","repo":{"repoNameOverride":"agentteam.kb"}}`))
	createSource.Header.Set("Authorization", authHeader)
	createSource.Header.Set("X-Workspace-Id", "wsp_1")
	createSourceRec := httptest.NewRecorder()
	mux.ServeHTTP(createSourceRec, createSource)

	require.Equal(t, http.StatusConflict, createSourceRec.Code, createSourceRec.Body.String())
	var body struct {
		Error   string `json:"error"`
		Details struct {
			Field             string `json:"field"`
			SuggestedRepoName string `json:"suggestedRepoName"`
		} `json:"details"`
	}
	require.NoError(t, json.Unmarshal(createSourceRec.Body.Bytes(), &body))
	require.Equal(t, "slug_reserved_suffix", body.Error)
	require.Equal(t, "repoNameOverride", body.Details.Field)
	require.Equal(t, "agentteam", body.Details.SuggestedRepoName)
}

func TestKBHandlerCreatesCodebaseSourceWithoutProjectMembership(t *testing.T) {
	mux := newKBTestMux(t)
	createKB := httptest.NewRequest(http.MethodPost, "/api/v1/kb", bytes.NewBufferString(`{"name":"Runbooks"}`))
	createKB.Header.Set("Authorization", "Bearer "+jwt(map[string]any{"sub": "user-1"}))
	createKB.Header.Set("X-Workspace-Id", "wsp_1")
	createKBRec := httptest.NewRecorder()
	mux.ServeHTTP(createKBRec, createKB)
	require.Equal(t, http.StatusCreated, createKBRec.Code)
	var created struct {
		ID string `json:"id"`
	}
	require.NoError(t, json.Unmarshal(createKBRec.Body.Bytes(), &created))

	createSource := httptest.NewRequest(http.MethodPost, "/api/v1/kb/"+created.ID+"/sources", bytes.NewBufferString(`{"name":"AgentTeam","sourceType":"codebase","defaultRef":"main"}`))
	createSource.Header.Set("Authorization", "Bearer "+jwt(map[string]any{"sub": "user-1"}))
	createSource.Header.Set("X-Workspace-Id", "wsp_1")
	createSourceRec := httptest.NewRecorder()
	mux.ServeHTTP(createSourceRec, createSource)
	require.Equal(t, http.StatusCreated, createSourceRec.Code)
	require.Contains(t, createSourceRec.Body.String(), `"sourceType":"codebase"`)
	require.NotContains(t, createSourceRec.Body.String(), "locale")
	require.NotContains(t, createSourceRec.Body.String(), `"repoFullName"`)
	require.NotContains(t, createSourceRec.Body.String(), "project_id")
	require.NotContains(t, createSourceRec.Body.String(), "submodule")
	var source struct {
		ID string `json:"id"`
	}
	require.NoError(t, json.Unmarshal(createSourceRec.Body.Bytes(), &source))
	require.NotEmpty(t, source.ID)

	createFileRepoSourceWithVisibility := httptest.NewRequest(http.MethodPost, "/api/v1/kb/"+created.ID+"/sources", bytes.NewBufferString(`{"name":"Runbook files","sourceType":"file_repository","repoMode":"create","repo":{"name":"Runbook files","repoNameOverride":"runbook-files","description":"Runbook file repository","visibility":"public","defaultBranch":"main"},"indexingMode":"repo_wiki_and_files"}`))
	createFileRepoSourceWithVisibility.Header.Set("Authorization", "Bearer "+jwt(map[string]any{"sub": "user-1"}))
	createFileRepoSourceWithVisibility.Header.Set("X-Workspace-Id", "wsp_1")
	createFileRepoSourceWithVisibilityRec := httptest.NewRecorder()
	mux.ServeHTTP(createFileRepoSourceWithVisibilityRec, createFileRepoSourceWithVisibility)
	require.Equal(t, http.StatusBadRequest, createFileRepoSourceWithVisibilityRec.Code)

	createFileRepoSource := httptest.NewRequest(http.MethodPost, "/api/v1/kb/"+created.ID+"/sources", bytes.NewBufferString(`{"name":"Runbook files","sourceType":"file_repository","repoMode":"create","repo":{"name":"Runbook files","repoNameOverride":"runbook-files","description":"Runbook file repository","defaultBranch":"main"},"indexingMode":"repo_wiki_and_files"}`))
	createFileRepoSource.Header.Set("Authorization", "Bearer "+jwt(map[string]any{"sub": "user-1"}))
	createFileRepoSource.Header.Set("X-Workspace-Id", "wsp_1")
	createFileRepoSourceRec := httptest.NewRecorder()
	mux.ServeHTTP(createFileRepoSourceRec, createFileRepoSource)
	require.Equal(t, http.StatusCreated, createFileRepoSourceRec.Code, createFileRepoSourceRec.Body.String())
	require.Contains(t, createFileRepoSourceRec.Body.String(), `"sourceType":"file_repository"`)
	require.NotContains(t, createFileRepoSourceRec.Body.String(), `"repoFullName"`)

	databaseSource := httptest.NewRequest(http.MethodPost, "/api/v1/kb/"+created.ID+"/sources", bytes.NewBufferString(`{"name":"DB schema","sourceType":"database_schema","configJson":{"databaseType":"mysql"}}`))
	databaseSource.Header.Set("Authorization", "Bearer "+jwt(map[string]any{"sub": "user-1"}))
	databaseSource.Header.Set("X-Workspace-Id", "wsp_1")
	databaseSourceRec := httptest.NewRecorder()
	mux.ServeHTTP(databaseSourceRec, databaseSource)
	require.Equal(t, http.StatusCreated, databaseSourceRec.Code)
	require.Contains(t, databaseSourceRec.Body.String(), `"sourceType":"database_schema"`)
	require.Contains(t, databaseSourceRec.Body.String(), `"configJson":"{\"databaseType\":\"mysql\"}"`)
	require.NotContains(t, databaseSourceRec.Body.String(), `"repoFullName"`)
	require.Contains(t, databaseSourceRec.Body.String(), `"indexingMode":"kb_repo_materialized"`)

	connectorSource := httptest.NewRequest(http.MethodPost, "/api/v1/kb/"+created.ID+"/sources", bytes.NewBufferString(`{"name":"Drive","sourceType":"external_connector","configJson":"{\"provider\":\"google_drive\"}"}`))
	connectorSource.Header.Set("Authorization", "Bearer "+jwt(map[string]any{"sub": "user-1"}))
	connectorSource.Header.Set("X-Workspace-Id", "wsp_1")
	connectorSourceRec := httptest.NewRecorder()
	mux.ServeHTTP(connectorSourceRec, connectorSource)
	require.Equal(t, http.StatusCreated, connectorSourceRec.Code)
	require.Contains(t, connectorSourceRec.Body.String(), `"sourceType":"external_connector"`)
	require.NotContains(t, connectorSourceRec.Body.String(), `"repoFullName"`)

	invalidSource := httptest.NewRequest(http.MethodPost, "/api/v1/kb/"+created.ID+"/sources", bytes.NewBufferString(`{"name":"Sheet","sourceType":"spreadsheet"}`))
	invalidSource.Header.Set("Authorization", "Bearer "+jwt(map[string]any{"sub": "user-1"}))
	invalidSource.Header.Set("X-Workspace-Id", "wsp_1")
	invalidSourceRec := httptest.NewRecorder()
	mux.ServeHTTP(invalidSourceRec, invalidSource)
	require.Equal(t, http.StatusBadRequest, invalidSourceRec.Code)
}

func jwt(claims map[string]any) string {
	header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"none"}`))
	payload, _ := json.Marshal(claims)
	return header + "." + base64.RawURLEncoding.EncodeToString(payload) + "."
}

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

func TestKBHandlerListsKnowledgeBaseActivity(t *testing.T) {
	mux, svc := newKBTestMuxAndServiceWithWiki(t, nil)
	ctx := context.Background()
	base, err := svc.CreateKnowledgeBase(ctx, "wsp_1", "owner", kbservice.KBMutation{Name: "Runbooks"})
	require.NoError(t, err)
	source, err := svc.CreateSource(ctx, "wsp_1", "owner", base.ID, kbservice.KBSourceMutation{SourceType: "codebase", Name: "AgentTeam"})
	require.NoError(t, err)

	req := httptest.NewRequest(http.MethodGet, "/api/v1/kb/activity?assetId="+base.ID+"&eventType=source_created&limit=10", nil)
	req.Header.Set("Authorization", "Bearer "+jwt(map[string]any{"sub": "owner"}))
	req.Header.Set("X-Workspace-Id", "wsp_1")
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)

	require.Equal(t, http.StatusOK, rec.Code)
	var response struct {
		Items []struct {
			ID        string         `json:"id"`
			AssetID   string         `json:"assetId"`
			EventType string         `json:"eventType"`
			Details   map[string]any `json:"details"`
		} `json:"items"`
	}
	require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &response))
	require.Len(t, response.Items, 1)
	require.Equal(t, "source_created", response.Items[0].EventType)
	require.Equal(t, source.ID, response.Items[0].AssetID)
	require.Equal(t, base.ID, response.Items[0].Details["knowledgeBaseId"])
	require.Equal(t, source.ID, response.Items[0].Details["sourceId"])
}
