package roles

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"strings"
	"sync/atomic"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/AgentTeam/internal/contentpayload"
	contentservice "github.com/tidbcloud/AgentTeam/internal/service/content"
	"github.com/tidbcloud/AgentTeam/internal/service/content/migration"
	"github.com/tidbcloud/AgentTeam/internal/service/resource/repos"
	contentstore "github.com/tidbcloud/AgentTeam/internal/store/content"
	migrationstore "github.com/tidbcloud/AgentTeam/internal/store/content/migration"
	rolestore "github.com/tidbcloud/AgentTeam/internal/store/resource/roles"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type fakeRoleMigrationExporter struct {
	exports atomic.Int64
	files   []migration.File
}

func (fake *fakeRoleMigrationExporter) ExportExactRolePackage(context.Context, string, string, string, string, []rolestore.RolePackageManifestItem, contentpayload.Limits) (migration.Export, error) {
	fake.exports.Add(1)
	return migration.Export{Files: append([]migration.File(nil), fake.files...)}, nil
}

func TestExactRoleContentMigrationResumesAtomicProjectionWithoutCutover(t *testing.T) {
	ctx := context.Background()
	db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(
		&contentstore.Authority{}, &contentstore.VersionedContent{},
		&contentstore.ContentVersion{}, &contentstore.VersionOperation{}, &contentstore.VersionPath{},
		&contentstore.VersionPin{}, &contentstore.Outbox{}, &contentstore.StagedPayloadGC{},
		&migrationstore.Mapping{}, &migrationstore.Job{},
		&rolestore.Role{}, &rolestore.RoleVersion{}, &rolestore.RolePackageManifestItem{}, &rolestore.RoleGrant{},
		&rolestore.RoleRealization{}, &rolestore.RoleRealizationItem{}, &rolestore.AgentRoleBinding{}, &rolestore.RoleOperationReceipt{},
	))
	tree, err := contentpayload.NewFilesystemTreeBackend(t.TempDir())
	require.NoError(t, err)
	t.Cleanup(func() { require.NoError(t, tree.Close()) })
	var ids atomic.Int64
	newID := func(prefix string) (string, error) { return fmt.Sprintf("%s_%d", prefix, ids.Add(1)), nil }
	core, err := contentservice.NewTree(contentstore.New(db), tree, newID)
	require.NoError(t, err)
	access, err := NewRoleContentAccessPolicy(roleContentWorkspaceAccess{allowed: map[string]bool{"wsp_1\x00usr_1": true}}, rolestore.New(db))
	require.NoError(t, err)
	authorized, err := contentservice.NewAuthorizedService(core, access)
	require.NoError(t, err)
	worker, err := migration.NewWorker(migrationstore.New(db), authorized, newID, rolePackageContentDomain, "role-migration-test", 3*time.Minute)
	require.NoError(t, err)

	now := time.Date(2026, 8, 21, 5, 0, 0, 0, time.UTC)
	role := rolestore.Role{
		ID: "role_1", WorkspaceID: "wsp_1", OwnerType: "user", OwnerID: "usr_1", Slug: "reviewer", Name: "Reviewer",
		Description: "Review", TagsJSON: "[]", Definition: "review carefully", RuntimeHintsJSON: "{}", AttachmentsJSON: "[]",
		Visibility: rolestore.VisibilityPrivate, ActiveVersionID: "rlv_1", IsActive: true,
		CreatedBy: "usr_1", UpdatedBy: "usr_1", CreatedAt: now, UpdatedAt: now,
	}
	require.NoError(t, db.Create(&role).Error)
	require.NoError(t, db.Create(&rolestore.RoleVersion{
		ID: "rlv_1", RoleID: role.ID, WorkspaceID: role.WorkspaceID, Version: 1, Status: "published",
		Definition: role.Definition, RuntimeHintsJSON: "{}", ProvenanceJSON: "{}", NonPortableSummaryJSON: "[]",
		CreatedBy: "usr_1", CreatedAt: now,
	}).Error)
	legacyRef := "/api/v3/repos/wsp_1/reviewer.rolepkg/contents/IDENTITY.md?ref=" + strings.Repeat("9", 40)
	require.NoError(t, db.Create(&rolestore.RolePackageManifestItem{
		ID: "rpmi_1", RoleVersionID: "rlv_1", RoleID: role.ID, WorkspaceID: role.WorkspaceID,
		ItemType: ManifestItemRoleDefinitionAsset, Name: "IDENTITY.md", Required: true, RealizationAction: "materialize",
		SourceKind: "inline", ContentRef: legacyRef, ContentHash: rolePackageContentHash(role.Definition), SortOrder: 0, MetadataJSON: "{}",
	}).Error)
	require.NoError(t, db.Create(&rolestore.RoleGrant{
		ID: "rgr_1", WorkspaceID: role.WorkspaceID, RoleID: role.ID, SubjectType: "user", SubjectID: "usr_1", Role: roleGrantOwner,
		CreatedBy: "usr_1", UpdatedBy: "usr_1", CreatedAt: now, UpdatedAt: now,
	}).Error)
	authority := contentstore.New(db)
	require.NoError(t, db.Create(&contentstore.Authority{
		ID: "cta_role_1", WorkspaceID: role.WorkspaceID, Domain: rolePackageContentDomain, BindingKey: role.ID,
		State: contentstore.AuthorityStateLegacyActive, Generation: 1, UpdatedBy: "operator", CreatedAt: now, UpdatedAt: now,
	}).Error)
	exporter := &fakeRoleMigrationExporter{files: []migration.File{{Path: roleContentRefDefinition, Data: []byte(role.Definition)}}}
	service := New(rolestore.New(db), Options{
		ContentStore: newTestRolePackageContentStore(), ContentAuthority: authority,
		MigrationWorker: worker, MigrationExporter: exporter,
	})

	var failProjection atomic.Bool
	failProjection.Store(true)
	require.NoError(t, db.Callback().Update().Before("gorm:update").Register("test:role_migration_projection_failure", func(tx *gorm.DB) {
		if tx.Statement != nil && tx.Statement.Schema != nil && tx.Statement.Schema.Table == (rolestore.RolePackageManifestItem{}).TableName() && failProjection.CompareAndSwap(true, false) {
			tx.AddError(errors.New("injected role migration projection failure"))
		}
	}))
	request := RoleContentMigrationRequest{RoleVersionID: "rlv_1", RepositoryID: "42", CommitSHA: strings.Repeat("a", 40)}
	_, err = service.RunExactRoleContentMigration(ctx, role.WorkspaceID, "usr_1", role.ID, request)
	require.ErrorContains(t, err, "injected role migration projection failure")
	require.EqualValues(t, 1, exporter.exports.Load())

	result, err := service.RunExactRoleContentMigration(ctx, role.WorkspaceID, "usr_1", role.ID, request)
	require.NoError(t, err)
	require.EqualValues(t, 1, exporter.exports.Load(), "mapped retry must not read AGS again")
	require.NotEmpty(t, result.ContentID)
	require.NotEmpty(t, result.ContentVersionID)
	var version rolestore.RoleVersion
	require.NoError(t, db.First(&version, "id = ?", "rlv_1").Error)
	require.Equal(t, result.ContentID, version.ContentID)
	require.Equal(t, result.ContentVersionID, version.ContentVersionID)
	var item rolestore.RolePackageManifestItem
	require.NoError(t, db.First(&item, "id = ?", "rpmi_1").Error)
	require.Equal(t, result.ContentID, item.ContentID)
	require.Equal(t, result.ContentVersionID, item.ContentVersionID)
	require.Equal(t, roleContentRefDefinition, item.ContentPath)
	require.Equal(t, nativeRolePackageContentRef(role.WorkspaceID, result.ContentID, result.ContentVersionID, roleContentRefDefinition), item.ContentRef)

	var contentVersions, contentOperations, roleVersions int64
	require.NoError(t, db.Model(&contentstore.ContentVersion{}).Count(&contentVersions).Error)
	require.NoError(t, db.Model(&contentstore.VersionOperation{}).Count(&contentOperations).Error)
	require.NoError(t, db.Model(&rolestore.RoleVersion{}).Count(&roleVersions).Error)
	require.EqualValues(t, 1, contentVersions)
	require.EqualValues(t, 1, contentOperations)
	require.EqualValues(t, 1, roleVersions, "migration must update the existing RoleVersion")
	exact, found, err := authority.GetAuthority(ctx, role.WorkspaceID, rolePackageContentDomain, role.ID)
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, contentstore.AuthorityStateLegacyActive, exact.State)

	replayed, err := service.RunExactRoleContentMigration(ctx, role.WorkspaceID, "usr_1", role.ID, request)
	require.NoError(t, err)
	require.True(t, replayed.Replayed)
	require.Equal(t, result.ContentVersionID, replayed.ContentVersionID)
	require.EqualValues(t, 1, exporter.exports.Load())
}

func TestExportExactRolePackagePinsFinalCommitAndVerifiesRepositoryAndHashes(t *testing.T) {
	commit := strings.Repeat("a", 40)
	definition := "review carefully"
	knowledge := "prefer small diffs"
	requests := make([]string, 0)
	handler := http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
		requests = append(requests, request.URL.RequestURI())
		switch request.URL.Path {
		case "/api/v3/repos/wsp_1/reviewer.rolepkg":
			_ = json.NewEncoder(response).Encode(map[string]any{"id": 42})
		case "/api/v3/repos/wsp_1/reviewer.rolepkg/contents/IDENTITY.md":
			require.Equal(t, commit, request.URL.Query().Get("ref"))
			_ = json.NewEncoder(response).Encode(map[string]any{"type": "file", "encoding": "base64", "content": base64.StdEncoding.EncodeToString([]byte(definition))})
		case "/api/v3/repos/wsp_1/reviewer.rolepkg/contents/.agents/knowledge/rubric.md":
			require.Equal(t, commit, request.URL.Query().Get("ref"))
			_ = json.NewEncoder(response).Encode(map[string]any{"type": "file", "encoding": "base64", "content": base64.StdEncoding.EncodeToString([]byte(knowledge))})
		default:
			http.NotFound(response, request)
		}
	})
	store := AGSHTTPRolePackageContentStore{Handler: handler, AuthorizationHeader: "Bearer test"}
	items := []rolestore.RolePackageManifestItem{
		{ItemType: ManifestItemRoleDefinitionAsset, Name: "IDENTITY.md", ContentRef: "/api/v3/repos/wsp_1/reviewer.rolepkg/contents/IDENTITY.md?ref=" + strings.Repeat("1", 40), ContentHash: rolePackageContentHash(definition)},
		{ItemType: AttachmentPortableKnowledge, Name: "Rubric", ContentRef: "/api/v3/repos/wsp_1/reviewer.rolepkg/contents/.agents/knowledge/rubric.md?ref=" + strings.Repeat("2", 40), ContentHash: rolePackageContentHash(knowledge)},
		{ItemType: AttachmentKnowledgeBase, Name: "Handbook", SourceResourceID: "kb_1"},
	}
	limits := contentpayload.Limits{MaxFiles: 10, MaxFileBytes: 1024, MaxTotalBytes: 4096, MaxManifestBytes: 4096}

	exported, err := store.ExportExactRolePackage(context.Background(), "wsp_1", "role_1", "42", commit, items, limits)
	require.NoError(t, err)
	require.Equal(t, []string{roleContentRefDefinition, ".agents/knowledge/rubric.md"}, []string{exported.Files[0].Path, exported.Files[1].Path})
	require.Len(t, requests, 3, "one exact repository inspection plus one read per portable item")

	_, err = store.ExportExactRolePackage(context.Background(), "wsp_1", "role_1", "43", commit, items, limits)
	require.ErrorIs(t, err, repos.ErrInvalidArgument)
	items[0].ContentHash = strings.Repeat("0", 64)
	_, err = store.ExportExactRolePackage(context.Background(), "wsp_1", "role_1", "42", commit, items, limits)
	require.ErrorContains(t, err, "source hash mismatch")
}

func TestExportExactRolePackageRejectsOversizedResponseBeforeDecode(t *testing.T) {
	commit := strings.Repeat("a", 40)
	handler := http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
		switch request.URL.Path {
		case "/api/v3/repos/wsp_1/reviewer.rolepkg":
			_ = json.NewEncoder(response).Encode(map[string]any{"id": 42})
		case "/api/v3/repos/wsp_1/reviewer.rolepkg/contents/IDENTITY.md":
			_ = json.NewEncoder(response).Encode(map[string]any{
				"type": "file", "encoding": "base64", "content": base64.StdEncoding.EncodeToString([]byte(strings.Repeat("x", 1<<20))),
			})
		default:
			http.NotFound(response, request)
		}
	})
	store := AGSHTTPRolePackageContentStore{Handler: handler}
	_, err := store.ExportExactRolePackage(context.Background(), "wsp_1", "role_1", "42", commit, []rolestore.RolePackageManifestItem{{
		ItemType: ManifestItemRoleDefinitionAsset, Name: "IDENTITY.md",
		ContentRef:  "/api/v3/repos/wsp_1/reviewer.rolepkg/contents/IDENTITY.md?ref=" + commit,
		ContentHash: rolePackageContentHash("expected"),
	}}, contentpayload.Limits{MaxFiles: 1, MaxFileBytes: 8, MaxTotalBytes: 8, MaxManifestBytes: 1024})
	require.ErrorContains(t, err, "response exceeds byte limit")
}

func TestDisabledRoleContentMigrationHasNoJobOrAuthoritySideEffects(t *testing.T) {
	db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(&migrationstore.Job{}, &contentstore.Authority{}))
	service := New(rolestore.New(db), Options{})
	_, err = service.RunExactRoleContentMigration(context.Background(), "wsp_1", "usr_1", "role_1", RoleContentMigrationRequest{
		RoleVersionID: "rlv_1", RepositoryID: "42", CommitSHA: strings.Repeat("a", 40),
	})
	require.ErrorIs(t, err, repos.ErrInvalidArgument)
	var jobs, authorities int64
	require.NoError(t, db.Model(&migrationstore.Job{}).Count(&jobs).Error)
	require.NoError(t, db.Model(&contentstore.Authority{}).Count(&authorities).Error)
	require.Zero(t, jobs)
	require.Zero(t, authorities)
}

func TestRoleContentMigrationRejectsManifestLookaheadBeforeExport(t *testing.T) {
	db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
	require.NoError(t, err)
	require.NoError(t, db.AutoMigrate(&rolestore.Role{}, &rolestore.RoleVersion{}, &rolestore.RolePackageManifestItem{}, &rolestore.RoleGrant{}))
	now := time.Date(2026, 8, 21, 10, 0, 0, 0, time.UTC)
	role := rolestore.Role{
		ID: "role_large", WorkspaceID: "wsp_1", OwnerType: "user", OwnerID: "usr_1", Slug: "large", Name: "Large",
		Visibility: rolestore.VisibilityPrivate, ActiveVersionID: "rlv_large", IsActive: true,
		TagsJSON: "[]", RuntimeHintsJSON: "{}", AttachmentsJSON: "[]", CreatedBy: "usr_1", UpdatedBy: "usr_1", CreatedAt: now, UpdatedAt: now,
	}
	require.NoError(t, db.Create(&role).Error)
	require.NoError(t, db.Create(&rolestore.RoleVersion{
		ID: role.ActiveVersionID, RoleID: role.ID, WorkspaceID: role.WorkspaceID, Version: 1, Status: "published",
		RuntimeHintsJSON: "{}", ProvenanceJSON: "{}", NonPortableSummaryJSON: "[]", CreatedBy: "usr_1", CreatedAt: now,
	}).Error)
	require.NoError(t, db.Create(&rolestore.RoleGrant{
		ID: "rgr_large", WorkspaceID: role.WorkspaceID, RoleID: role.ID, SubjectType: "user", SubjectID: "usr_1", Role: roleGrantOwner,
		CreatedBy: "usr_1", UpdatedBy: "usr_1", CreatedAt: now, UpdatedAt: now,
	}).Error)
	items := make([]rolestore.RolePackageManifestItem, 0, rolestore.MaxRolePackageManifestItems+1)
	for index := 0; index <= rolestore.MaxRolePackageManifestItems; index++ {
		items = append(items, rolestore.RolePackageManifestItem{
			ID: fmt.Sprintf("rpmi_%03d", index), RoleVersionID: role.ActiveVersionID, RoleID: role.ID, WorkspaceID: role.WorkspaceID,
			ItemType: AttachmentPortableKnowledge, Name: fmt.Sprintf("item-%03d", index), SortOrder: index, MetadataJSON: "{}",
		})
	}
	require.NoError(t, db.Create(&items).Error)
	service := New(rolestore.New(db), Options{})
	_, _, err = service.loadRoleMigrationTarget(context.Background(), role.WorkspaceID, "usr_1", role.ID, role.ActiveVersionID)
	require.ErrorIs(t, err, repos.ErrInvalidArgument)
	require.ErrorContains(t, err, "exceeds item limit")
}
