package roles

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/AgentTeam/internal/service/kb"
	"github.com/tidbcloud/AgentTeam/internal/service/resource/repos"
	"github.com/tidbcloud/AgentTeam/internal/service/skills"
	rolesstore "github.com/tidbcloud/AgentTeam/internal/store/resource/roles"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type fakeSkillBinder struct {
	calls []skillBindCall
}

type skillBindCall struct {
	workspaceID string
	subject     string
	agentID     string
	skillID     string
	input       skills.AgentSkillBindingMutation
}

func (f *fakeSkillBinder) BindAgentSkill(ctx context.Context, workspaceID, subject, agentID, skillID string, input skills.AgentSkillBindingMutation) (skills.AgentSkillBindingResult, error) {
	f.calls = append(f.calls, skillBindCall{workspaceID: workspaceID, subject: subject, agentID: agentID, skillID: skillID, input: input})
	return skills.AgentSkillBindingResult{AgentID: agentID, SkillID: skillID, Branch: firstNonEmpty(input.Branch, input.LegacyBranchHint, "main")}, nil
}

type fakeKBGrantor struct {
	calls []kbGrantCall
}

type kbGrantCall struct {
	workspaceID string
	subject     string
	agentID     string
	kbID        string
	accessLevel string
}

func (f *fakeKBGrantor) GrantAgentKnowledgeBaseAccess(ctx context.Context, workspaceID, subject, agentID, kbID, accessLevel string) (kb.AgentKnowledgeBaseGrantResult, error) {
	f.calls = append(f.calls, kbGrantCall{workspaceID: workspaceID, subject: subject, agentID: agentID, kbID: kbID, accessLevel: accessLevel})
	return kb.AgentKnowledgeBaseGrantResult{WorkspaceID: workspaceID, AgentID: agentID, KnowledgeBaseID: kbID, AccessLevel: "read"}, nil
}

type testRolePackageContentStore struct {
	contents map[string]string
}

type unavailableRolePackageContentStore struct{}

func (unavailableRolePackageContentStore) StoreRolePackage(context.Context, StoreRolePackageInput) (StoreRolePackageOutput, error) {
	return StoreRolePackageOutput{}, rolePackageDependencyError(errors.New("store unavailable"))
}

func (unavailableRolePackageContentStore) ResolveRolePackage(context.Context, ResolveRolePackageInput) (string, error) {
	return "", rolePackageDependencyError(errors.New(`resolve role package content: status 404: {"message":"Not Found","documentation_url":"https://docs.github.com/rest"}`))
}

func newTestRolePackageContentStore() *testRolePackageContentStore {
	return &testRolePackageContentStore{contents: map[string]string{}}
}

func (s *testRolePackageContentStore) StoreRolePackage(_ context.Context, input StoreRolePackageInput) (StoreRolePackageOutput, error) {
	assets := make(map[string]RolePackageStoredAsset, len(input.Assets))
	for _, asset := range input.Assets {
		assetPath, err := rolePackageAssetPath(asset)
		if err != nil {
			return StoreRolePackageOutput{}, err
		}
		content := strings.TrimSpace(asset.Content)
		if content == "" {
			return StoreRolePackageOutput{}, errRolePackageContentRequired()
		}
		ref := "ags://test/" + input.WorkspaceID + "/" + input.RoleID + "/" + input.VersionID + "/" + assetPath
		s.contents[ref] = content
		assets[assetPath] = RolePackageStoredAsset{Path: assetPath, ContentRef: ref, ContentHash: testSHA256Hex(content)}
	}
	return StoreRolePackageOutput{Assets: assets}, nil
}

func (s *testRolePackageContentStore) ResolveRolePackage(_ context.Context, input ResolveRolePackageInput) (string, error) {
	content, ok := s.contents[strings.TrimSpace(input.ContentRef)]
	if !ok {
		return "", errRolePackageContentRequired()
	}
	return content, nil
}

func testSHA256Hex(content string) string {
	sum := sha256.Sum256([]byte(content))
	return hex.EncodeToString(sum[:])
}

func newRoleServiceForTest(t *testing.T, options Options) *Service {
	t.Helper()
	db, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), "roles.db")), &gorm.Config{})
	require.NoError(t, err)
	sqlDB, err := db.DB()
	require.NoError(t, err)
	t.Cleanup(func() { require.NoError(t, sqlDB.Close()) })
	require.NoError(t, db.AutoMigrate(&rolesstore.Role{}, &rolesstore.RoleVersion{}, &rolesstore.RolePackageManifestItem{}, &rolesstore.RoleGrant{}, &rolesstore.RoleRealization{}, &rolesstore.RoleRealizationItem{}, &rolesstore.AgentRoleBinding{}, &rolesstore.RoleOperationReceipt{}))
	if options.Now == nil {
		fixed := time.Date(2026, 6, 24, 1, 2, 3, 0, time.UTC)
		options.Now = func() time.Time { return fixed }
	}
	if options.ContentStore == nil {
		options.ContentStore = newTestRolePackageContentStore()
	}
	return New(rolesstore.New(db), options)
}

func TestCapabilitiesDistinguishRoleLiteFromPackageContent(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{ContentStore: FailClosedRolePackageContentStore{}})

	capabilities, err := svc.Capabilities(context.Background(), "wsp_1", "usr_1")
	require.NoError(t, err)
	require.True(t, capabilities.Enabled)
	require.True(t, capabilities.PresetRoles)
	require.True(t, capabilities.MyRoles)
	require.True(t, capabilities.Realization)
	require.False(t, capabilities.RolePackage)
	require.Contains(t, capabilities.RolePackageDisabledReason, "content storage")
}

func TestCapabilitiesExposeRolePackageWhenContentStoreConfigured(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})

	capabilities, err := svc.Capabilities(context.Background(), "wsp_1", "usr_1")
	require.NoError(t, err)
	require.True(t, capabilities.RolePackage)
	require.Empty(t, capabilities.RolePackageDisabledReason)
}

func TestPresetRolesSelectLocalizedTemplate(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})

	zhPresets, err := svc.ListPresetRoles(context.Background(), "wsp_1", "usr_1", "zh-CN")
	require.NoError(t, err)
	require.NotEmpty(t, zhPresets)
	require.Equal(t, "预设角色", presetLabel(zhPresets[0].Source))
	require.Equal(t, "zh-CN", zhPresets[0].Locale)

	enPreset, err := svc.GetPresetRole(context.Background(), "wsp_1", "usr_1", "research-agent", "en-US")
	require.NoError(t, err)
	require.Equal(t, "Research Assistant", enPreset.Name)
	require.Equal(t, SourcePresetRole, enPreset.Source)
	require.Equal(t, "research-agent", enPreset.SourcePresetID)

	zhPreset, err := svc.GetPresetRole(context.Background(), "wsp_1", "usr_1", "research-agent", "zh-CN")
	require.NoError(t, err)
	require.Len(t, zhPreset.RolePackageManifest.Items, 1)
	require.Equal(t, ManifestItemRoleDefinitionAsset, zhPreset.RolePackageManifest.Items[0].Type)
}

func TestPresetRolesIncludeBuiltinCatalog(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})

	presets, err := svc.ListPresetRoles(context.Background(), "wsp_1", "usr_1", "zh-CN")
	require.NoError(t, err)
	require.Len(t, presets, 22)

	ids := make(map[string]bool, len(presets))
	for _, preset := range presets {
		ids[preset.SourcePresetID] = true
	}
	for _, id := range []string{
		"general-purpose",
		"backend-developer",
		"code-reviewer",
		"debugger",
		"devops-engineer",
		"frontend-developer",
		"qa-engineer",
		"testing-assistant",
		"security-auditor",
	} {
		require.True(t, ids[id], "preset catalog missing built-in role %q", id)
	}

	enPresets, err := svc.ListPresetRoles(context.Background(), "wsp_1", "usr_1", "en-US")
	require.NoError(t, err)
	require.Len(t, enPresets, 22)
}

func TestPresetRolesExposeTestingAssistantThroughService(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})

	preset, err := svc.GetPresetRole(context.Background(), "wsp_1", "usr_1", "testing-assistant", "zh-CN")
	require.NoError(t, err)
	require.Equal(t, "测试助手", preset.Name)
	require.Equal(t, "testing", preset.Scenario)
	require.Contains(t, preset.Tags, "testing")
	require.Equal(t, SourcePresetRole, preset.Source)
	require.Equal(t, "testing-assistant", preset.SourcePresetID)
	require.Len(t, preset.RolePackageManifest.Items, 1)
	require.Equal(t, ManifestItemRoleDefinitionAsset, preset.RolePackageManifest.Items[0].Type)
}

func TestPresetRolesIncludeCommonCompanyRoleCatalog(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})

	presets, err := svc.ListPresetRoles(context.Background(), "wsp_1", "usr_1", "zh-CN")
	require.NoError(t, err)

	ids := make(map[string]bool, len(presets))
	for _, preset := range presets {
		ids[preset.SourcePresetID] = true
	}
	for _, id := range []string{
		"customer-support-specialist",
		"data-analyst",
		"finance-analyst",
		"legal-compliance-advisor",
		"marketing-strategist",
		"operations-manager",
		"people-ops-partner",
		"product-manager",
		"project-coordinator",
		"sales-solutions-consultant",
		"technical-writer",
		"ux-designer",
	} {
		require.True(t, ids[id], "preset catalog missing common company role %q", id)
	}

	preset, err := svc.GetPresetRole(context.Background(), "wsp_1", "usr_1", "technical-writer", "en-US")
	require.NoError(t, err)
	require.Equal(t, "Technical Writer", preset.Name)
	require.Equal(t, "technical-writing", preset.Scenario)
	require.Equal(t, []string{"documentation", "technical-writing", "writing"}, preset.Tags)

	for _, id := range []string{
		"customer-support-specialist",
		"data-analyst",
		"finance-analyst",
		"legal-compliance-advisor",
		"marketing-strategist",
		"operations-manager",
		"people-ops-partner",
		"product-manager",
		"project-coordinator",
		"sales-solutions-consultant",
		"technical-writer",
		"ux-designer",
	} {
		preset, err := svc.GetPresetRole(context.Background(), "wsp_1", "usr_1", id, "en-US")
		require.NoError(t, err)
		require.Contains(t, preset.Definition, "## Working Mode", "preset %q should include a full IDENTITY.md working contract", id)
		require.Contains(t, preset.Definition, "## Focus On", "preset %q should include a full IDENTITY.md focus section", id)
		require.Contains(t, preset.Definition, "## Quality Checks", "preset %q should include a full IDENTITY.md quality section", id)
		require.Contains(t, preset.Definition, "## Return", "preset %q should include a full IDENTITY.md return contract", id)
	}
}

func TestPresetRoleDefinitionsAreFullWorkingContracts(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})

	for _, locale := range []string{"zh-CN", "en-US"} {
		t.Run(locale, func(t *testing.T) {
			presets, err := svc.ListPresetRoles(context.Background(), "wsp_1", "usr_1", locale)
			require.NoError(t, err)
			require.NotEmpty(t, presets)

			for _, preset := range presets {
				definition := strings.TrimSpace(preset.Definition)
				require.GreaterOrEqual(t, len(strings.Split(definition, "\n")), 20, "preset %q locale %q should not be a one-line role definition", preset.SourcePresetID, locale)
				if locale == "zh-CN" {
					require.Contains(t, definition, "## 工作模式", "preset %q locale %q should include a working mode section", preset.SourcePresetID, locale)
					require.Contains(t, definition, "## 重点关注", "preset %q locale %q should include a focus section", preset.SourcePresetID, locale)
					require.Contains(t, definition, "## 质量检查", "preset %q locale %q should include a quality section", preset.SourcePresetID, locale)
					require.Contains(t, definition, "## 返回内容", "preset %q locale %q should include a return contract", preset.SourcePresetID, locale)
					continue
				}
				require.Contains(t, definition, "## Working Mode", "preset %q locale %q should include a working mode section", preset.SourcePresetID, locale)
				require.Contains(t, definition, "## Focus On", "preset %q locale %q should include a focus section", preset.SourcePresetID, locale)
				require.Contains(t, definition, "## Quality Checks", "preset %q locale %q should include a quality section", preset.SourcePresetID, locale)
				require.Contains(t, definition, "## Return", "preset %q locale %q should include a return contract", preset.SourcePresetID, locale)
			}
		})
	}
}

func TestPresetRolesKeepBuiltinDefinitionStructure(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})

	for _, id := range []string{
		"general-purpose",
		"backend-developer",
		"code-reviewer",
		"debugger",
		"devops-engineer",
		"frontend-developer",
		"qa-engineer",
		"security-auditor",
	} {
		preset, err := svc.GetPresetRole(context.Background(), "wsp_1", "usr_1", id, "en-US")
		require.NoError(t, err)

		definition := strings.TrimSpace(preset.Definition)
		require.Contains(t, definition, "# "+preset.Name, "preset %q should keep the built-in Role title", id)
		require.Contains(t, definition, "## Working Mode", "preset %q should keep the built-in Role working mode", id)
		require.Contains(t, definition, "## Focus On", "preset %q should keep the built-in Role focus section", id)
		require.Contains(t, definition, "## Quality Checks", "preset %q should keep the built-in Role quality checks", id)
		require.Contains(t, definition, "## Return", "preset %q should keep the built-in Role return contract", id)
	}

	backend, err := svc.GetPresetRole(context.Background(), "wsp_1", "usr_1", "backend-developer", "en-US")
	require.NoError(t, err)
	require.Contains(t, backend.Definition, "Own backend changes as production behavior")
	require.Contains(t, backend.Definition, "## Implementation Checks")

	general, err := svc.GetPresetRole(context.Background(), "wsp_1", "usr_1", "general-purpose", "en-US")
	require.NoError(t, err)
	require.Contains(t, general.Definition, "Own one complete product path")
	require.Contains(t, general.Definition, "## Integration Checks")
}

func TestAddPresetToMyRolesCreatesEditableUserRole(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})

	role, err := svc.AddPresetToMyRoles(context.Background(), "wsp_1", "usr_1", AddPresetRequest{PresetID: "research-agent", Locale: "zh-CN", DisplayName: "我的研究助手"})
	require.NoError(t, err)
	require.Equal(t, SourceMyRole, role.Source)
	require.Equal(t, "我的研究助手", role.Name)
	require.Equal(t, "research-agent", role.SourcePresetID)
	require.Equal(t, 1, role.SourcePresetVersion)
	require.Equal(t, roleGrantOwner, role.Role)
	require.True(t, role.CanManage)
	require.NotEmpty(t, role.RolePackageManifest.Items)
}

func TestCreateRoleAvoidsSoftDeletedSlug(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})
	ctx := context.Background()
	input := Mutation{Name: "后端开发助手", Definition: "backend role instructions"}

	first, err := svc.CreateRole(ctx, "wsp_1", "usr_1", input)
	require.NoError(t, err)
	second, err := svc.CreateRole(ctx, "wsp_1", "usr_1", input)
	require.NoError(t, err)
	require.NoError(t, svc.DeleteRole(ctx, "wsp_1", "usr_1", second.ID))

	third, err := svc.CreateRole(ctx, "wsp_1", "usr_1", input)
	require.NoError(t, err)
	require.NotEqual(t, first.Slug, third.Slug)
	require.NotEqual(t, second.Slug, third.Slug)
}

func TestDeleteRoleHidesUserRole(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})

	role, err := svc.CreateRole(context.Background(), "wsp_1", "usr_1", Mutation{
		Name:       "Temporary role",
		Definition: "temporary role instructions",
	})
	require.NoError(t, err)

	require.NoError(t, svc.DeleteRole(context.Background(), "wsp_1", "usr_1", role.ID))

	_, err = svc.GetRole(context.Background(), "wsp_1", "usr_1", role.ID)
	require.ErrorIs(t, err, repos.ErrNotFound)

	page, err := svc.ListRoles(context.Background(), "wsp_1", "usr_1", RoleListViewAll, "", 1, 20)
	require.NoError(t, err)
	for _, item := range page.Items {
		require.NotEqual(t, role.ID, item.ID)
	}
}

func TestWorkspaceSharedRoleIsDiscoverableUsableAndReadOnlyForMembers(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})
	ctx := context.Background()
	role, err := svc.CreateRole(ctx, "wsp_1", "usr_owner", Mutation{
		Name:       "Shared reviewer",
		Definition: "review production changes",
	})
	require.NoError(t, err)
	require.Equal(t, RoleVisibilityPrivate, role.Visibility)

	shared, err := svc.UpdateRoleVisibility(ctx, "wsp_1", "usr_owner", role.ID, RoleVisibilityWorkspace)
	require.NoError(t, err)
	require.Equal(t, RoleVisibilityWorkspace, shared.Visibility)
	shared, err = svc.UpdateRole(ctx, "wsp_1", "usr_owner", role.ID, Mutation{
		Name:       "Shared senior reviewer",
		Definition: "review production changes carefully",
	})
	require.NoError(t, err)
	require.Equal(t, RoleVisibilityWorkspace, shared.Visibility, "content updates must preserve workspace visibility")

	page, err := svc.ListRoles(ctx, "wsp_1", "usr_member", RoleListViewWorkspace, "", 1, 20)
	require.NoError(t, err)
	require.Len(t, page.Items, 1)
	require.Equal(t, role.ID, page.Items[0].ID)
	require.Equal(t, roleGrantView, page.Items[0].Role)
	require.False(t, page.Items[0].CanManage)
	require.False(t, page.Items[0].Editable)

	opened, err := svc.GetRole(ctx, "wsp_1", "usr_member", role.ID)
	require.NoError(t, err)
	require.Equal(t, roleGrantView, opened.Role)
	require.Equal(t, RoleVisibilityWorkspace, opened.Visibility)
	require.False(t, opened.CanManage)
	require.False(t, opened.Editable)

	result, err := svc.RealizeRoleForAgent(ctx, "wsp_1", "usr_member", role.ID, RealizationRequest{AgentID: "agt_1", DryRun: true})
	require.NoError(t, err)
	require.Equal(t, role.ID, result.RoleID)

	_, err = svc.UpdateRole(ctx, "wsp_1", "usr_member", role.ID, Mutation{Name: "Changed", Definition: "changed"})
	require.ErrorIs(t, err, repos.ErrForbidden)
	_, err = svc.UpdateRoleVisibility(ctx, "wsp_1", "usr_member", role.ID, RoleVisibilityPrivate)
	require.ErrorIs(t, err, repos.ErrForbidden)
	require.ErrorIs(t, svc.DeleteRole(ctx, "wsp_1", "usr_member", role.ID), repos.ErrForbidden)
}

func TestUpdateRoleVisibilityDoesNotPublishVersion(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})
	ctx := context.Background()
	role, err := svc.CreateRole(ctx, "wsp_1", "usr_owner", Mutation{Name: "Reviewer", Definition: "review"})
	require.NoError(t, err)

	_, err = svc.UpdateRoleVisibility(ctx, "wsp_1", "usr_owner", role.ID, RoleVisibilityWorkspace)
	require.NoError(t, err)
	updated, err := svc.UpdateRoleVisibility(ctx, "wsp_1", "usr_owner", role.ID, RoleVisibilityPrivate)
	require.NoError(t, err)
	require.Equal(t, RoleVisibilityPrivate, updated.Visibility)

	var rows []rolesstore.RoleVersion
	require.NoError(t, svc.store.DB().Where("role_id = ?", role.ID).Order("version ASC").Find(&rows).Error)
	require.Len(t, rows, 1)
	require.Equal(t, 1, rows[0].Version)
}

func TestCreateRoleFromAgentCreatesOwnedAgentDerivedRole(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})

	role, err := svc.CreateRoleFromAgent(context.Background(), "wsp_1", "usr_1", AgentDerivedMutation{
		AgentID: "agt_source",
		Mutation: Mutation{
			Name:       "Trained reviewer",
			Definition: "review production Go changes",
			Attachments: []Attachment{
				{Type: AttachmentPortableKnowledge, Name: "Review rubric", Content: "prefer small diffs", Required: true},
				{Type: AttachmentSkill, Name: "Review skill", ResourceID: "skill_1", Required: true},
				{Type: AttachmentKnowledgeBase, Name: "Runbooks", ResourceID: "kb_1", Required: false},
				{Type: AttachmentMemory, Name: "Memory seed", Realization: RealizationCreate, Required: false},
			},
		},
	})
	require.NoError(t, err)
	require.Equal(t, SourceAgentDerivedRole, role.Source)
	require.Equal(t, "agt_source", role.SourceAgentID)
	require.Equal(t, "usr_1", role.OwnerID)
	require.True(t, role.Editable)
	require.True(t, role.CanManage)
	require.Equal(t, roleGrantOwner, role.Role)
	require.Equal(t, "agent", role.Provenance.Kind)
	require.Equal(t, "agt_source", role.Provenance.Ref)
	require.True(t, role.RolePackage.Portable)
	require.Equal(t, []string{AttachmentRoleDefinition, AttachmentPortableKnowledge, AttachmentSkill, AttachmentKnowledgeBase}, role.RolePackage.Sections)
	require.Empty(t, role.RolePackage.NonPortable)

	stored, err := svc.GetRole(context.Background(), "wsp_1", "usr_1", role.ID)
	require.NoError(t, err)
	require.Equal(t, SourceAgentDerivedRole, stored.Source)
	require.Equal(t, "agt_source", stored.SourceAgentID)
	require.Equal(t, "agt_source", stored.Provenance.Ref)
}

func TestCreateRoleFromAgentRejectsNonPortableAttachments(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})

	_, err := svc.CreateRoleFromAgent(context.Background(), "wsp_1", "usr_1", AgentDerivedMutation{
		AgentID: "agt_source",
		Mutation: Mutation{
			Name:       "Unsafe role",
			Definition: "copy too much",
			Attachments: []Attachment{
				{Type: "message_history", Name: "raw transcript", Content: "private chat"},
			},
		},
	})
	require.ErrorIs(t, err, repos.ErrInvalidArgument)
	require.Contains(t, err.Error(), `role package item type "message_history" is not portable`)
}

func TestCreateRoleFromAgentInvalidInputIncludesReason(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})

	tests := []struct {
		name    string
		input   AgentDerivedMutation
		wantErr string
	}{
		{
			name: "source agent",
			input: AgentDerivedMutation{
				Mutation: Mutation{Name: "Reviewer", Definition: "review"},
			},
			wantErr: "source agent id is required",
		},
		{
			name: "role name",
			input: AgentDerivedMutation{
				AgentID:  "agt_source",
				Mutation: Mutation{Definition: "review"},
			},
			wantErr: "role name is required",
		},
		{
			name: "role definition",
			input: AgentDerivedMutation{
				AgentID:  "agt_source",
				Mutation: Mutation{Name: "Reviewer"},
			},
			wantErr: "role definition is required",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			_, err := svc.CreateRoleFromAgent(context.Background(), "wsp_1", "usr_1", tt.input)
			require.ErrorIs(t, err, repos.ErrInvalidArgument)
			require.Contains(t, err.Error(), tt.wantErr)
		})
	}
}

func TestUpdateRoleFromAgentKeepsUserOwnedTargetAndSourceAgent(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})
	role, err := svc.CreateRoleFromAgent(context.Background(), "wsp_1", "usr_1", AgentDerivedMutation{
		AgentID:  "agt_first",
		Mutation: Mutation{Name: "Reviewer", Definition: "review"},
	})
	require.NoError(t, err)

	updated, err := svc.UpdateRoleFromAgent(context.Background(), "wsp_1", "usr_1", role.ID, AgentDerivedMutation{
		AgentID: "agt_second",
		Mutation: Mutation{
			Name:       "Reviewer v2",
			Definition: "review and summarize",
			Attachments: []Attachment{
				{Type: AttachmentPortableKnowledge, Name: "Lessons", Content: "be precise"},
			},
		},
	})
	require.NoError(t, err)
	require.Equal(t, role.ID, updated.ID)
	require.Equal(t, SourceAgentDerivedRole, updated.Source)
	require.Equal(t, "agt_second", updated.SourceAgentID)
	require.Equal(t, "usr_1", updated.OwnerID)
	require.True(t, updated.Editable)
	require.True(t, updated.CanManage)
	require.Equal(t, "Reviewer v2", updated.Name)
	require.Equal(t, []string{AttachmentRoleDefinition, AttachmentPortableKnowledge}, updated.RolePackage.Sections)
}

func TestRolePackageSectionsForPresetAndMyRole(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})

	preset, err := svc.GetPresetRole(context.Background(), "wsp_1", "usr_1", "research-agent", "en-US")
	require.NoError(t, err)
	require.Equal(t, SourcePresetRole, preset.Source)
	require.False(t, preset.Editable)
	require.False(t, preset.CanManage)
	require.Equal(t, "preset", preset.Provenance.Kind)
	require.True(t, preset.RolePackage.Portable)
	require.Contains(t, preset.RolePackage.Sections, AttachmentRoleDefinition)
	require.NotContains(t, preset.RolePackage.Sections, AttachmentMemory)

	role, err := svc.AddPresetToMyRoles(context.Background(), "wsp_1", "usr_1", AddPresetRequest{PresetID: "research-agent", Locale: "en-US"})
	require.NoError(t, err)
	require.Equal(t, SourceMyRole, role.Source)
	require.True(t, role.Editable)
	require.True(t, role.CanManage)
	require.Equal(t, "usr_1", role.OwnerID)
	require.Equal(t, "preset", role.Provenance.Kind)
	require.Equal(t, "research-agent", role.Provenance.Ref)
	require.Equal(t, preset.RolePackage.Sections, role.RolePackage.Sections)
}

func TestRealizeRoleDelegatesTypedAttachments(t *testing.T) {
	skills := &fakeSkillBinder{}
	kb := &fakeKBGrantor{}
	svc := newRoleServiceForTest(t, Options{Skills: skills, KB: kb})
	role, err := svc.CreateRole(context.Background(), "wsp_1", "usr_1", Mutation{
		Name:       "Builder",
		Definition: "build",
		Attachments: []Attachment{
			{Type: AttachmentSkill, Name: "Reviewer", ResourceID: "skill_1", Branch: "release", Required: true},
			{Type: AttachmentKnowledgeBase, Name: "Runbooks", ResourceID: "kb_1", AccessLevel: "view", Required: true},
			{Type: AttachmentMemory, Name: "Role memory", Required: true, Realization: RealizationCreate},
		},
	})
	require.NoError(t, err)

	result, err := svc.RealizeRoleForAgent(context.Background(), "wsp_1", "usr_1", role.ID, RealizationRequest{AgentID: "agt_1", Apply: true, StorageScope: "workspace", TemplateLocale: "zh-CN", Actor: repos.ResourceRepositoryActor{Provider: "agentteam", Subject: "usr_1", Login: "alice"}})
	require.NoError(t, err)
	require.False(t, result.DryRun)
	require.Len(t, result.Items, 3)
	require.Len(t, skills.calls, 1)
	require.Equal(t, "skill_1", skills.calls[0].skillID)
	require.Empty(t, skills.calls[0].input.Branch)
	require.Equal(t, "release", skills.calls[0].input.LegacyBranchHint)
	require.Len(t, kb.calls, 1)
	require.Equal(t, "kb_1", kb.calls[0].kbID)
	require.Equal(t, "view", kb.calls[0].accessLevel)
}

func TestRealizeRoleMaterializesPortableSkillWithoutBinding(t *testing.T) {
	skills := &fakeSkillBinder{}
	svc := newRoleServiceForTest(t, Options{Skills: skills})
	role, err := svc.CreateRole(context.Background(), "wsp_1", "usr_1", Mutation{
		Name:       "Reviewer",
		Definition: "review changes",
		Attachments: []Attachment{
			{Type: AttachmentSkill, Name: "code-review", Content: "# Code review", Required: false},
		},
	})
	require.NoError(t, err)

	var manifestItem rolesstore.RolePackageManifestItem
	require.NoError(t, svc.store.DB().Where("role_id = ? AND item_type = ?", role.ID, AttachmentSkill).First(&manifestItem).Error)
	require.Equal(t, RealizationCopy, manifestItem.RealizationAction)
	require.Equal(t, "template", manifestItem.SourceKind)
	require.Empty(t, manifestItem.SourceResourceID)
	require.Empty(t, manifestItem.Content)
	require.NotEmpty(t, manifestItem.ContentRef)

	dryRun, err := svc.RealizeRoleForAgent(context.Background(), "wsp_1", "usr_1", role.ID, RealizationRequest{AgentID: "agt_1", DryRun: true})
	require.NoError(t, err)
	require.Equal(t, RealizationCopy, dryRun.Items[1].Action)
	require.Equal(t, RealizationStatusPlanned, dryRun.Items[1].Status)
	require.Equal(t, "portable skill will be materialized on apply", dryRun.Items[1].Message)

	result, err := svc.RealizeRoleForAgent(context.Background(), "wsp_1", "usr_1", role.ID, RealizationRequest{AgentID: "agt_1", Apply: true})
	require.NoError(t, err)
	require.Len(t, result.Items, 2)
	require.Empty(t, skills.calls)
	require.Equal(t, AttachmentSkill, result.Items[1].ManifestItemType)
	require.Equal(t, RealizationCopy, result.Items[1].Action)
	require.Equal(t, RealizationStatusRealized, result.Items[1].Status)
	require.Equal(t, "portable skill is materialized by role package", result.Items[1].Message)
}

func TestRoleLegacyAttachmentsJSONOmitsPortableSkillContent(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})
	largeSkill := strings.Repeat("portable skill\n", 6000)
	role, err := svc.CreateRole(context.Background(), "wsp_1", "usr_1", Mutation{
		Name:       "Coordinator",
		Definition: "coordinate team work",
		Attachments: []Attachment{
			{Type: AttachmentSkill, Name: "Portable", Content: largeSkill, Required: false},
		},
	})
	require.NoError(t, err)

	var storedRole rolesstore.Role
	require.NoError(t, svc.store.DB().Where("workspace_id = ? AND id = ?", "wsp_1", role.ID).First(&storedRole).Error)
	require.Contains(t, storedRole.AttachmentsJSON, `"type":"skill"`)
	require.NotContains(t, storedRole.AttachmentsJSON, `"content"`)
	require.NotContains(t, storedRole.AttachmentsJSON, "portable skill")

	var manifestItem rolesstore.RolePackageManifestItem
	require.NoError(t, svc.store.DB().Where("role_id = ? AND item_type = ?", role.ID, AttachmentSkill).First(&manifestItem).Error)
	require.NotEmpty(t, manifestItem.ContentRef)

	content, err := svc.contentStore.ResolveRolePackage(context.Background(), ResolveRolePackageInput{ContentRef: manifestItem.ContentRef})
	require.NoError(t, err)
	require.Equal(t, strings.TrimSpace(largeSkill), content)
}

func TestRealizeRoleDependencyMessageHidesPackageContentDetails(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})
	definition := strings.Repeat("review\n", 80)
	role, err := svc.CreateRole(context.Background(), "wsp_1", "usr_1", Mutation{
		Name:       "Reviewer",
		Definition: definition,
	})
	require.NoError(t, err)
	require.NoError(t, svc.store.DB().
		Model(&rolesstore.RoleVersion{}).
		Where("id = ?", role.RoleVersionID).
		Update("definition", roleDefinitionSummary(definition)).
		Error)

	svc.contentStore = unavailableRolePackageContentStore{}
	_, err = svc.RealizeRoleForAgent(context.Background(), "wsp_1", "usr_1", role.ID, RealizationRequest{AgentID: "agt_1", Apply: true})

	require.Error(t, err)
	require.True(t, IsRoleDependencyUnavailable(err), "RealizeRoleForAgent error = %v", err)
	message := RoleDependencyUnavailableMessage(err)
	require.Contains(t, message, "role content is temporarily unavailable")
	require.NotContains(t, message, "documentation_url")
	require.NotContains(t, message, "github.com")
	require.NotContains(t, message, "resolve role package content")
}

func TestRealizeRoleRecoversPackageContentFromVersionBranch(t *testing.T) {
	store := newTestRolePackageContentStore()
	svc := newRoleServiceForTest(t, Options{ContentStore: store})
	role, err := svc.CreateRole(context.Background(), "wsp_1", "usr_1", Mutation{
		Name:       "Reviewer",
		Definition: "review",
	})
	require.NoError(t, err)

	brokenRef := "http://ags.test/api/v3/repos/wsp_1/reviewer.rolepkg/contents/IDENTITY.md?ref=stale-commit"
	fallbackRef := rolePackageBranchContentRef(brokenRef, role.RoleVersionID)
	require.NotEmpty(t, fallbackRef)
	store.contents[fallbackRef] = "review"
	require.NoError(t, svc.store.DB().
		Model(&rolesstore.RolePackageManifestItem{}).
		Where("role_version_id = ? AND item_type = ?", role.RoleVersionID, ManifestItemRoleDefinitionAsset).
		Updates(map[string]any{"content_ref": brokenRef, "content_hash": rolePackageContentHash("review")}).
		Error)

	result, err := svc.RealizeRoleForAgent(context.Background(), "wsp_1", "usr_1", role.ID, RealizationRequest{AgentID: "agt_1", Apply: true})

	require.NoError(t, err)
	require.Len(t, result.Items, 1)
	require.Equal(t, RealizationStatusRealized, result.Items[0].Status)
}

func TestRealizeRoleRecoversRoleDefinitionFromPublishedVersion(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})
	fullDefinition := strings.Repeat("full role definition\n", 30)
	role, err := svc.CreateRole(context.Background(), "wsp_1", "usr_1", Mutation{
		Name:       "Full role",
		Definition: fullDefinition,
		Attachments: []Attachment{
			{Type: AttachmentSkill, Name: "Optional skill", Content: "# optional skill", Required: false},
		},
	})
	require.NoError(t, err)

	var stored rolesstore.Role
	require.NoError(t, svc.store.DB().Where("id = ?", role.ID).First(&stored).Error)
	require.Less(t, len(stored.Definition), len(fullDefinition))
	var version rolesstore.RoleVersion
	require.NoError(t, svc.store.DB().Where("id = ?", role.RoleVersionID).First(&version).Error)
	require.Equal(t, strings.TrimSpace(fullDefinition), version.Definition)

	svc.contentStore = unavailableRolePackageContentStore{}
	result, err := svc.RealizeRoleForAgent(context.Background(), "wsp_1", "usr_1", role.ID, RealizationRequest{AgentID: "agt_1", Apply: true})
	require.NoError(t, err)
	require.Len(t, result.Items, 2)
	require.Equal(t, RealizationStatusRealized, result.Items[0].Status)
	require.Equal(t, RealizationStatusUnresolved, result.Items[1].Status)

	got, err := svc.GetRole(context.Background(), "wsp_1", "usr_1", role.ID)
	require.NoError(t, err)
	require.Equal(t, strings.TrimSpace(fullDefinition), got.Definition)
	require.Equal(t, strings.TrimSpace(fullDefinition), got.RolePackageManifest.Items[0].Content)
	require.Empty(t, got.RolePackageManifest.Items[1].Content)
}

func TestRealizeRoleRejectsMissingRequiredContentWhenOnlySummaryCanRecover(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})
	fullDefinition := strings.Repeat("full role definition\n", 30)
	role, err := svc.CreateRole(context.Background(), "wsp_1", "usr_1", Mutation{
		Name:       "Broken role",
		Definition: fullDefinition,
	})
	require.NoError(t, err)

	summary := roleDefinitionSummary(fullDefinition)
	require.NoError(t, svc.store.DB().
		Model(&rolesstore.RoleVersion{}).
		Where("id = ?", role.RoleVersionID).
		Update("definition", summary).
		Error)
	svc.contentStore = unavailableRolePackageContentStore{}

	_, err = svc.RealizeRoleForAgent(context.Background(), "wsp_1", "usr_1", role.ID, RealizationRequest{AgentID: "agt_1", Apply: true})

	require.Error(t, err)
	require.True(t, IsRoleDependencyUnavailable(err), "RealizeRoleForAgent error = %v", err)
	require.Contains(t, RoleDependencyUnavailableMessage(err), "role content is temporarily unavailable")
}

func TestRealizeRoleRecoversPresetRoleDefinitionWhenPackageMissing(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})
	role, err := svc.AddPresetToMyRoles(context.Background(), "wsp_1", "usr_1", AddPresetRequest{PresetID: "backend-developer", Locale: "zh-CN"})
	require.NoError(t, err)
	preset, err := svc.GetPresetRole(context.Background(), "wsp_1", "usr_1", "backend-developer", "zh-CN")
	require.NoError(t, err)
	require.NoError(t, svc.store.DB().
		Model(&rolesstore.RoleVersion{}).
		Where("id = ?", role.RoleVersionID).
		Update("definition", roleDefinitionSummary(preset.Definition)).
		Error)
	svc.contentStore = unavailableRolePackageContentStore{}

	result, err := svc.RealizeRoleForAgent(context.Background(), "wsp_1", "usr_1", role.ID, RealizationRequest{AgentID: "agt_1", Apply: true})

	require.NoError(t, err)
	require.Len(t, result.Items, 1)
	require.Equal(t, RealizationStatusRealized, result.Items[0].Status)
	got, err := svc.GetRole(context.Background(), "wsp_1", "usr_1", role.ID)
	require.NoError(t, err)
	require.Equal(t, preset.Definition, got.Definition)
	require.Equal(t, preset.Definition, got.RolePackageManifest.Items[0].Content)
}

func TestRealizeRoleIgnoresLegacyMemoryAttachment(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})
	role, err := svc.CreateRole(context.Background(), "wsp_1", "usr_1", Mutation{
		Name:       "Forker",
		Definition: "fork",
		Attachments: []Attachment{
			{Type: AttachmentMemory, Name: "Source memory", ResourceID: "mem_source", Required: true},
		},
	})
	require.NoError(t, err)

	result, err := svc.RealizeRoleForAgent(context.Background(), "wsp_1", "usr_1", role.ID, RealizationRequest{AgentID: "agt_1", DryRun: true})
	require.NoError(t, err)
	require.True(t, result.DryRun)
	require.Len(t, result.Items, 1)
	require.Equal(t, ManifestItemRoleDefinitionAsset, result.Items[0].ManifestItemType)
}

func TestRealizeRoleFailsClosedForRequiredUnresolvedAttachment(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})
	role, err := svc.CreateRole(context.Background(), "wsp_1", "usr_1", Mutation{
		Name:       "Requires KB",
		Definition: "needs kb",
		Attachments: []Attachment{
			{Type: AttachmentKnowledgeBase, Name: "Private KB", Required: true},
		},
	})
	require.NoError(t, err)

	_, err = svc.RealizeRoleForAgent(context.Background(), "wsp_1", "usr_1", role.ID, RealizationRequest{AgentID: "agt_1", Apply: true})
	require.ErrorIs(t, err, repos.ErrInvalidArgument)
}

func TestGetRoleRejectsUnrelatedUsers(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})
	role, err := svc.CreateRole(context.Background(), "wsp_1", "owner", Mutation{Name: "Private Role", Definition: "private"})
	require.NoError(t, err)

	_, err = svc.GetRole(context.Background(), "wsp_1", "outsider", role.ID)
	require.ErrorIs(t, err, repos.ErrForbidden)
}

func presetLabel(source string) string {
	if source == SourcePresetRole {
		return "预设角色"
	}
	return source
}

func TestCreateRoleFromAgentPersistsPublishedVersionAndManifest(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})
	role, err := svc.CreateRoleFromAgent(context.Background(), "wsp_1", "usr_1", AgentDerivedMutation{
		AgentID: "agt_source",
		Mutation: Mutation{
			Name:       "Reviewer",
			Definition: "review",
			Attachments: []Attachment{
				{Type: AttachmentPortableKnowledge, Name: "Rubric", Content: "small diffs", Required: true},
				{Type: AttachmentKnowledgeBase, Name: "Runbooks", ResourceID: "kb_1", Realization: RealizationGrant},
			},
		},
	})
	require.NoError(t, err)

	var rows []rolesstore.RoleVersion
	require.NoError(t, svc.store.DB().Where("role_id = ?", role.ID).Order("version ASC").Find(&rows).Error)
	require.Len(t, rows, 1)
	require.Equal(t, 1, rows[0].Version)
	require.Equal(t, "published", rows[0].Status)
	require.Equal(t, role.Definition, rows[0].Definition)

	var items []rolesstore.RolePackageManifestItem
	require.NoError(t, svc.store.DB().Where("role_version_id = ?", rows[0].ID).Order("sort_order ASC").Find(&items).Error)
	require.Len(t, items, 3)
	require.Equal(t, ManifestItemRoleDefinitionAsset, items[0].ItemType)
	require.Equal(t, "IDENTITY.md", items[0].Name)
	require.Equal(t, AttachmentPortableKnowledge, items[1].ItemType)
	require.Equal(t, AttachmentKnowledgeBase, items[2].ItemType)

	stored, err := svc.store.GetRole(context.Background(), "wsp_1", role.ID)
	require.NoError(t, err)
	require.Equal(t, rows[0].ID, stored.ActiveVersionID)
}

func TestUpdateRoleFromAgentPublishesNewImmutableVersion(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})
	role, err := svc.CreateRoleFromAgent(context.Background(), "wsp_1", "usr_1", AgentDerivedMutation{
		AgentID:  "agt_first",
		Mutation: Mutation{Name: "Reviewer", Definition: "review", Attachments: []Attachment{{Type: AttachmentPortableKnowledge, Name: "Rubric", Content: "v1"}}},
	})
	require.NoError(t, err)

	updated, err := svc.UpdateRoleFromAgent(context.Background(), "wsp_1", "usr_1", role.ID, AgentDerivedMutation{
		AgentID:  "agt_second",
		Mutation: Mutation{Name: "Reviewer", Description: "updated metadata", Definition: "review v2", Attachments: []Attachment{{Type: AttachmentPortableKnowledge, Name: "Rubric", Content: "v2"}}},
	})
	require.NoError(t, err)
	require.Equal(t, "updated metadata", updated.Description)

	updatedAgain, err := svc.UpdateRoleFromAgent(context.Background(), "wsp_1", "usr_1", role.ID, AgentDerivedMutation{
		AgentID:  "agt_third",
		Mutation: Mutation{Name: "Reviewer", Description: "updated metadata again", Definition: "review v3", Attachments: []Attachment{{Type: AttachmentPortableKnowledge, Name: "Rubric", Content: "v3"}}},
	})
	require.NoError(t, err)
	require.Equal(t, "updated metadata again", updatedAgain.Description)

	var rows []rolesstore.RoleVersion
	require.NoError(t, svc.store.DB().Where("role_id = ?", role.ID).Order("version ASC").Find(&rows).Error)
	require.Len(t, rows, 3)
	require.Equal(t, 1, rows[0].Version)
	require.Equal(t, 2, rows[1].Version)
	require.Equal(t, 3, rows[2].Version)
	require.Equal(t, "review", rows[0].Definition)
	require.Equal(t, "review v2", rows[1].Definition)
	require.Equal(t, "review v3", rows[2].Definition)

	contents := make([]string, 0, 3)
	for _, row := range rows {
		var items []rolesstore.RolePackageManifestItem
		require.NoError(t, svc.store.DB().Where("role_version_id = ?", row.ID).Order("sort_order ASC").Find(&items).Error)
		require.Len(t, items, 2)
		require.Empty(t, items[0].Content)
		require.NotEmpty(t, items[0].ContentRef)
		require.Empty(t, items[1].Content)
		require.NotEmpty(t, items[1].ContentRef)
		content, err := svc.contentStore.ResolveRolePackage(context.Background(), ResolveRolePackageInput{ContentRef: items[1].ContentRef})
		require.NoError(t, err)
		contents = append(contents, content)
	}
	require.Equal(t, []string{"v1", "v2", "v3"}, contents)

	stored, err := svc.store.GetRole(context.Background(), "wsp_1", role.ID)
	require.NoError(t, err)
	require.Equal(t, rows[2].ID, stored.ActiveVersionID)

	got, err := svc.GetRole(context.Background(), "wsp_1", "usr_1", role.ID)
	require.NoError(t, err)
	require.Equal(t, "review v3", got.Definition)
	require.Len(t, got.RolePackageManifest.Items, 2)
	require.Equal(t, "v3", got.RolePackageManifest.Items[1].Content)
}

func TestRealizeRolePersistsCurrentBindingAndRealization(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})
	role, err := svc.CreateRole(context.Background(), "wsp_1", "usr_1", Mutation{
		Name:        "Identity Keeper",
		Definition:  "keep identity",
		Attachments: []Attachment{{Type: AttachmentMemory, Name: "Identity", Required: true, Realization: RealizationCreate}},
	})
	require.NoError(t, err)

	result, err := svc.RealizeRoleForAgent(context.Background(), "wsp_1", "usr_1", role.ID, RealizationRequest{AgentID: "agt_1", Apply: true, StorageScope: "workspace"})
	require.NoError(t, err)
	require.Len(t, result.Items, 1)
	require.Equal(t, RealizationStatusRealized, result.Items[0].Status)

	var realizations []rolesstore.RoleRealization
	require.NoError(t, svc.store.DB().Order("created_at ASC").Find(&realizations).Error)
	require.Len(t, realizations, 1)
	require.Equal(t, role.ID, realizations[0].RoleID)
	require.Equal(t, RealizationStatusRealized, realizations[0].Status)
	var items []rolesstore.RoleRealizationItem
	require.NoError(t, svc.store.DB().Where("realization_id = ?", realizations[0].ID).Order("sort_order ASC").Find(&items).Error)
	require.Len(t, items, 1)
	require.Equal(t, ManifestItemRoleDefinitionAsset, items[0].ManifestItemType)
	var manifestItems []rolesstore.RolePackageManifestItem
	require.NoError(t, svc.store.DB().Where("role_version_id = ?", realizations[0].RoleVersionID).Order("sort_order ASC").Find(&manifestItems).Error)
	require.Len(t, manifestItems, 1)
	require.Equal(t, manifestItems[0].ID, items[0].ManifestItemID)

	var binding rolesstore.AgentRoleBinding
	require.NoError(t, svc.store.DB().Where("workspace_id = ? AND agent_id = ?", "wsp_1", "agt_1").First(&binding).Error)
	require.Equal(t, "my_role", binding.BindingKind)
	require.Equal(t, role.ID, binding.RoleID)
	require.Equal(t, realizations[0].RoleVersionID, binding.RoleVersionID)
	require.Equal(t, "memory/system/identity.md", binding.ActiveDefinitionPath)
	require.Equal(t, realizations[0].ID, binding.RealizationID)
}

func TestRealizeRoleUsesRequestedImmutableVersion(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})
	role, err := svc.CreateRoleFromAgent(context.Background(), "wsp_1", "usr_1", AgentDerivedMutation{
		AgentID:  "agt_first",
		Mutation: Mutation{Name: "Reviewer", Definition: "review v1", Attachments: []Attachment{{Type: AttachmentPortableKnowledge, Name: "Rubric", Content: "v1"}}},
	})
	require.NoError(t, err)
	v1 := role.RoleVersionID
	updated, err := svc.UpdateRoleFromAgent(context.Background(), "wsp_1", "usr_1", role.ID, AgentDerivedMutation{
		AgentID:  "agt_second",
		Mutation: Mutation{Name: "Reviewer", Definition: "review v2", Attachments: []Attachment{{Type: AttachmentPortableKnowledge, Name: "Rubric", Content: "v2"}}},
	})
	require.NoError(t, err)
	require.NotEqual(t, v1, updated.RoleVersionID)

	result, err := svc.RealizeRoleForAgent(context.Background(), "wsp_1", "usr_1", role.ID, RealizationRequest{AgentID: "agt_1", RoleVersionID: v1, Apply: true, StorageScope: "workspace"})
	require.NoError(t, err)
	require.Len(t, result.Items, 2)

	var realization rolesstore.RoleRealization
	require.NoError(t, svc.store.DB().Where("agent_id = ?", "agt_1").First(&realization).Error)
	require.Equal(t, v1, realization.RoleVersionID)
	var manifestItems []rolesstore.RolePackageManifestItem
	require.NoError(t, svc.store.DB().Where("role_version_id = ?", v1).Order("sort_order ASC").Find(&manifestItems).Error)
	require.Len(t, manifestItems, 2)
	var items []rolesstore.RoleRealizationItem
	require.NoError(t, svc.store.DB().Where("realization_id = ?", realization.ID).Order("sort_order ASC").Find(&items).Error)
	require.Equal(t, manifestItems[0].ID, items[0].ManifestItemID)
	require.Equal(t, manifestItems[1].ID, items[1].ManifestItemID)
}

func TestRealizeRoleRejectsUnavailableRequestedVersion(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})
	role, err := svc.CreateRoleFromAgent(context.Background(), "wsp_1", "usr_1", AgentDerivedMutation{
		AgentID:  "agt_first",
		Mutation: Mutation{Name: "Reviewer", Definition: "review v1", Attachments: []Attachment{{Type: AttachmentPortableKnowledge, Name: "Rubric", Content: "v1"}}},
	})
	require.NoError(t, err)

	_, err = svc.RealizeRoleForAgent(context.Background(), "wsp_1", "usr_1", role.ID, RealizationRequest{AgentID: "agt_1", RoleVersionID: "rlv_missing", Apply: true})

	require.ErrorIs(t, err, repos.ErrNotFound)
}

func TestRealizePresetRolePersistsPresetDerivedBinding(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})

	role, result, err := svc.RealizePresetRoleForAgent(context.Background(), "wsp_1", "usr_1", AddPresetRequest{PresetID: "research-agent", Locale: "en-US"}, RealizationRequest{AgentID: "agt_2", Apply: true, StorageScope: "workspace"})
	require.NoError(t, err)
	require.NotEmpty(t, result.RoleID)
	require.Equal(t, role.ID, result.RoleID)

	var binding rolesstore.AgentRoleBinding
	require.NoError(t, svc.store.DB().Where("workspace_id = ? AND agent_id = ?", "wsp_1", "agt_2").First(&binding).Error)
	require.Equal(t, "preset_derived", binding.BindingKind)
	require.Equal(t, role.SourcePresetID, binding.SourcePresetRoleID)
	require.NotEmpty(t, binding.SourcePresetVersionID)
	require.Equal(t, "memory/system/identity.md", binding.ActiveDefinitionPath)
	require.NotEmpty(t, binding.RealizationID)
	var realization rolesstore.RoleRealization
	require.NoError(t, svc.store.DB().Where("id = ?", binding.RealizationID).First(&realization).Error)
	var manifestItems []rolesstore.RolePackageManifestItem
	require.NoError(t, svc.store.DB().Where("role_version_id = ?", realization.RoleVersionID).Order("sort_order ASC").Find(&manifestItems).Error)
	require.NotEmpty(t, manifestItems)
	var items []rolesstore.RoleRealizationItem
	require.NoError(t, svc.store.DB().Where("realization_id = ?", binding.RealizationID).Order("sort_order ASC").Find(&items).Error)
	require.Len(t, items, len(manifestItems))
	require.Equal(t, manifestItems[0].ID, items[0].ManifestItemID)
}

func TestFrontendDeveloperPresetRealizesDefaultPortableSkills(t *testing.T) {
	skills := &fakeSkillBinder{}
	svc := newRoleServiceForTest(t, Options{Skills: skills})
	wantSkillNames := []string{
		"frontend-ui-engineering",
		"browser-testing-with-devtools",
	}

	preset, err := svc.GetPresetRole(context.Background(), "wsp_1", "usr_1", "frontend-developer", "en-US")
	require.NoError(t, err)
	require.Contains(t, preset.RolePackage.Sections, AttachmentSkill)
	require.Empty(t, preset.RolePackage.Unresolved)

	_, dryRun, err := svc.RealizePresetRoleForAgent(context.Background(), "wsp_1", "usr_1", AddPresetRequest{PresetID: "frontend-developer", Locale: "en-US"}, RealizationRequest{AgentID: "agt_frontend", DryRun: true})
	require.NoError(t, err)
	require.Empty(t, skills.calls)
	dryRunSkills := realizationItemsByType(dryRun.Items, AttachmentSkill)
	require.Len(t, dryRunSkills, len(wantSkillNames))
	for index, item := range dryRunSkills {
		require.Equal(t, wantSkillNames[index], item.ManifestItemName)
		require.Equal(t, RealizationCopy, item.Action)
		require.Equal(t, RealizationStatusPlanned, item.Status)
		require.False(t, item.Required)
		require.Empty(t, item.ResourceID)
		require.Equal(t, "portable skill will be materialized on apply", item.Message)
	}

	role, result, err := svc.RealizePresetRoleForAgent(context.Background(), "wsp_1", "usr_1", AddPresetRequest{PresetID: "frontend-developer", Locale: "en-US"}, RealizationRequest{AgentID: "agt_frontend", Apply: true})
	require.NoError(t, err)
	require.Empty(t, skills.calls)
	appliedSkills := realizationItemsByType(result.Items, AttachmentSkill)
	require.Len(t, appliedSkills, len(wantSkillNames))
	for index, item := range appliedSkills {
		require.Equal(t, wantSkillNames[index], item.ManifestItemName)
		require.Equal(t, RealizationCopy, item.Action)
		require.Equal(t, RealizationStatusRealized, item.Status)
		require.False(t, item.Required)
		require.Empty(t, item.ResourceID)
		require.Equal(t, "portable skill is materialized by role package", item.Message)
	}

	var manifestItems []rolesstore.RolePackageManifestItem
	require.NoError(t, svc.store.DB().
		Where("role_id = ? AND item_type = ?", role.ID, AttachmentSkill).
		Order("sort_order ASC").
		Find(&manifestItems).Error)
	require.Len(t, manifestItems, len(wantSkillNames))
	for index, item := range manifestItems {
		require.Equal(t, wantSkillNames[index], item.Name)
		require.Equal(t, RealizationCopy, item.RealizationAction)
		require.Empty(t, item.SourceResourceID)
		require.Empty(t, item.Content)
		require.Contains(t, item.ContentRef, ".agents/skills/"+wantSkillNames[index]+"/SKILL.md")
		content, err := svc.contentStore.ResolveRolePackage(context.Background(), ResolveRolePackageInput{ContentRef: item.ContentRef})
		require.NoError(t, err)
		require.Contains(t, content, "---\n")
		require.Contains(t, content, "description:")
	}
}

func realizationItemsByType(items []RealizationItem, itemType string) []RealizationItem {
	out := make([]RealizationItem, 0)
	for _, item := range items {
		if item.ManifestItemType == itemType {
			out = append(out, item)
		}
	}
	return out
}

func TestRealizeRoleDryRunDoesNotPersistBindingOrRealization(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})
	role, err := svc.CreateRole(context.Background(), "wsp_1", "usr_1", Mutation{Name: "Dry", Definition: "dry", Attachments: []Attachment{{Type: AttachmentMemory, Name: "Identity", ResourceID: "mem_source", Required: true, Realization: RealizationFork}}})
	require.NoError(t, err)

	_, err = svc.RealizeRoleForAgent(context.Background(), "wsp_1", "usr_1", role.ID, RealizationRequest{AgentID: "agt_1", DryRun: true})
	require.NoError(t, err)

	var realizationCount int64
	require.NoError(t, svc.store.DB().Model(&rolesstore.RoleRealization{}).Count(&realizationCount).Error)
	require.Zero(t, realizationCount)
	var bindingCount int64
	require.NoError(t, svc.store.DB().Model(&rolesstore.AgentRoleBinding{}).Count(&bindingCount).Error)
	require.Zero(t, bindingCount)
}

func TestGetRoleOmitsResolvedContentWhenPackageStoreFailsClosed(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})
	role, err := svc.CreateRole(context.Background(), "wsp_1", "usr_1", Mutation{Name: "Reviewer", Definition: "review", Attachments: []Attachment{{Type: AttachmentPortableKnowledge, Name: "Rubric", Content: "small diffs"}}})
	require.NoError(t, err)

	svc.contentStore = FailClosedRolePackageContentStore{}
	got, err := svc.GetRole(context.Background(), "wsp_1", "usr_1", role.ID)
	require.NoError(t, err)
	require.Equal(t, "review", got.Definition)
	require.Len(t, got.RolePackageManifest.Items, 2)
	require.Equal(t, "review", got.RolePackageManifest.Items[0].Content)
	require.Empty(t, got.RolePackageManifest.Items[1].Content)
	require.Len(t, got.RolePackage.Unresolved, 1)
	require.Contains(t, got.RolePackage.Unresolved[0].Reason, "portable knowledge content_ref is unresolved")

	var rows []rolesstore.RolePackageManifestItem
	require.NoError(t, svc.store.DB().Where("role_id = ?", role.ID).Order("sort_order ASC").Find(&rows).Error)
	require.Len(t, rows, 2)
	for _, row := range rows {
		require.Empty(t, row.Content)
		require.NotEmpty(t, row.ContentRef)
		require.NotEmpty(t, row.ContentHash)
	}
}

func TestCreateRoleFailsClosedWithoutAGSContentStore(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{ContentStore: FailClosedRolePackageContentStore{}})
	_, err := svc.CreateRole(context.Background(), "wsp_1", "usr_1", Mutation{Name: "Reviewer", Definition: "review"})
	require.ErrorIs(t, err, repos.ErrInvalidArgument)
	require.Contains(t, err.Error(), "role package content storage is required")
}

func TestRealizeRoleRequiresExplicitPreviewOrApply(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})
	role, err := svc.CreateRole(context.Background(), "wsp_1", "usr_1", Mutation{Name: "Reviewer", Definition: "review"})
	require.NoError(t, err)

	_, err = svc.RealizeRoleForAgent(context.Background(), "wsp_1", "usr_1", role.ID, RealizationRequest{AgentID: "agt_1"})
	require.ErrorIs(t, err, repos.ErrInvalidArgument)
	require.Contains(t, err.Error(), "dryRun or apply explicitly")
}

func TestRealizeRoleRejectsConflictingPreviewAndApplyModes(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})
	role, err := svc.CreateRole(context.Background(), "wsp_1", "usr_1", Mutation{Name: "Reviewer", Definition: "review"})
	require.NoError(t, err)

	_, err = svc.RealizeRoleForAgent(context.Background(), "wsp_1", "usr_1", role.ID, RealizationRequest{AgentID: "agt_1", DryRun: true, Apply: true})
	require.ErrorIs(t, err, repos.ErrInvalidArgument)
	require.Contains(t, err.Error(), "either dryRun preview or apply")
}

func TestRealizeRoleApplyFailsClosedWithoutAGSRolePackageCapability(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{ContentStore: FailClosedRolePackageContentStore{}})
	role, err := svc.CreateRole(context.Background(), "wsp_1", "usr_1", Mutation{Name: "Role Lite", Definition: "review"})
	require.Error(t, err)
	require.ErrorIs(t, err, repos.ErrInvalidArgument)
	require.Contains(t, err.Error(), "role package content storage")
	require.Empty(t, role.ID)
}
