package system

import (
	"context"
	"errors"
	"testing"

	"kra/internal/biz"
	"kra/internal/conf"

	"gorm.io/gorm"
)

func authorityUintPointer(value uint) *uint { return &value }

func enableStrictAuthorityTestMode(data *Data) {
	currentData, _ := data.runtime.Values()
	data.runtime.Replace(currentData, &conf.AdminBackend{System: &conf.AdminBackend_System{UseStrictAuth: true}})
}

func TestCopyAuthorityStrictPolicyValidationMatchesAdministrationContract(t *testing.T) {
	data := newTransactionTestData(t)
	enableStrictAuthorityTestMode(data)
	db := data.gormDB.WithContext(context.Background())
	rootID := uint(888)
	if err := db.Create(&[]authorityPO{
		{AuthorityID: rootID, AuthorityName: "root", ParentID: authorityUintPointer(0)},
		{AuthorityID: 900, AuthorityName: "source", ParentID: &rootID},
		{AuthorityID: 901, AuthorityName: "stale-source", ParentID: &rootID},
	}).Error; err != nil {
		t.Fatal(err)
	}
	if err := db.Create(&apiPO{Path: "/registered", Method: "GET"}).Error; err != nil {
		t.Fatal(err)
	}
	if err := db.Create(&[]casbinRulePO{
		newPolicyRule(900, "/registered", "GET"),
		newPolicyRule(901, "/stale", "POST"),
		// A root role is allowed to grant registered APIs without already
		// holding the policy, but a stale policy is still not a registered API.
		newPolicyRule(rootID, "/stale", "POST"),
	}).Error; err != nil {
		t.Fatal(err)
	}

	repo := &authorityAccessRepo{data: data}
	ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: rootID})
	created := &biz.Authority{AuthorityID: 910, AuthorityName: "copy", ParentID: &rootID}
	if err := repo.CopyAuthority(ctx, 900, created); err != nil {
		t.Fatalf("root copy of a registered API failed: %v", err)
	}
	if exists, err := policyExists(db, created.AuthorityID, "/registered", "GET"); err != nil || !exists {
		t.Fatalf("copied policy exists = %v, err = %v", exists, err)
	}

	staleCopy := &biz.Authority{AuthorityID: 911, AuthorityName: "stale-copy", ParentID: &rootID}
	if err := repo.CopyAuthority(ctx, 901, staleCopy); err == nil || err.Error() != "存在api不在权限列表中" {
		t.Fatalf("stale API copy error = %v", err)
	}
	var count int64
	if err := db.Unscoped().Model(&authorityPO{}).Where("authority_id = ?", staleCopy.AuthorityID).Count(&count).Error; err != nil {
		t.Fatal(err)
	}
	if count != 0 {
		t.Fatalf("failed copy persisted target authority: count=%d", count)
	}
}

func TestCopyAuthorityDuplicateIDWinsOverStrictParentValidation(t *testing.T) {
	data := newTransactionTestData(t)
	enableStrictAuthorityTestMode(data)
	db := data.gormDB.WithContext(context.Background())
	rootID := uint(888)
	if err := db.Create(&[]authorityPO{
		{AuthorityID: rootID, AuthorityName: "root", ParentID: authorityUintPointer(0)},
		{AuthorityID: 920, AuthorityName: "existing", ParentID: &rootID},
	}).Error; err != nil {
		t.Fatal(err)
	}
	invalidParent := uint(999999)
	ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: rootID})
	err := (&authorityAccessRepo{data: data}).CopyAuthority(ctx, 0, &biz.Authority{AuthorityID: 920, AuthorityName: "duplicate", ParentID: &invalidParent})
	if err == nil || err.Error() != "存在相同角色id" {
		t.Fatalf("duplicate copy error = %v", err)
	}
}

func TestListAuthoritiesRequiresCurrentAuthorityOutsideStrictMode(t *testing.T) {
	data := newTransactionTestData(t)
	ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: 999999})
	if _, err := (&authorityAccessRepo{data: data}).ListAuthorities(ctx); !errors.Is(err, gorm.ErrRecordNotFound) {
		t.Fatalf("missing current authority error = %v", err)
	}
}

func TestDeleteAuthorityIgnoresOrphanedUserAssociation(t *testing.T) {
	data := newTransactionTestData(t)
	ctx := context.Background()
	db := data.gormDB.WithContext(ctx)
	if err := db.Create(&authorityPO{AuthorityID: 930, AuthorityName: "orphan-links"}).Error; err != nil {
		t.Fatal(err)
	}
	if err := db.Create(&userAuthorityPO{SysUserID: 999999, SysAuthorityAuthorityID: 930}).Error; err != nil {
		t.Fatal(err)
	}
	if err := (&authorityAccessRepo{data: data}).DeleteAuthority(ctx, 930); err != nil {
		t.Fatalf("delete with orphaned user association: %v", err)
	}
	var authority authorityPO
	if err := db.Unscoped().Where("authority_id = ?", 930).First(&authority).Error; !errors.Is(err, gorm.ErrRecordNotFound) {
		t.Fatalf("deleted authority lookup error = %v", err)
	}
	var links int64
	if err := db.Model(&userAuthorityPO{}).Where("sys_authority_authority_id = ?", 930).Count(&links).Error; err != nil {
		t.Fatal(err)
	}
	if links != 0 {
		t.Fatalf("orphaned user associations were not removed: %d", links)
	}
}

func TestSetAuthorityUsersKeepsPrimaryRoleFallbackBehavior(t *testing.T) {
	data := newTransactionTestData(t)
	ctx := context.Background()
	db := data.gormDB.WithContext(ctx)
	if err := db.Create(&[]authorityPO{{AuthorityID: 940, AuthorityName: "removed"}, {AuthorityID: 941, AuthorityName: "fallback"}}).Error; err != nil {
		t.Fatal(err)
	}
	users := []userPO{
		{Username: "with-fallback", Password: "hash", AuthorityID: 940, Enable: 1},
		{Username: "without-fallback", Password: "hash", AuthorityID: 940, Enable: 1},
	}
	if err := db.Create(&users).Error; err != nil {
		t.Fatal(err)
	}
	if err := db.Create(&[]userAuthorityPO{
		{SysUserID: users[0].ID, SysAuthorityAuthorityID: 940},
		{SysUserID: users[0].ID, SysAuthorityAuthorityID: 941},
		{SysUserID: users[1].ID, SysAuthorityAuthorityID: 940},
	}).Error; err != nil {
		t.Fatal(err)
	}
	if err := (&authorityAccessRepo{data: data}).SetAuthorityUsers(ctx, 940, nil); err != nil {
		t.Fatal(err)
	}
	var persisted []userPO
	if err := db.Where("id IN ?", []uint{users[0].ID, users[1].ID}).Order("id").Find(&persisted).Error; err != nil {
		t.Fatal(err)
	}
	if len(persisted) != 2 || persisted[0].AuthorityID != 941 || persisted[1].AuthorityID != 940 {
		t.Fatalf("primary authority fallback = %+v", persisted)
	}
}

func TestAuthorityCustomDataScopeUsesCompatibleColumns(t *testing.T) {
	data := newTransactionTestData(t)
	ctx := context.Background()
	repo := &authorityAccessRepo{data: data}
	if err := repo.SetDataScope(ctx, 950, 5, []uint{7, 7, 8}); err != nil {
		t.Fatal(err)
	}
	ids, err := repo.DataScopeDepartmentIDs(ctx, 950)
	if err != nil {
		t.Fatal(err)
	}
	if len(ids) != 3 || ids[0] != 7 || ids[1] != 7 || ids[2] != 8 {
		t.Fatalf("custom department IDs = %v", ids)
	}
	if err = repo.SetDataScope(ctx, 950, 3, []uint{9}); err != nil {
		t.Fatal(err)
	}
	ids, err = repo.DataScopeDepartmentIDs(ctx, 950)
	if err != nil {
		t.Fatal(err)
	}
	if len(ids) != 0 {
		t.Fatalf("non-custom scope retained departments: %v", ids)
	}
}

func TestSetAuthorityUsersStrictRejectsUsersOutsideManagedRoles(t *testing.T) {
	data := newTransactionTestData(t)
	enableStrictAuthorityTestMode(data)
	db := data.gormDB.WithContext(context.Background())
	rootID, actorID, childID, siblingID := uint(888), uint(1000), uint(1001), uint(2000)
	if err := db.Create(&[]authorityPO{
		{AuthorityID: rootID, ParentID: authorityUintPointer(0)},
		{AuthorityID: actorID, ParentID: &rootID},
		{AuthorityID: childID, ParentID: &actorID},
		{AuthorityID: siblingID, ParentID: &rootID},
	}).Error; err != nil {
		t.Fatal(err)
	}
	users := []userPO{
		{Username: "managed", Password: "hash", AuthorityID: childID, Enable: 1},
		{Username: "outside", Password: "hash", AuthorityID: siblingID, Enable: 1},
	}
	if err := db.Create(&users).Error; err != nil {
		t.Fatal(err)
	}
	ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: actorID})
	repo := &authorityAccessRepo{data: data}
	if err := repo.SetAuthorityUsers(ctx, childID, []uint{users[1].ID}); err == nil {
		t.Fatal("SetAuthorityUsers() accepted a user outside the managed role tree")
	}
	if err := repo.SetAuthorityUsers(ctx, childID, []uint{users[0].ID}); err != nil {
		t.Fatalf("SetAuthorityUsers() rejected a managed user: %v", err)
	}
}

func TestSetAuthorityUsersStrictRejectsMixedRoleUserAlreadyLinked(t *testing.T) {
	data := newTransactionTestData(t)
	enableStrictAuthorityTestMode(data)
	db := data.gormDB.WithContext(context.Background())
	rootID, actorID, childID, siblingID := uint(888), uint(1100), uint(1101), uint(2100)
	if err := db.Create(&[]authorityPO{
		{AuthorityID: rootID, ParentID: authorityUintPointer(0)},
		{AuthorityID: actorID, ParentID: &rootID},
		{AuthorityID: childID, ParentID: &actorID},
		{AuthorityID: siblingID, ParentID: &rootID},
	}).Error; err != nil {
		t.Fatal(err)
	}
	user := userPO{Username: "mixed", Password: "hash", AuthorityID: childID, Enable: 1}
	if err := db.Create(&user).Error; err != nil {
		t.Fatal(err)
	}
	if err := db.Create(&[]userAuthorityPO{
		{SysUserID: user.ID, SysAuthorityAuthorityID: childID},
		{SysUserID: user.ID, SysAuthorityAuthorityID: siblingID},
	}).Error; err != nil {
		t.Fatal(err)
	}
	ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: actorID})
	if err := (&authorityAccessRepo{data: data}).SetAuthorityUsers(ctx, childID, nil); err == nil {
		t.Fatal("SetAuthorityUsers() modified a linked user that also has an out-of-scope role")
	}
	var count int64
	if err := db.Model(&userAuthorityPO{}).Where("sys_user_id = ? AND sys_authority_authority_id = ?", user.ID, childID).Count(&count).Error; err != nil {
		t.Fatal(err)
	}
	if count != 1 {
		t.Fatalf("rejected update changed the existing target-role link: count=%d", count)
	}
}

func TestUpdateAuthorityStrictRejectsHierarchyCycle(t *testing.T) {
	data := newTransactionTestData(t)
	enableStrictAuthorityTestMode(data)
	db := data.gormDB.WithContext(context.Background())
	rootID, actorID, targetID, childID := uint(888), uint(1200), uint(1201), uint(1202)
	if err := db.Create(&[]authorityPO{
		{AuthorityID: rootID, ParentID: authorityUintPointer(0)},
		{AuthorityID: actorID, ParentID: &rootID},
		{AuthorityID: targetID, AuthorityName: "target", ParentID: &actorID},
		{AuthorityID: childID, ParentID: &targetID},
	}).Error; err != nil {
		t.Fatal(err)
	}
	ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: actorID})
	err := (&authorityAccessRepo{data: data}).UpdateAuthority(ctx, &biz.Authority{AuthorityID: targetID, AuthorityName: "target", ParentID: &childID})
	if err == nil {
		t.Fatal("UpdateAuthority() accepted a parent that forms a cycle")
	}
	var stored authorityPO
	if err := db.Where("authority_id = ?", targetID).First(&stored).Error; err != nil {
		t.Fatal(err)
	}
	if stored.ParentID == nil || *stored.ParentID != actorID {
		t.Fatalf("rejected update changed parent_id: %+v", stored.ParentID)
	}
}

func TestSetDataScopeValidatesScopeAndStrictDepartments(t *testing.T) {
	data := newTransactionTestData(t)
	enableStrictAuthorityTestMode(data)
	db := data.gormDB.WithContext(context.Background())
	rootID, actorID, childID := uint(888), uint(1300), uint(1301)
	if err := db.Create(&[]authorityPO{
		{AuthorityID: rootID, ParentID: authorityUintPointer(0)},
		{AuthorityID: actorID, ParentID: &rootID},
		{AuthorityID: childID, ParentID: &actorID},
	}).Error; err != nil {
		t.Fatal(err)
	}
	ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: actorID})
	repo := &authorityAccessRepo{data: data}
	if err := repo.SetDataScope(ctx, childID, 0, nil); !errors.Is(err, errInvalidDataScope) {
		t.Fatalf("SetDataScope() invalid scope error = %v", err)
	}
	if err := repo.SetDataScope(ctx, childID, 5, []uint{999999}); err == nil {
		t.Fatal("SetDataScope() accepted a missing department in strict mode")
	}
	department := departmentPO{Name: "managed"}
	if err := db.Create(&department).Error; err != nil {
		t.Fatal(err)
	}
	if err := repo.SetDataScope(ctx, childID, 5, []uint{department.ID}); err != nil {
		t.Fatalf("SetDataScope() rejected an existing department: %v", err)
	}
}

func TestSetDataScopeStrictRejectsDepartmentsOutsideActorScope(t *testing.T) {
	data := newTransactionTestData(t)
	enableStrictAuthorityTestMode(data)
	db := data.gormDB.WithContext(context.Background())
	rootID, actorID, childID := uint(888), uint(1350), uint(1351)
	if err := db.Create(&[]authorityPO{
		{AuthorityID: rootID, ParentID: authorityUintPointer(0)},
		{AuthorityID: actorID, ParentID: &rootID, DataScope: 5},
		{AuthorityID: childID, ParentID: &actorID},
	}).Error; err != nil {
		t.Fatal(err)
	}
	departments := []departmentPO{{Name: "allowed"}, {Name: "outside"}}
	if err := db.Create(&departments).Error; err != nil {
		t.Fatal(err)
	}
	actorUser := userPO{Username: "scope-actor", Password: "hash", AuthorityID: actorID, DeptID: departments[0].ID, Enable: 1}
	if err := db.Create(&actorUser).Error; err != nil {
		t.Fatal(err)
	}
	if err := db.Create(&authorityDepartmentPO{AuthorityID: actorID, DepartmentID: departments[0].ID}).Error; err != nil {
		t.Fatal(err)
	}
	ctx := biz.NewActorContext(context.Background(), biz.Actor{UserID: actorUser.ID, AuthorityID: actorID})
	repo := &authorityAccessRepo{data: data}
	if err := repo.SetDataScope(ctx, childID, 5, []uint{departments[1].ID}); err == nil {
		t.Fatal("SetDataScope() accepted a department outside the actor's data scope")
	}
	if err := repo.SetDataScope(ctx, childID, 5, []uint{departments[0].ID}); err != nil {
		t.Fatalf("SetDataScope() rejected a department inside the actor's data scope: %v", err)
	}
}

func TestStrictDataScopeGrantRejectsBroaderChildScopes(t *testing.T) {
	data := newTransactionTestData(t)
	enableStrictAuthorityTestMode(data)
	db := data.gormDB.WithContext(context.Background())
	rootID, actorID, sourceID, targetID := uint(888), uint(1370), uint(1371), uint(1372)
	if err := db.Create(&[]authorityPO{
		{AuthorityID: rootID, ParentID: authorityUintPointer(0), DataScope: 1},
		{AuthorityID: actorID, ParentID: &rootID, DataScope: 3},
		{AuthorityID: sourceID, ParentID: &actorID, DataScope: 4},
		{AuthorityID: targetID, AuthorityName: "target", ParentID: &actorID, DataScope: 4},
	}).Error; err != nil {
		t.Fatal(err)
	}
	actorUser := userPO{Username: "limited-scope-actor", Password: "hash", AuthorityID: actorID, Enable: 1}
	if err := db.Create(&actorUser).Error; err != nil {
		t.Fatal(err)
	}
	ctx := biz.NewActorContext(context.Background(), biz.Actor{UserID: actorUser.ID, AuthorityID: actorID})
	repo := &authorityAccessRepo{data: data}

	created := &biz.Authority{AuthorityID: 1373, AuthorityName: "created", ParentID: &actorID}
	if err := repo.CreateAuthority(ctx, created); !errors.Is(err, errInvalidDataScope) {
		t.Fatalf("CreateAuthority() broader scope error = %v", err)
	}
	if created.DataScope != 1 {
		t.Fatalf("CreateAuthority() did not normalize zero scope before validation: %d", created.DataScope)
	}
	copied := &biz.Authority{AuthorityID: 1374, AuthorityName: "copied", ParentID: &actorID}
	if err := repo.CopyAuthority(ctx, sourceID, copied); !errors.Is(err, errInvalidDataScope) {
		t.Fatalf("CopyAuthority() broader scope error = %v", err)
	}
	if copied.DataScope != 1 {
		t.Fatalf("CopyAuthority() did not normalize zero scope before validation: %d", copied.DataScope)
	}
	if err := repo.UpdateAuthority(ctx, &biz.Authority{AuthorityID: targetID, AuthorityName: "target", ParentID: &actorID, DataScope: 2}); !errors.Is(err, errInvalidDataScope) {
		t.Fatalf("UpdateAuthority() broader scope error = %v", err)
	}
	if err := repo.SetDataScope(ctx, targetID, 1, nil); !errors.Is(err, errInvalidDataScope) {
		t.Fatalf("SetDataScope() broader scope error = %v", err)
	}

	allowed := &biz.Authority{AuthorityID: 1375, AuthorityName: "self-only", ParentID: &actorID, DataScope: 4}
	if err := repo.CreateAuthority(ctx, allowed); err != nil {
		t.Fatalf("CreateAuthority() rejected a narrower scope: %v", err)
	}
	var stored authorityPO
	if err := db.Where("authority_id = ?", targetID).First(&stored).Error; err != nil {
		t.Fatal(err)
	}
	if stored.DataScope != 4 {
		t.Fatalf("rejected grants changed target data scope to %d", stored.DataScope)
	}
}

func TestCreateAuthorityStrictDefaultsStayWithinActorPermissions(t *testing.T) {
	data := newTransactionTestData(t)
	enableStrictAuthorityTestMode(data)
	db := data.gormDB.WithContext(context.Background())
	rootID, actorID := uint(888), uint(1400)
	if err := db.Create(&[]authorityPO{
		{AuthorityID: rootID, ParentID: authorityUintPointer(0)},
		{AuthorityID: actorID, ParentID: &rootID},
	}).Error; err != nil {
		t.Fatal(err)
	}
	dashboard := menuPO{Name: "dashboard", Path: "dashboard"}
	if err := db.Create(&dashboard).Error; err != nil {
		t.Fatal(err)
	}
	if err := db.Create(&apiPO{Path: "/menu/getMenu", Method: "POST"}).Error; err != nil {
		t.Fatal(err)
	}
	ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: actorID})
	repo := &authorityAccessRepo{data: data}
	first := &biz.Authority{AuthorityID: 1401, AuthorityName: "no-defaults", ParentID: &actorID}
	if err := repo.CreateAuthority(ctx, first); err != nil {
		t.Fatal(err)
	}
	var menuLinks int64
	if err := db.Model(&authorityMenuPO{}).Where("sys_authority_authority_id = ?", first.AuthorityID).Count(&menuLinks).Error; err != nil {
		t.Fatal(err)
	}
	if menuLinks != 0 {
		t.Fatalf("new child inherited an unowned default menu: count=%d", menuLinks)
	}
	if exists, err := policyExists(db, first.AuthorityID, "/menu/getMenu", "POST"); err != nil || exists {
		t.Fatalf("new child inherited an unowned default API: exists=%v err=%v", exists, err)
	}

	if err := db.Create(&authorityMenuPO{SysAuthorityAuthorityID: actorID, SysBaseMenuID: dashboard.ID}).Error; err != nil {
		t.Fatal(err)
	}
	policy := newPolicyRule(actorID, "/menu/getMenu", "POST")
	if err := db.Create(&policy).Error; err != nil {
		t.Fatal(err)
	}
	second := &biz.Authority{AuthorityID: 1402, AuthorityName: "owned-defaults", ParentID: &actorID}
	if err := repo.CreateAuthority(ctx, second); err != nil {
		t.Fatal(err)
	}
	if err := db.Model(&authorityMenuPO{}).Where("sys_authority_authority_id = ? AND sys_base_menu_id = ?", second.AuthorityID, dashboard.ID).Count(&menuLinks).Error; err != nil {
		t.Fatal(err)
	}
	if menuLinks != 1 {
		t.Fatalf("new child did not inherit the owned dashboard menu: count=%d", menuLinks)
	}
	if exists, err := policyExists(db, second.AuthorityID, "/menu/getMenu", "POST"); err != nil || !exists {
		t.Fatalf("new child owned default API exists=%v err=%v", exists, err)
	}
}

func TestCopyAuthorityStrictValidatesCopiedMenusAndButtons(t *testing.T) {
	data := newTransactionTestData(t)
	enableStrictAuthorityTestMode(data)
	db := data.gormDB.WithContext(context.Background())
	rootID, actorID, sourceID := uint(888), uint(1500), uint(1501)
	if err := db.Create(&[]authorityPO{
		{AuthorityID: rootID, ParentID: authorityUintPointer(0)},
		{AuthorityID: actorID, ParentID: &rootID},
		{AuthorityID: sourceID, ParentID: &actorID},
	}).Error; err != nil {
		t.Fatal(err)
	}
	if err := db.Create(&[]menuPO{{ID: 10, Name: "owned"}, {ID: 11, Name: "unowned"}}).Error; err != nil {
		t.Fatal(err)
	}
	if err := db.Create(&[]menuButtonPO{{ID: 31, MenuID: 10, Name: "owned"}, {ID: 32, MenuID: 10, Name: "unowned"}}).Error; err != nil {
		t.Fatal(err)
	}
	if err := db.Create(&[]authorityMenuPO{
		{SysAuthorityAuthorityID: actorID, SysBaseMenuID: 10},
		{SysAuthorityAuthorityID: sourceID, SysBaseMenuID: 10},
		{SysAuthorityAuthorityID: sourceID, SysBaseMenuID: 11},
	}).Error; err != nil {
		t.Fatal(err)
	}
	if err := db.Create(&[]authorityButtonPO{
		{AuthorityID: actorID, MenuID: 10, ButtonID: 31},
		{AuthorityID: sourceID, MenuID: 10, ButtonID: 31},
		{AuthorityID: sourceID, MenuID: 10, ButtonID: 32},
	}).Error; err != nil {
		t.Fatal(err)
	}
	ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: actorID})
	repo := &authorityAccessRepo{data: data}
	if err := repo.CopyAuthority(ctx, sourceID, &biz.Authority{AuthorityID: 1510, AuthorityName: "menu-fail", ParentID: &actorID}); err == nil {
		t.Fatal("CopyAuthority() copied a menu not assigned to the actor")
	}
	if err := db.Where("sys_authority_authority_id = ? AND sys_base_menu_id = ?", sourceID, 11).Delete(&authorityMenuPO{}).Error; err != nil {
		t.Fatal(err)
	}
	if err := repo.CopyAuthority(ctx, sourceID, &biz.Authority{AuthorityID: 1511, AuthorityName: "button-fail", ParentID: &actorID}); err == nil {
		t.Fatal("CopyAuthority() copied a button not assigned to the actor")
	}
	if err := db.Where("authority_id = ? AND sys_base_menu_btn_id = ?", sourceID, 32).Delete(&authorityButtonPO{}).Error; err != nil {
		t.Fatal(err)
	}
	if err := repo.CopyAuthority(ctx, sourceID, &biz.Authority{AuthorityID: 1512, AuthorityName: "valid", ParentID: &actorID}); err != nil {
		t.Fatalf("CopyAuthority() rejected owned permissions: %v", err)
	}
	for _, failedID := range []uint{1510, 1511} {
		var count int64
		if err := db.Unscoped().Model(&authorityPO{}).Where("authority_id = ?", failedID).Count(&count).Error; err != nil {
			t.Fatal(err)
		}
		if count != 0 {
			t.Fatalf("failed copy persisted authority %d", failedID)
		}
	}
}

func TestAuthorityCreateAndCopyNormalizeZeroDataScope(t *testing.T) {
	data := newTransactionTestData(t)
	db := data.gormDB.WithContext(context.Background())
	repo := &authorityAccessRepo{data: data}

	created := &biz.Authority{AuthorityID: 1600, AuthorityName: "created"}
	if err := repo.CreateAuthority(context.Background(), created); err != nil {
		t.Fatal(err)
	}
	if created.DataScope != 1 {
		t.Fatalf("created data scope = %d, want 1", created.DataScope)
	}

	if err := db.Create(&authorityPO{AuthorityID: 1601, AuthorityName: "source", DataScope: 3}).Error; err != nil {
		t.Fatal(err)
	}
	copied := &biz.Authority{AuthorityID: 1602, AuthorityName: "copied"}
	if err := repo.CopyAuthority(context.Background(), 1601, copied); err != nil {
		t.Fatal(err)
	}
	if copied.DataScope != 1 {
		t.Fatalf("copied data scope = %d, want 1", copied.DataScope)
	}

	var stored []authorityPO
	if err := db.Where("authority_id IN ?", []uint{created.AuthorityID, copied.AuthorityID}).Order("authority_id").Find(&stored).Error; err != nil {
		t.Fatal(err)
	}
	if len(stored) != 2 || stored[0].DataScope != 1 || stored[1].DataScope != 1 {
		t.Fatalf("stored normalized data scopes = %+v", stored)
	}
}

func TestAuthorityMutationsRejectInvalidDataScope(t *testing.T) {
	data := newTransactionTestData(t)
	db := data.gormDB.WithContext(context.Background())
	repo := &authorityAccessRepo{data: data}
	if err := db.Create(&[]authorityPO{
		{AuthorityID: 1700, AuthorityName: "source", DataScope: 3},
		{AuthorityID: 1701, AuthorityName: "target", DataScope: 3},
	}).Error; err != nil {
		t.Fatal(err)
	}

	for _, scope := range []int{-1, 6} {
		if err := repo.CreateAuthority(context.Background(), &biz.Authority{AuthorityID: uint(1800 + scope + 1), AuthorityName: "invalid-create", DataScope: scope}); !errors.Is(err, errInvalidDataScope) {
			t.Fatalf("CreateAuthority() scope %d error = %v", scope, err)
		}
		if err := repo.CopyAuthority(context.Background(), 1700, &biz.Authority{AuthorityID: uint(1900 + scope + 1), AuthorityName: "invalid-copy", DataScope: scope}); !errors.Is(err, errInvalidDataScope) {
			t.Fatalf("CopyAuthority() scope %d error = %v", scope, err)
		}
		if err := repo.UpdateAuthority(context.Background(), &biz.Authority{AuthorityID: 1701, AuthorityName: "invalid-update", DataScope: scope}); !errors.Is(err, errInvalidDataScope) {
			t.Fatalf("UpdateAuthority() scope %d error = %v", scope, err)
		}
	}
}

func TestUpdateAuthorityZeroDataScopeKeepsStoredValue(t *testing.T) {
	data := newTransactionTestData(t)
	db := data.gormDB.WithContext(context.Background())
	if err := db.Create(&authorityPO{AuthorityID: 2000, AuthorityName: "before", DataScope: 3}).Error; err != nil {
		t.Fatal(err)
	}

	if err := (&authorityAccessRepo{data: data}).UpdateAuthority(context.Background(), &biz.Authority{AuthorityID: 2000, AuthorityName: "after", DataScope: 0}); err != nil {
		t.Fatal(err)
	}
	var stored authorityPO
	if err := db.Where("authority_id = ?", 2000).First(&stored).Error; err != nil {
		t.Fatal(err)
	}
	if stored.DataScope != 3 || stored.AuthorityName != "after" {
		t.Fatalf("updated authority = %+v, want data scope 3 and name after", stored)
	}
}
