package kb

import (
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	contentservice "github.com/tidbcloud/AgentTeam/internal/service/content"
	contentstore "github.com/tidbcloud/AgentTeam/internal/store/content"
	storekb "github.com/tidbcloud/AgentTeam/internal/store/kb"
	"github.com/tidbcloud/AgentTeam/internal/store/resource/repos"
)

func TestNativeKBSourceContentReaderUsesExactImmutableVersion(t *testing.T) {
	fixture := newKBSourceContentMigrationFixture(t, true)
	firstFiles := []KBSourceContentExportFile{
		{Path: "README.md", Data: []byte("first")},
		{Path: "docs/a.md", Data: []byte("a")},
		{Path: "docs/deep/b.md", Data: []byte("b")},
	}
	firstDigest, firstSize := testKBSourceExportDigest(firstFiles)
	fixture.exporter.result = KBSourceContentExport{
		Files: firstFiles, RootDigest: firstDigest, SizeBytes: firstSize,
		SourceCommittedAt: time.Date(2026, 8, 20, 1, 2, 3, 0, time.UTC),
	}
	first, err := fixture.service.RunExact(t.Context(), fixture.request)
	require.NoError(t, err)

	reader := NativeKBSourceContentReader{Content: fixture.authorized, Search: fixture.store}
	readRequest := KBWikiReadRequest{
		PrincipalID: "owner", PrincipalType: kbContentSubjectUser,
		WorkspaceID: "wsp_1", KnowledgeBaseID: "kb_1", SourceID: "src_1",
		ContentID: first.ContentID, ContentVersionID: first.ContentVersionID,
		RepoRole: repos.RepoRolePrimary, Slug: "README.md", Revision: first.ContentVersionID,
	}
	page, err := reader.ReadWikiPage(t.Context(), readRequest)
	require.NoError(t, err)
	require.Equal(t, "first", page.Body)
	require.Equal(t, first.ContentVersionID, page.SHA)
	search, err := reader.SearchWiki(t.Context(), KBWikiSearchRequest{
		PrincipalID: "owner", PrincipalType: kbContentSubjectUser,
		WorkspaceID: "wsp_1", KnowledgeBaseID: "kb_1", SourceID: "src_1",
		ContentID: first.ContentID, ContentVersionID: first.ContentVersionID,
		RepoRole: repos.RepoRolePrimary, Revision: first.ContentVersionID,
		Query: "first", Limit: 10,
	})
	require.NoError(t, err)
	require.Len(t, search.Results, 1)
	require.Equal(t, "README.md", search.Results[0].Slug)
	require.Equal(t, first.ContentVersionID, search.Results[0].SHA)

	root, err := reader.ListWikiTree(t.Context(), KBWikiTreeRequest{
		PrincipalID: "owner", PrincipalType: kbContentSubjectUser,
		WorkspaceID: "wsp_1", KnowledgeBaseID: "kb_1", SourceID: "src_1",
		ContentID: first.ContentID, ContentVersionID: first.ContentVersionID,
		RepoRole: repos.RepoRolePrimary, Revision: first.ContentVersionID,
	})
	require.NoError(t, err)
	require.Equal(t, []KBWikiTreeEntry{
		{Path: "README.md", Name: "README.md", Kind: "file", Slug: "README.md", Title: "README.md", SHA: first.ContentVersionID, Size: 5},
		{Path: "docs", Name: "docs", Kind: "directory", Title: "docs", IsDir: true, SHA: first.ContentVersionID},
	}, root)
	docs, err := reader.ListWikiTree(t.Context(), KBWikiTreeRequest{
		PrincipalID: "owner", PrincipalType: kbContentSubjectUser,
		WorkspaceID: "wsp_1", KnowledgeBaseID: "kb_1", SourceID: "src_1",
		ContentID: first.ContentID, ContentVersionID: first.ContentVersionID,
		RepoRole: repos.RepoRolePrimary, Path: "docs", Revision: first.ContentVersionID,
	})
	require.NoError(t, err)
	require.Equal(t, []KBWikiTreeEntry{
		{Path: "docs/a.md", Name: "a.md", Kind: "file", Slug: "docs/a.md", Title: "docs/a.md", SHA: first.ContentVersionID, Size: 1},
		{Path: "docs/deep", Name: "deep", Kind: "directory", Title: "docs/deep", IsDir: true, SHA: first.ContentVersionID},
	}, docs)

	now := time.Date(2026, 8, 21, 6, 0, 0, 0, time.UTC)
	require.NoError(t, fixture.db.Create(&storekb.SourceRevision{
		ID: "rev_2", WorkspaceID: "wsp_1", KnowledgeBaseID: "kb_1", SourceID: "src_1",
		SourceCommitSHA: strings.Repeat("b", 40), CreatedAt: now, UpdatedAt: now,
	}).Error)
	fixture.request.SourceRevisionID = "rev_2"
	fixture.request.CommitSHA = strings.Repeat("b", 40)
	fixture.request.ExpectedHeadVersionID = first.ContentVersionID
	secondFiles := []KBSourceContentExportFile{{Path: "README.md", Data: []byte("second")}}
	secondDigest, secondSize := testKBSourceExportDigest(secondFiles)
	fixture.exporter.result = KBSourceContentExport{Files: secondFiles, RootDigest: secondDigest, SizeBytes: secondSize, SourceCommittedAt: now}
	second, err := fixture.service.RunExact(t.Context(), fixture.request)
	require.NoError(t, err)

	oldPage, err := reader.ReadWikiPage(t.Context(), readRequest)
	require.NoError(t, err)
	require.Equal(t, "first", oldPage.Body, "an old citation must replay its exact immutable ContentVersion")
	readRequest.ContentVersionID = second.ContentVersionID
	readRequest.Revision = second.ContentVersionID
	newPage, err := reader.ReadWikiPage(t.Context(), readRequest)
	require.NoError(t, err)
	require.Equal(t, "second", newPage.Body)

	_, err = reader.ReadWikiPage(t.Context(), KBWikiReadRequest{
		PrincipalID: "owner", PrincipalType: kbContentSubjectUser,
		WorkspaceID: "wsp_1", KnowledgeBaseID: "kb_1", SourceID: "src_1",
		ContentID: first.ContentID, ContentVersionID: first.ContentVersionID,
		RepoRole: repos.RepoRolePrimary, Slug: "../secret", Revision: first.ContentVersionID,
	})
	require.ErrorContains(t, err, "path is invalid")

	require.NoError(t, fixture.db.Model(&storekb.Source{}).Where("workspace_id = ? AND id = ?", "wsp_1", "src_1").Update("deleted_at", now).Error)
	_, err = reader.ReadWikiPage(t.Context(), readRequest)
	require.ErrorIs(t, err, contentservice.ErrAccessDenied)
}

func TestNativeKBSourceContentReaderRequiresConfiguredProjection(t *testing.T) {
	reader := NativeKBSourceContentReader{}
	_, err := reader.SearchWiki(t.Context(), KBWikiSearchRequest{})
	require.ErrorContains(t, err, "search projection is not configured")
}

func TestKBServiceRoutesNativeAuthorityByExactVersionAndReplaysOldCitation(t *testing.T) {
	fixture := newKBSourceContentMigrationFixture(t, true)
	fixture.workspace.allowed["wsp_1\x00agt_reader"] = true
	now := time.Date(2026, 8, 21, 7, 0, 0, 0, time.UTC)
	require.NoError(t, fixture.db.Create(&storekb.KnowledgeBaseGrant{
		ID: "kbg_agent", WorkspaceID: "wsp_1", KnowledgeBaseID: "kb_1", SubjectType: "agent", SubjectID: "agt_reader", Role: "view",
		RepositoryAccessProfile: storekb.KnowledgeBaseRepositoryAccessProfileRead,
		CreatedBy:               "owner", UpdatedBy: "owner", CreatedAt: now, UpdatedAt: now,
	}).Error)

	first, err := fixture.service.RunExact(t.Context(), fixture.request)
	require.NoError(t, err)
	authorityStore := contentstore.New(fixture.db)
	_, err = authorityStore.ActivateNativeAuthority(t.Context(), contentstore.ActivateNativeAuthorityParams{
		WorkspaceID: "wsp_1", Domain: kbSourceContentDomain, BindingKey: "src_1", Checkpoint: first.ContentVersionID, Actor: "owner", Now: now,
	})
	require.NoError(t, err)
	svc := NewKBServiceWithOptions(fixture.store, func() time.Time { return now }, KBServiceOptions{
		WikiReader:       &recordingKBWikiReader{},
		NativeWikiReader: NativeKBSourceContentReader{Content: fixture.authorized, Search: fixture.store},
		ContentAuthority: authorityStore,
		RepositoryStore:  fixture.repos,
	})
	search, err := svc.SearchKnowledge(t.Context(), "wsp_1", "owner", KBSearchRequest{
		AgentID: "agt_reader", KnowledgeBaseIDs: []string{"kb_1"}, SourceIDs: []string{"src_1"}, Query: "current", Limit: 10,
	})
	require.NoError(t, err)
	require.Len(t, search.Hits, 1)
	require.Equal(t, first.ContentVersionID, search.Hits[0].Revision)
	require.Contains(t, search.Hits[0].CitationURI, first.ContentVersionID)

	read, err := svc.ReadKnowledge(t.Context(), "wsp_1", "owner", KBReadRequest{
		AgentID: "agt_reader", KnowledgeBaseIDs: []string{"kb_1"}, SourceID: "src_1", Slug: "README.md",
	})
	require.NoError(t, err)
	require.Equal(t, "current", read.Body)
	require.Equal(t, first.ContentVersionID, read.Revision)
	oldCitation := read.CitationURI

	require.NoError(t, fixture.db.Model(&storekb.SourceRevision{}).Where("id = ?", "rev_1").Update("is_current", false).Error)
	require.NoError(t, fixture.db.Create(&storekb.SourceRevision{
		ID: "rev_2", WorkspaceID: "wsp_1", KnowledgeBaseID: "kb_1", SourceID: "src_1", SourceCommitSHA: strings.Repeat("b", 40),
		IsCurrent: true, CreatedAt: now, UpdatedAt: now,
	}).Error)
	fixture.request.SourceRevisionID = "rev_2"
	fixture.request.CommitSHA = strings.Repeat("b", 40)
	fixture.request.ExpectedHeadVersionID = first.ContentVersionID
	fixture.exporter.result.Files = []KBSourceContentExportFile{{Path: "README.md", Data: []byte("next")}}
	fixture.exporter.result.RootDigest, fixture.exporter.result.SizeBytes = testKBSourceExportDigest(fixture.exporter.result.Files)
	fixture.exporter.result.SourceCommittedAt = now
	second, err := fixture.service.RunExact(t.Context(), fixture.request)
	require.NoError(t, err)
	require.NotEqual(t, first.ContentVersionID, second.ContentVersionID)

	replayed, err := svc.ReadKnowledge(t.Context(), "wsp_1", "owner", KBReadRequest{AgentID: "agt_reader", CitationURI: oldCitation})
	require.NoError(t, err)
	require.Equal(t, "current", replayed.Body)
	require.Equal(t, first.ContentVersionID, replayed.Revision)

	// The live exact product override must revoke the mapped canonical role
	// even while the old materialization projection remains current.
	require.NoError(t, fixture.db.Create(&storekb.KnowledgeBaseSourceRepositoryAccessOverride{
		ID:          storekb.KnowledgeBaseSourceRepositoryAccessOverrideID("wsp_1", "kb_1", "kbg_agent", "src_1", repos.RepoRolePrimary),
		WorkspaceID: "wsp_1", KnowledgeBaseID: "kb_1", GrantID: "kbg_agent", SourceID: "src_1", RepoRole: repos.RepoRolePrimary,
		SubjectType: "agent", SubjectID: "agt_reader", RepositoryAccessProfile: storekb.KnowledgeBaseRepositoryAccessProfileNone,
		CreatedBy: "owner", UpdatedBy: "owner", CreatedAt: now, UpdatedAt: now,
	}).Error)
	_, err = svc.ReadKnowledge(t.Context(), "wsp_1", "owner", KBReadRequest{AgentID: "agt_reader", CitationURI: oldCitation})
	require.Error(t, err)
	require.NoError(t, fixture.store.DeleteKnowledgeBaseSourceRepositoryAccessOverride(t.Context(), "wsp_1", "kb_1", "kbg_agent", "src_1", repos.RepoRolePrimary))

	fixture.workspace.allowed["wsp_1\x00agt_reader"] = false
	_, err = svc.ReadKnowledge(t.Context(), "wsp_1", "owner", KBReadRequest{AgentID: "agt_reader", CitationURI: oldCitation})
	require.ErrorIs(t, err, contentservice.ErrAccessDenied)
}

func TestKBServiceNativeAuthorityFailsClosedWithoutExactMapping(t *testing.T) {
	fixture := newKBSourceContentMigrationFixture(t, true)
	authorityStore := contentstore.New(fixture.db)
	_, err := authorityStore.ActivateNativeAuthority(t.Context(), contentstore.ActivateNativeAuthorityParams{
		WorkspaceID: "wsp_1", Domain: kbSourceContentDomain, BindingKey: "src_1", Checkpoint: "missing", Actor: "owner", Now: time.Now().UTC(),
	})
	require.NoError(t, err)
	svc := NewKBServiceWithOptions(fixture.store, time.Now, KBServiceOptions{
		NativeWikiReader: NativeKBSourceContentReader{Content: fixture.authorized, Search: fixture.store}, ContentAuthority: authorityStore, RepositoryStore: fixture.repos,
	})
	_, err = svc.projectRetrievalSources(t.Context(), "wsp_1", []storekb.Source{{
		ID: "src_1", WorkspaceID: "wsp_1", KnowledgeBaseID: "kb_1", SourceType: "codebase", Status: "ready",
	}})
	require.ErrorContains(t, err, "no complete exact ContentVersion mapping")
}
