package kb

import (
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

func TestProjectSourceContentMappingCreatesExactMappingAndProjection(t *testing.T) {
	store, db, revision := newSourceContentMappingStore(t)
	params := exactSourceContentMappingParams(revision)

	mapping, err := store.ProjectSourceContentMapping(t.Context(), params)
	require.NoError(t, err)
	require.Equal(t, params.ContentVersionID, mapping.ContentVersionID)

	current, err := store.GetCurrentSourceRevision(t.Context(), revision.WorkspaceID, revision.KnowledgeBaseID, revision.SourceID)
	require.NoError(t, err)
	require.Equal(t, params.ContentID, current.SourceContentID)
	require.Equal(t, params.ContentVersionID, current.SourceContentVersionID)
	require.NotNil(t, current.SourceContentVerifiedAt)
	require.Equal(t, int64(1), sourceMigrationRowCount(t, db, &SourceContentMapping{}))
	require.Equal(t, int64(2), sourceMigrationRowCount(t, db, &SourceSearchDocument{}))
	hits, hasMore, err := store.SearchSourceContentDocuments(t.Context(), revision.WorkspaceID, revision.SourceID, params.ContentID, params.ContentVersionID, "second", 10, 0)
	require.NoError(t, err)
	require.False(t, hasMore)
	require.Equal(t, []SourceSearchDocumentHit{{Path: "docs/second.md", Title: "second.md", SearchText: "second document"}}, hits)

	params.ID = "map_retry"
	replayed, err := store.ProjectSourceContentMapping(t.Context(), params)
	require.NoError(t, err)
	require.Equal(t, mapping.ID, replayed.ID)
	require.Equal(t, int64(1), sourceMigrationRowCount(t, db, &SourceContentMapping{}))
}

func TestProjectSourceContentMappingRejectsChangedRevisionWithoutMapping(t *testing.T) {
	store, db, revision := newSourceContentMappingStore(t)
	params := exactSourceContentMappingParams(revision)
	require.NoError(t, db.Model(&SourceRevision{}).Where("id = ?", revision.ID).Update("source_commit_sha", strings.Repeat("b", 40)).Error)

	_, err := store.ProjectSourceContentMapping(t.Context(), params)
	require.ErrorIs(t, err, ErrSourceContentMigrationConflict)
	require.Equal(t, int64(0), sourceMigrationRowCount(t, db, &SourceContentMapping{}))
}

func TestProjectSourceContentMappingRejectsConflictingLegacyIdentity(t *testing.T) {
	store, db, revision := newSourceContentMappingStore(t)
	params := exactSourceContentMappingParams(revision)
	other := SourceContentMapping{
		ID: "map_other", WorkspaceID: revision.WorkspaceID, KnowledgeBaseID: "kb_other", SourceID: "src_other", SourceRevisionID: "rev_other",
		RepositoryRole: params.RepositoryRole, RepositoryID: params.RepositoryID, CanonicalBranch: params.CanonicalBranch, CommitSHA: params.CommitSHA,
		ContentID: "ctn_other", ContentVersionID: "ctv_other", RootDigest: strings.Repeat("e", 64), FileCount: 1, SizeBytes: 1,
		SourceCommittedAt: params.SourceCommittedAt,
		VerifiedAt:        &params.VerifiedAt, CreatedAt: params.VerifiedAt, UpdatedAt: params.VerifiedAt,
	}
	require.NoError(t, db.Create(&other).Error)

	_, err := store.ProjectSourceContentMapping(t.Context(), params)
	require.ErrorIs(t, err, ErrSourceContentMigrationConflict)
	current, loadErr := store.GetCurrentSourceRevision(t.Context(), revision.WorkspaceID, revision.KnowledgeBaseID, revision.SourceID)
	require.NoError(t, loadErr)
	require.Empty(t, current.SourceContentID)
}

func TestSourceContentMappingExactACLReadsUseBoundedIndexes(t *testing.T) {
	store, db, revision := newSourceContentMappingStore(t)
	params := exactSourceContentMappingParams(revision)
	_, err := store.ProjectSourceContentMapping(t.Context(), params)
	require.NoError(t, err)
	oneRowRoles, err := store.ListSourceContentRepositoryRolesByContent(t.Context(), revision.WorkspaceID, revision.KnowledgeBaseID, revision.SourceID, params.ContentID)
	require.NoError(t, err)
	require.Equal(t, []string{"canonical"}, oneRowRoles)
	oneRowRolePlan := sqliteQueryPlanDetails(t, db, sourceContentRepositoryRolePresenceQuery,
		sourceContentRepositoryRolePresenceArgs(revision.WorkspaceID, revision.KnowledgeBaseID, revision.SourceID, params.ContentID)...)
	require.Equal(t, 7, strings.Count(oneRowRolePlan, "idx_kb_source_content_mapping_content"), oneRowRolePlan)

	require.NoError(t, db.Create(&SourceContentMapping{
		ID: "map_2", WorkspaceID: revision.WorkspaceID, KnowledgeBaseID: revision.KnowledgeBaseID, SourceID: revision.SourceID, SourceRevisionID: revision.ID,
		RepositoryRole: "mirror", RepositoryID: "43", CanonicalBranch: "main", CommitSHA: strings.Repeat("b", 40),
		ContentID: params.ContentID, ContentVersionID: "ctv_2", RootDigest: strings.Repeat("e", 64), FileCount: 1, SizeBytes: 1,
		SourceCommittedAt: params.SourceCommittedAt, VerifiedAt: &params.VerifiedAt, CreatedAt: params.VerifiedAt, UpdatedAt: params.VerifiedAt,
	}).Error)

	mappings, err := store.ListSourceContentMappingsByVersions(t.Context(), revision.WorkspaceID, revision.SourceID, params.ContentID, []string{"ctv_1", "ctv_2"})
	require.NoError(t, err)
	require.Len(t, mappings, 2)
	roles, err := store.ListSourceContentRepositoryRolesByContent(t.Context(), revision.WorkspaceID, revision.KnowledgeBaseID, revision.SourceID, params.ContentID)
	require.NoError(t, err)
	require.ElementsMatch(t, []string{"canonical", "mirror"}, roles)

	versionPlan := sqliteQueryPlanDetails(t, db,
		"SELECT workspace_id, knowledge_base_id, source_id, repository_role, content_id, content_version_id FROM knowledge_base_source_content_mappings WHERE workspace_id = ? AND source_id = ? AND content_id = ? AND content_version_id IN (?, ?) LIMIT 2",
		revision.WorkspaceID, revision.SourceID, params.ContentID, "ctv_1", "ctv_2")
	require.Contains(t, versionPlan, "idx_knowledge_base_source_content_mappings_content_version_id")

	// The lineage-wide ACL lookup must not revisit immutable version history.
	// Seed 100k additional versions under one known role and prove the planner
	// still performs the same seven exact/range covering-index seeks.
	require.NoError(t, db.Exec(`
WITH RECURSIVE history(n) AS (
	SELECT 1
	UNION ALL
	SELECT n + 1 FROM history WHERE n < 100000
)
INSERT INTO knowledge_base_source_content_mappings
	(id, workspace_id, knowledge_base_id, source_id, source_revision_id, repository_role,
	 ags_repository_id, canonical_branch, ags_commit_sha, content_id, content_version_id,
	 root_digest, file_count, size_bytes, source_committed_at, created_at, updated_at)
SELECT printf('map_history_%06d', n), ?, ?, ?, ?, 'canonical',
	printf('repo_history_%06d', n), 'main', ?, ?, printf('ctv_history_%06d', n),
	?, 1, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
FROM history`, revision.WorkspaceID, revision.KnowledgeBaseID, revision.SourceID, revision.ID,
		strings.Repeat("f", 40), params.ContentID, strings.Repeat("e", 64)).Error)
	manyRowRoles, err := store.ListSourceContentRepositoryRolesByContent(t.Context(), revision.WorkspaceID, revision.KnowledgeBaseID, revision.SourceID, params.ContentID)
	require.NoError(t, err)
	require.ElementsMatch(t, []string{"canonical", "mirror"}, manyRowRoles)
	manyRowRolePlan := sqliteQueryPlanDetails(t, db, sourceContentRepositoryRolePresenceQuery,
		sourceContentRepositoryRolePresenceArgs(revision.WorkspaceID, revision.KnowledgeBaseID, revision.SourceID, params.ContentID)...)
	require.Equal(t, oneRowRolePlan, manyRowRolePlan)
	require.Equal(t, 7, strings.Count(manyRowRolePlan, "idx_kb_source_content_mapping_content"), manyRowRolePlan)
}

func sqliteQueryPlanDetails(t *testing.T, db *gorm.DB, query string, args ...any) string {
	t.Helper()
	var rows []struct {
		Detail string
	}
	require.NoError(t, db.Raw("EXPLAIN QUERY PLAN "+query, args...).Scan(&rows).Error)
	details := make([]string, 0, len(rows))
	for _, row := range rows {
		details = append(details, row.Detail)
	}
	return strings.Join(details, "\n")
}

func newSourceContentMappingStore(t *testing.T) (*Store, *gorm.DB, SourceRevision) {
	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(&Source{}, &SourceRevision{}, &SourceContentMapping{}, &SourceSearchDocument{}))
	sqlDB, err := db.DB()
	require.NoError(t, err)
	t.Cleanup(func() { require.NoError(t, sqlDB.Close()) })
	now := time.Date(2026, 8, 21, 5, 0, 0, 0, time.UTC)
	source := Source{ID: "src_1", WorkspaceID: "wsp_1", KnowledgeBaseID: "kb_1", SourceType: "codebase", Status: "ready", Name: "AgentTeam", CreatedBy: "owner", UpdatedBy: "owner", CreatedAt: now, UpdatedAt: now}
	require.NoError(t, db.Create(&source).Error)
	revision := SourceRevision{ID: "rev_1", WorkspaceID: source.WorkspaceID, KnowledgeBaseID: source.KnowledgeBaseID, SourceID: source.ID, SourceCommitSHA: strings.Repeat("a", 40), IsCurrent: true, CreatedAt: now, UpdatedAt: now}
	require.NoError(t, db.Create(&revision).Error)
	return New(db), db, revision
}

func exactSourceContentMappingParams(revision SourceRevision) ProjectSourceContentMappingParams {
	documents := []SourceSearchDocumentInput{
		{Path: "README.md", Title: "README.md", SearchText: "first document"},
		{Path: "docs/second.md", Title: "second.md", SearchText: "second document"},
	}
	return ProjectSourceContentMappingParams{
		ID: "map_1", WorkspaceID: revision.WorkspaceID, KnowledgeBaseID: revision.KnowledgeBaseID,
		SourceID: revision.SourceID, SourceRevisionID: revision.ID, RepositoryRole: "canonical", RepositoryID: "42",
		CanonicalBranch: "main", CommitSHA: revision.SourceCommitSHA, ContentID: "ctn_1", ContentVersionID: "ctv_1",
		RootDigest: strings.Repeat("d", 64), FileCount: 2, SizeBytes: 12,
		SearchProjectionDigest: SourceSearchProjectionDigest(documents, false), SearchDocuments: documents,
		SourceCommittedAt: time.Date(2026, 8, 20, 4, 0, 0, 0, time.UTC), VerifiedAt: time.Date(2026, 8, 21, 5, 30, 0, 0, time.UTC),
	}
}

func sourceMigrationRowCount(t *testing.T, db *gorm.DB, model any) int64 {
	t.Helper()
	var count int64
	require.NoError(t, db.Model(model).Count(&count).Error)
	return count
}
