package roles

import (
	"bytes"
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/api/meshxapi"
	"github.com/tidbcloud/AgentTeam/internal/identity/trusted"
	"github.com/tidbcloud/AgentTeam/internal/service/resource/repos"
	roleservice "github.com/tidbcloud/AgentTeam/internal/service/roles"
)

type fakeRoleService struct {
	capabilities roleservice.Capabilities
	presets      []roleservice.Role
	role         roleservice.Role
	realization  roleservice.RealizationResult
	impact       roleservice.RealizationImpact
	realizeErr   error

	createFromAgentInput roleservice.AgentDerivedMutation
	updateFromAgentInput roleservice.AgentDerivedMutation
	realizeInput         roleservice.RealizationRequest
	presetImpactInput    roleservice.AddPresetRequest
	impactInput          roleservice.RealizationRequest
	listView             string
	listQuery            string
	visibilityInput      string
	deletedRoleID        string
	getVersionID         string
	versionPage          roleservice.RoleVersionPage
	rollbackExpectedID   string
	rollbackTargetID     string
	rollbackErr          error
	versionPageNumber    int
	versionPerPage       int
}

func (s *fakeRoleService) Capabilities(context.Context, string, string) (roleservice.Capabilities, error) {
	return s.capabilities, nil
}

func (s *fakeRoleService) ListRoles(ctx context.Context, workspaceID, subject, view, query string, page, perPage int) (roleservice.Page, error) {
	s.listView = view
	s.listQuery = query
	return roleservice.Page{Items: []roleservice.Role{s.role}, Page: 1, PerPage: 20, Total: 1}, nil
}

func (s *fakeRoleService) ListPresetRoles(context.Context, string, string, string) ([]roleservice.Role, error) {
	return append([]roleservice.Role(nil), s.presets...), nil
}

func (s *fakeRoleService) GetRole(context.Context, string, string, string) (roleservice.Role, error) {
	return s.role, nil
}

func (s *fakeRoleService) GetRoleVersion(ctx context.Context, workspaceID, subject, id string, versionID string) (roleservice.Role, error) {
	s.getVersionID = versionID
	role := s.role
	role.RoleVersionID = versionID
	return role, nil
}

func (s *fakeRoleService) ListRoleVersions(_ context.Context, _, _, _ string, page, perPage int) (roleservice.RoleVersionPage, error) {
	s.versionPageNumber = page
	s.versionPerPage = perPage
	return s.versionPage, nil
}

func (s *fakeRoleService) RollbackRoleVersion(_ context.Context, _, _, _, expectedRoleVersionID, targetRoleVersionID string) (roleservice.Role, error) {
	s.rollbackExpectedID = expectedRoleVersionID
	s.rollbackTargetID = targetRoleVersionID
	return s.role, s.rollbackErr
}

func (s *fakeRoleService) GetPresetRole(context.Context, string, string, string, string) (roleservice.Role, error) {
	return s.role, nil
}

func (s *fakeRoleService) CreateRole(context.Context, string, string, roleservice.Mutation) (roleservice.Role, error) {
	return s.role, nil
}

func (s *fakeRoleService) UpdateRole(context.Context, string, string, string, roleservice.Mutation) (roleservice.Role, error) {
	return s.role, nil
}

func (s *fakeRoleService) UpdateRoleVisibility(ctx context.Context, workspaceID, subject, id, visibility string) (roleservice.Role, error) {
	s.visibilityInput = visibility
	role := s.role
	role.Visibility = visibility
	return role, nil
}

func (s *fakeRoleService) DeleteRole(ctx context.Context, workspaceID, subject, id string) error {
	s.deletedRoleID = id
	return nil
}

func (s *fakeRoleService) CreateRoleFromAgent(ctx context.Context, workspaceID, subject string, input roleservice.AgentDerivedMutation) (roleservice.Role, error) {
	s.createFromAgentInput = input
	return s.role, nil
}

func (s *fakeRoleService) UpdateRoleFromAgent(ctx context.Context, workspaceID, subject, id string, input roleservice.AgentDerivedMutation) (roleservice.Role, error) {
	s.updateFromAgentInput = input
	return s.role, nil
}

func (s *fakeRoleService) AddPresetToMyRoles(context.Context, string, string, roleservice.AddPresetRequest) (roleservice.Role, error) {
	return s.role, nil
}

func (s *fakeRoleService) PlanRoleRealizationImpact(ctx context.Context, workspaceID, subject, id string, input roleservice.RealizationRequest) (roleservice.RealizationImpact, error) {
	s.impactInput = input
	if s.realizeErr != nil {
		return roleservice.RealizationImpact{}, s.realizeErr
	}
	return s.impact, nil
}

func (s *fakeRoleService) PlanPresetRoleRealizationImpact(ctx context.Context, workspaceID, subject string, input roleservice.AddPresetRequest, realization roleservice.RealizationRequest) (roleservice.RealizationImpact, error) {
	s.presetImpactInput = input
	s.impactInput = realization
	if s.realizeErr != nil {
		return roleservice.RealizationImpact{}, s.realizeErr
	}
	return s.impact, nil
}

func (s *fakeRoleService) RealizeRoleForAgent(ctx context.Context, workspaceID, subject, id string, input roleservice.RealizationRequest) (roleservice.RealizationResult, error) {
	s.realizeInput = input
	if s.realizeErr != nil {
		return roleservice.RealizationResult{}, s.realizeErr
	}
	return s.realization, nil
}

func (s *fakeRoleService) RealizePresetRoleForAgent(ctx context.Context, workspaceID, subject string, input roleservice.AddPresetRequest, realization roleservice.RealizationRequest) (roleservice.Role, roleservice.RealizationResult, error) {
	s.realizeInput = realization
	if s.realizeErr != nil {
		return roleservice.Role{}, roleservice.RealizationResult{}, s.realizeErr
	}
	return s.role, s.realization, nil
}

func TestCapabilitiesReturnsDisabledWhenHandlerMissing(t *testing.T) {
	mux := http.NewServeMux()
	RegisterRoutes(mux, nil, nil)

	request := httptest.NewRequest(http.MethodGet, "/api/v1/roles/capabilities", nil)
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, request)

	require.Equal(t, http.StatusOK, rec.Code)
	var body dto.RoleCapabilities
	require.NoError(t, json.NewDecoder(rec.Body).Decode(&body))
	require.False(t, body.Enabled)
	require.NotEmpty(t, body.DisabledReason)
}

func TestCapabilitiesReturnsRoleCapability(t *testing.T) {
	handler := NewRolesHandler(&fakeRoleService{capabilities: roleservice.Capabilities{Enabled: true, PresetRoles: true, MyRoles: true, Realization: true, RolePackage: true}})
	request := httptest.NewRequest(http.MethodGet, "/api/v1/roles/capabilities", nil)
	request.Header.Set(meshxapi.HeaderWorkspaceID, "wsp_1")

	response, err := handler.Capabilities(meshxapi.AuthenticatedRequest{Request: request, Identity: trusted.Identity{Subject: "usr_1"}})
	require.NoError(t, err)
	body := response.Body.(dto.RoleCapabilities)
	require.True(t, body.Enabled)
	require.True(t, body.PresetRoles)
	require.True(t, body.MyRoles)
	require.True(t, body.Realization)
	require.True(t, body.RolePackage)
}

func TestDeleteRoleReturnsNoContent(t *testing.T) {
	service := &fakeRoleService{}
	handler := NewRolesHandler(service)
	request := httptest.NewRequest(http.MethodDelete, "/api/v1/roles/role_1", nil)
	request.SetPathValue("role_id", "role_1")
	request.Header.Set(meshxapi.HeaderWorkspaceID, "wsp_1")

	response, err := handler.Delete(meshxapi.AuthenticatedRequest{Request: request, Identity: trusted.Identity{Subject: "usr_1"}})
	require.NoError(t, err)
	require.Equal(t, http.StatusNoContent, response.Status)
	require.True(t, response.NoBody)
	require.Equal(t, "role_1", service.deletedRoleID)
}

func TestListPresetsReturnsPagedRoleList(t *testing.T) {
	now := time.Date(2026, 6, 24, 1, 2, 3, 0, time.UTC)
	handler := NewRolesHandler(&fakeRoleService{presets: []roleservice.Role{{
		ID: "preset_role_research", Source: roleservice.SourcePresetRole, Name: "研究助手",
		Definition: "research", RuntimeHints: roleservice.RuntimeHints{Runtime: "codex"},
		RolePackageManifest: roleservice.RolePackageManifest{Items: []roleservice.RolePackageManifestItem{{Type: roleservice.ManifestItemRoleDefinitionAsset, Name: "IDENTITY.md", Action: "materialize", Content: "research"}, {Type: roleservice.AttachmentMemory, Name: "研究记忆", Action: roleservice.RealizationCreate}}},
		IsActive:            true, CreatedAt: now, UpdatedAt: now,
	}}})
	request := httptest.NewRequest(http.MethodGet, "/api/v1/roles/presets?locale=zh-CN", nil)
	request.Header.Set(meshxapi.HeaderWorkspaceID, "wsp_1")

	response, err := handler.ListPresets(meshxapi.AuthenticatedRequest{Request: request, Identity: trusted.Identity{Subject: "usr_1"}})
	require.NoError(t, err)
	body := response.Body.(dto.RoleListResponse)
	require.Len(t, body.Items, 1)
	require.Equal(t, roleservice.SourcePresetRole, body.Items[0].Source)
	require.Equal(t, "codex", body.Items[0].RuntimeHints.Runtime)
	require.Equal(t, roleservice.AttachmentMemory, body.Items[0].RolePackageManifest.Items[1].Type)
}

func TestListPassesCollectionViewAndQuery(t *testing.T) {
	service := &fakeRoleService{role: roleservice.Role{ID: "role_1", Source: roleservice.SourceMyRole, Name: "Reviewer", Definition: "review", Visibility: roleservice.RoleVisibilityWorkspace, IsActive: true}}
	handler := NewRolesHandler(service)
	request := httptest.NewRequest(http.MethodGet, "/api/v1/roles?view=workspace&q=review", nil)
	request.Header.Set(meshxapi.HeaderWorkspaceID, "wsp_1")

	response, err := handler.List(meshxapi.AuthenticatedRequest{Request: request, Identity: trusted.Identity{Subject: "usr_1"}})
	require.NoError(t, err)
	body := response.Body.(dto.RoleListResponse)
	require.Len(t, body.Items, 1)
	require.Equal(t, roleservice.RoleVisibilityWorkspace, body.Items[0].Visibility)
	require.Equal(t, "workspace", service.listView)
	require.Equal(t, "review", service.listQuery)
}

func TestGetReturnsRoleContractFields(t *testing.T) {
	now := time.Date(2026, 6, 24, 1, 2, 3, 0, time.UTC)
	handler := NewRolesHandler(&fakeRoleService{role: roleservice.Role{
		ID: "role_1", WorkspaceID: "wsp_1", Source: roleservice.SourceAgentDerivedRole, SourceAgentID: "agt_source",
		Name: "Reviewer", Definition: "review", RuntimeHints: roleservice.RuntimeHints{Runtime: "codex"},
		Visibility:          roleservice.RoleVisibilityWorkspace,
		RolePackageManifest: roleservice.RolePackageManifest{Items: []roleservice.RolePackageManifestItem{{Type: roleservice.ManifestItemRoleDefinitionAsset, Name: "IDENTITY.md", Action: "materialize", Content: "review"}, {Type: roleservice.AttachmentPortableKnowledge, Name: "Rubric", Content: "small diffs"}}},
		RoleVersionID:       "rlv_1",
		IsActive:            true, Role: "owner", CanManage: true, Editable: true, OwnerID: "usr_1",
		Provenance:  roleservice.RoleProvenance{Kind: "agent", Ref: "agt_source"},
		RolePackage: roleservice.RolePackage{Portable: true, Sections: []string{roleservice.AttachmentRoleDefinition, roleservice.AttachmentPortableKnowledge}},
		CreatedAt:   now, UpdatedAt: now,
	}})
	request := httptest.NewRequest(http.MethodGet, "/api/v1/roles/role_1", nil)
	request.SetPathValue("role_id", "role_1")
	request.Header.Set(meshxapi.HeaderWorkspaceID, "wsp_1")

	response, err := handler.Get(meshxapi.AuthenticatedRequest{Request: request, Identity: trusted.Identity{Subject: "usr_1"}})
	require.NoError(t, err)
	body := response.Body.(dto.Role)
	require.Equal(t, roleservice.SourceAgentDerivedRole, body.Source)
	require.Equal(t, roleservice.RoleVisibilityWorkspace, body.Visibility)
	require.Equal(t, "rlv_1", body.RoleVersionID)
	require.Equal(t, "agt_source", body.SourceAgentID)
	require.Equal(t, "usr_1", body.OwnerID)
	require.True(t, body.Editable)
	require.True(t, body.CanManage)
	require.Equal(t, dto.RoleProvenance{Kind: "agent", Ref: "agt_source"}, body.Provenance)
	require.Equal(t, []string{roleservice.AttachmentRoleDefinition, roleservice.AttachmentPortableKnowledge}, body.RolePackage.Sections)
}

func TestUpdateVisibilityUsesRoleVisibilityRequest(t *testing.T) {
	service := &fakeRoleService{role: roleservice.Role{ID: "role_1", Source: roleservice.SourceMyRole, Name: "Reviewer", Definition: "review", IsActive: true}}
	handler := NewRolesHandler(service)
	request := httptest.NewRequest(http.MethodPatch, "/api/v1/roles/role_1/visibility", nil)
	request.SetPathValue("role_id", "role_1")
	request.Header.Set(meshxapi.HeaderWorkspaceID, "wsp_1")

	response, err := handler.UpdateVisibility(meshxapi.AuthenticatedJSON[dto.RoleVisibilityRequest]{
		AuthenticatedRequest: meshxapi.AuthenticatedRequest{Request: request, Identity: trusted.Identity{Subject: "usr_1"}},
		Body:                 dto.RoleVisibilityRequest{Visibility: roleservice.RoleVisibilityWorkspace},
	})
	require.NoError(t, err)
	require.Equal(t, http.StatusOK, response.Status)
	require.Equal(t, roleservice.RoleVisibilityWorkspace, service.visibilityInput)
	body := response.Body.(dto.Role)
	require.Equal(t, roleservice.RoleVisibilityWorkspace, body.Visibility)
}

func TestCreateFromAgentUsesAgentDerivedMutation(t *testing.T) {
	service := &fakeRoleService{role: roleservice.Role{ID: "role_new", Source: roleservice.SourceAgentDerivedRole, Name: "Reviewer", Definition: "review", IsActive: true}}
	handler := NewRolesHandler(service)
	body := []byte(`{"agentId":"agt_source","name":"Reviewer","definition":"review","attachments":[{"type":"portable_knowledge","name":"Rubric","content":"small diffs"}]}`)
	request := httptest.NewRequest(http.MethodPost, "/api/v1/roles:from-agent", bytes.NewReader(body))
	request.Header.Set(meshxapi.HeaderWorkspaceID, "wsp_1")

	response, err := handler.CreateFromAgent(meshxapi.AuthenticatedJSON[dto.AgentDerivedRoleMutationRequest]{
		AuthenticatedRequest: meshxapi.AuthenticatedRequest{Request: request, Identity: trusted.Identity{Subject: "usr_1"}},
		Body:                 dto.AgentDerivedRoleMutationRequest{AgentID: "agt_source", RoleMutationRequest: dto.RoleMutationRequest{Name: "Reviewer", Definition: "review", RolePackageManifest: dto.RolePackageManifest{Items: []dto.RolePackageManifestItem{{Type: roleservice.ManifestItemRoleDefinitionAsset, Content: "review"}, {Type: roleservice.AttachmentPortableKnowledge, Name: "Rubric", Content: "small diffs"}}}}},
	})
	require.NoError(t, err)
	require.Equal(t, http.StatusCreated, response.Status)
	require.Equal(t, "agt_source", service.createFromAgentInput.AgentID)
	require.Equal(t, "Reviewer", service.createFromAgentInput.Name)
	require.Equal(t, roleservice.AttachmentPortableKnowledge, service.createFromAgentInput.Attachments[0].Type)
}

func TestUpdateFromAgentUsesAgentDerivedMutation(t *testing.T) {
	service := &fakeRoleService{role: roleservice.Role{ID: "role_1", Source: roleservice.SourceAgentDerivedRole, Name: "Reviewer v2", Definition: "review", IsActive: true}}
	handler := NewRolesHandler(service)
	request := httptest.NewRequest(http.MethodPost, "/api/v1/roles/role_1/from-agent", nil)
	request.SetPathValue("role_id", "role_1")
	request.Header.Set(meshxapi.HeaderWorkspaceID, "wsp_1")

	response, err := handler.UpdateFromAgent(meshxapi.AuthenticatedJSON[dto.AgentDerivedRoleMutationRequest]{
		AuthenticatedRequest: meshxapi.AuthenticatedRequest{Request: request, Identity: trusted.Identity{Subject: "usr_1"}},
		Body:                 dto.AgentDerivedRoleMutationRequest{AgentID: "agt_source", RoleMutationRequest: dto.RoleMutationRequest{Name: "Reviewer v2", Definition: "review"}},
	})
	require.NoError(t, err)
	require.Equal(t, http.StatusOK, response.Status)
	require.Equal(t, "agt_source", service.updateFromAgentInput.AgentID)
	require.Equal(t, "Reviewer v2", service.updateFromAgentInput.Name)
}

func TestRealizePassesActorForMemoryCreation(t *testing.T) {
	service := &fakeRoleService{realization: roleservice.RealizationResult{RoleID: "role_1", AgentID: "agt_1", Apply: true}}
	handler := NewRolesHandler(service)
	request := httptest.NewRequest(http.MethodPost, "/api/v1/roles/role_1/realize", nil)
	request.SetPathValue("role_id", "role_1")
	request.Header.Set(meshxapi.HeaderWorkspaceID, "wsp_1")

	_, err := handler.Realize(meshxapi.AuthenticatedJSON[dto.RoleRealizationRequest]{
		AuthenticatedRequest: meshxapi.AuthenticatedRequest{
			Request: request,
			Identity: trusted.Identity{
				Subject:           "usr_1",
				PreferredUsername: "Ada User",
				Name:              "Ada User",
				Email:             "ada@example.test",
			},
		},
		Body: dto.RoleRealizationRequest{AgentID: "agt_1", Apply: true},
	})
	require.NoError(t, err)
	require.True(t, service.realizeInput.Apply)
	require.Equal(t, repos.ResourceRepositoryActor{Provider: "agentteam", Subject: "usr_1", Login: "ada-user", Name: "Ada User", Email: "ada@example.test"}, service.realizeInput.Actor)
}

func TestRealizationImpactReturnsResourceDeltas(t *testing.T) {
	service := &fakeRoleService{impact: roleservice.RealizationImpact{ManagedMemories: 1, SkillRepositories: 2}}
	handler := NewRolesHandler(service)
	request := httptest.NewRequest(http.MethodPost, "/api/v1/internal/roles/role_1/realization-impact", nil)
	request.SetPathValue("role_id", "role_1")
	request.Header.Set(meshxapi.HeaderWorkspaceID, "wsp_1")

	response, err := handler.RealizationImpact(meshxapi.AuthenticatedJSON[dto.RoleRealizationRequest]{
		AuthenticatedRequest: meshxapi.AuthenticatedRequest{Request: request, Identity: trusted.Identity{Subject: "usr_1"}},
		Body:                 dto.RoleRealizationRequest{AgentID: "agt_1", Apply: true},
	})

	require.NoError(t, err)
	require.Equal(t, http.StatusOK, response.Status)
	require.True(t, service.impactInput.Apply)
	body := response.Body.(dto.RoleRealizationImpactResponse)
	require.Equal(t, int64(1), body.ManagedMemories)
	require.Equal(t, int64(2), body.SkillRepositories)
}

func TestRoleResponseMarshalsRolePackageManifest(t *testing.T) {
	now := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
	handler := NewRolesHandler(&fakeRoleService{role: roleservice.Role{
		ID: "role_1", WorkspaceID: "wsp_1", Source: roleservice.SourceAgentDerivedRole, SourceAgentID: "agt_source",
		Name: "Reviewer", Definition: "review",
		RolePackageManifest: roleservice.RolePackageManifest{Items: []roleservice.RolePackageManifestItem{{Type: roleservice.ManifestItemRoleDefinitionAsset, Name: "IDENTITY.md", Action: "materialize", Content: "review"}, {Type: roleservice.AttachmentPortableKnowledge, Name: "Rubric", Content: "small diffs"}}},
		IsActive:            true, Role: "owner", CanManage: true, Editable: true, OwnerID: "usr_1",
		Provenance:  roleservice.RoleProvenance{Kind: "agent", Ref: "agt_source"},
		RolePackage: roleservice.RolePackage{Portable: true, Sections: []string{roleservice.AttachmentRoleDefinition, roleservice.AttachmentPortableKnowledge}},
		CreatedAt:   now, UpdatedAt: now,
	}})
	request := httptest.NewRequest(http.MethodGet, "/api/v1/roles/role_1", nil)
	request.SetPathValue("role_id", "role_1")
	request.Header.Set(meshxapi.HeaderWorkspaceID, "wsp_1")

	response, err := handler.Get(meshxapi.AuthenticatedRequest{Request: request, Identity: trusted.Identity{Subject: "usr_1"}})
	require.NoError(t, err)
	payload, err := json.Marshal(response.Body)
	require.NoError(t, err)
	require.Contains(t, string(payload), "\"rolePackageManifest\":{\"items\":[")
	require.Contains(t, string(payload), roleservice.ManifestItemRoleDefinitionAsset)
	require.Contains(t, string(payload), roleservice.AttachmentPortableKnowledge)
	require.NotContains(t, string(payload), "\"attachments\"")
	require.Contains(t, string(payload), "\"manifest\":{\"items\":[")
}

func TestCreateFromAgentAcceptsRolePackageManifestRequest(t *testing.T) {
	service := &fakeRoleService{role: roleservice.Role{ID: "role_new", Source: roleservice.SourceAgentDerivedRole, Name: "Reviewer", Definition: "review", IsActive: true}}
	handler := NewRolesHandler(service)
	body := []byte(`{"agentId":"agt_source","name":"Reviewer","definition":"review","rolePackageManifest":{"items":[{"type":"role_definition_asset","content":"review"},{"type":"portable_knowledge","name":"Rubric","content":"small diffs"},{"type":"knowledge_base","name":"Runbooks","resourceId":"kb_1","action":"grant"}]}}`)
	request := httptest.NewRequest(http.MethodPost, "/api/v1/roles:from-agent", bytes.NewReader(body))
	request.Header.Set(meshxapi.HeaderWorkspaceID, "wsp_1")

	parsed := dto.AgentDerivedRoleMutationRequest{}
	require.NoError(t, json.Unmarshal(body, &parsed))
	require.Equal(t, "agt_source", parsed.AgentID)
	response, err := handler.CreateFromAgent(meshxapi.AuthenticatedJSON[dto.AgentDerivedRoleMutationRequest]{
		AuthenticatedRequest: meshxapi.AuthenticatedRequest{Request: request, Identity: trusted.Identity{Subject: "usr_1"}},
		Body:                 parsed,
	})
	require.NoError(t, err)
	require.Equal(t, http.StatusCreated, response.Status)
	require.Len(t, service.createFromAgentInput.Attachments, 2)
	require.Equal(t, roleservice.AttachmentPortableKnowledge, service.createFromAgentInput.Attachments[0].Type)
	require.Equal(t, roleservice.AttachmentKnowledgeBase, service.createFromAgentInput.Attachments[1].Type)
	require.Equal(t, roleservice.RealizationGrant, service.createFromAgentInput.Attachments[1].Realization)
}

func TestRealizePassesApplyMode(t *testing.T) {
	service := &fakeRoleService{realization: roleservice.RealizationResult{RoleID: "role_1", AgentID: "agt_1", Apply: true}}
	handler := NewRolesHandler(service)
	request := httptest.NewRequest(http.MethodPost, "/api/v1/roles/role_1/realize", bytes.NewBufferString(`{"agentId":"agt_1","apply":true}`))
	request.Header.Set(meshxapi.HeaderWorkspaceID, "wsp_1")
	response, err := handler.Realize(meshxapi.AuthenticatedJSON[dto.RoleRealizationRequest]{AuthenticatedRequest: meshxapi.AuthenticatedRequest{Request: request, Identity: trusted.Identity{Subject: "usr_1"}}, Body: dto.RoleRealizationRequest{AgentID: "agt_1", Apply: true}})
	require.NoError(t, err)
	require.True(t, service.realizeInput.Apply)
	body := response.Body.(dto.RoleRealizationResponse)
	require.True(t, body.Apply)
}

func TestGetPassesRequestedRoleVersion(t *testing.T) {
	service := &fakeRoleService{role: roleservice.Role{ID: "role_1", Source: roleservice.SourceAgentDerivedRole, Name: "Reviewer", Definition: "review", RoleVersionID: "rlv_active", IsActive: true}}
	handler := NewRolesHandler(service)
	request := httptest.NewRequest(http.MethodGet, "/api/v1/roles/role_1?versionId=rlv_1", nil)
	request.SetPathValue("role_id", "role_1")
	request.Header.Set(meshxapi.HeaderWorkspaceID, "wsp_1")

	response, err := handler.Get(meshxapi.AuthenticatedRequest{Request: request, Identity: trusted.Identity{Subject: "usr_1"}})

	require.NoError(t, err)
	require.Equal(t, "rlv_1", service.getVersionID)
	body := response.Body.(dto.Role)
	require.Equal(t, "rlv_1", body.RoleVersionID)
}

func TestRoleVersionHistoryAndRollbackTransport(t *testing.T) {
	now := time.Date(2026, 8, 21, 3, 0, 0, 0, time.UTC)
	service := &fakeRoleService{
		role: roleservice.Role{ID: "role_1", Source: roleservice.SourceMyRole, Name: "Reviewer", Definition: "review", RoleVersionID: "rlv_new", IsActive: true},
		versionPage: roleservice.RoleVersionPage{Items: []roleservice.RoleVersion{{
			ID: "rlv_new", Version: 3, ContentID: "ctn_1", ContentVersionID: "ctv_3", CreatedBy: "usr_1", CreatedAt: now, Active: true,
		}}, Page: 2, PerPage: 25, HasMore: true},
	}
	handler := NewRolesHandler(service)
	listRequest := httptest.NewRequest(http.MethodGet, "/api/v1/roles/role_1/history/versions?page=2&per_page=25", nil)
	listRequest.SetPathValue("role_id", "role_1")
	listRequest.Header.Set(meshxapi.HeaderWorkspaceID, "wsp_1")
	response, err := handler.ListVersions(meshxapi.AuthenticatedRequest{Request: listRequest, Identity: trusted.Identity{Subject: "usr_1"}})
	require.NoError(t, err)
	require.Equal(t, 2, service.versionPageNumber)
	require.Equal(t, 25, service.versionPerPage)
	list := response.Body.(dto.RoleVersionListResponse)
	require.Len(t, list.Items, 1)
	require.Equal(t, "ctv_3", list.Items[0].ContentVersionID)
	require.True(t, list.Items[0].Active)

	rollbackRequest := httptest.NewRequest(http.MethodPost, "/api/v1/roles/role_1/history/rollback", nil)
	rollbackRequest.SetPathValue("role_id", "role_1")
	rollbackRequest.Header.Set(meshxapi.HeaderWorkspaceID, "wsp_1")
	response, err = handler.RollbackVersion(meshxapi.AuthenticatedJSON[dto.RoleVersionRollbackRequest]{
		AuthenticatedRequest: meshxapi.AuthenticatedRequest{Request: rollbackRequest, Identity: trusted.Identity{Subject: "usr_1"}},
		Body:                 dto.RoleVersionRollbackRequest{ExpectedRoleVersionID: "rlv_new", TargetRoleVersionID: "rlv_old"},
	})
	require.NoError(t, err)
	require.Equal(t, "rlv_new", service.rollbackExpectedID)
	require.Equal(t, "rlv_old", service.rollbackTargetID)
	require.Equal(t, "role_1", response.Body.(dto.Role).ID)
}

func TestRoleVersionRollbackConflictIsStableHTTPConflict(t *testing.T) {
	service := &fakeRoleService{rollbackErr: roleservice.ErrRoleVersionConflict}
	mux := http.NewServeMux()
	RegisterRoutes(mux, NewRolesHandler(service), nil)
	request := httptest.NewRequest(http.MethodPost, "/api/v1/roles/role_1/history/rollback", strings.NewReader(`{"expectedRoleVersionId":"rlv_current","targetRoleVersionId":"rlv_old"}`))
	request.Header.Set(meshxapi.HeaderWorkspaceID, "wsp_1")
	request = request.WithContext(trusted.WithIdentity(request.Context(), trusted.Identity{Subject: "usr_1"}))
	recorder := httptest.NewRecorder()

	mux.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusConflict, recorder.Code)
	var body map[string]any
	require.NoError(t, json.NewDecoder(recorder.Body).Decode(&body))
	require.Equal(t, "role_version_conflict", body["error"])
}

func TestRealizePassesRequestedRoleVersion(t *testing.T) {
	service := &fakeRoleService{realization: roleservice.RealizationResult{RoleID: "role_1", AgentID: "agt_1", Apply: true}}
	handler := NewRolesHandler(service)
	request := httptest.NewRequest(http.MethodPost, "/api/v1/roles/role_1/realize", bytes.NewBufferString(`{"agentId":"agt_1","roleVersionId":"rlv_1","apply":true}`))
	request.Header.Set(meshxapi.HeaderWorkspaceID, "wsp_1")

	_, err := handler.Realize(meshxapi.AuthenticatedJSON[dto.RoleRealizationRequest]{
		AuthenticatedRequest: meshxapi.AuthenticatedRequest{Request: request, Identity: trusted.Identity{Subject: "usr_1"}},
		Body:                 dto.RoleRealizationRequest{AgentID: "agt_1", RoleVersionID: "rlv_1", Apply: true},
	})

	require.NoError(t, err)
	require.Equal(t, "rlv_1", service.realizeInput.RoleVersionID)
}

func TestRoleErrorHandlingClassifiesRolePackageDependencyFailure(t *testing.T) {
	packageStore := roleservice.AGSHTTPRolePackageContentStore{
		Handler:             rolePackageBranchFailureHandler(t),
		AuthorizationHeader: "Bearer test-meshx-internal",
	}
	_, dependencyErr := packageStore.StoreRolePackage(context.Background(), roleservice.StoreRolePackageInput{
		WorkspaceID: "wsp_1",
		RoleID:      "role_1",
		VersionID:   "rlv_1",
		Assets:      []roleservice.RolePackageAsset{{ItemType: roleservice.ManifestItemRoleDefinitionAsset, Name: "IDENTITY.md", Content: "role"}},
	})
	require.Error(t, dependencyErr)
	require.True(t, roleservice.IsRolePackageDependencyUnavailable(dependencyErr), "StoreRolePackage error = %v", dependencyErr)
	service := &fakeRoleService{realizeErr: dependencyErr}
	handler := NewRolesHandler(service)
	mux := http.NewServeMux()
	RegisterRoutes(mux, handler, nil)
	request := httptest.NewRequest(http.MethodPost, "/api/v1/roles/role_1/realize", bytes.NewBufferString(`{"agentId":"agt_1","apply":true}`))
	request.SetPathValue("role_id", "role_1")
	request.Header.Set(meshxapi.HeaderWorkspaceID, "wsp_1")
	request = request.WithContext(trusted.WithIdentity(request.Context(), trusted.Identity{Subject: "usr_1"}))
	recorder := httptest.NewRecorder()

	mux.ServeHTTP(recorder, request)

	require.Equal(t, http.StatusServiceUnavailable, recorder.Code)
	var body map[string]string
	require.NoError(t, json.NewDecoder(recorder.Body).Decode(&body))
	require.Equal(t, "role_dependency_unavailable", body["error"])
	require.Contains(t, body["message"], "role content is temporarily unavailable")
	require.NotContains(t, body["message"], "role dependency unavailable")
	require.NotContains(t, body["message"], "AGS is restarting")
	require.True(t, service.realizeInput.Apply)
}

func rolePackageBranchFailureHandler(t *testing.T) http.Handler {
	t.Helper()
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, "Bearer test-meshx-internal", r.Header.Get("Authorization"))
		switch {
		case r.Method == http.MethodGet && strings.HasPrefix(r.URL.Path, "/api/v3/repos/") && strings.Contains(r.URL.Path, "/branches/"):
			http.Error(w, "AGS is restarting", http.StatusServiceUnavailable)
		case r.Method == http.MethodGet && strings.HasPrefix(r.URL.Path, "/api/v3/repos/"):
			http.NotFound(w, r)
		case r.Method == http.MethodGet && strings.HasPrefix(r.URL.Path, "/api/v3/orgs/"):
			owner := strings.TrimPrefix(r.URL.Path, "/api/v3/orgs/")
			_ = json.NewEncoder(w).Encode(map[string]any{"login": owner})
		case r.Method == http.MethodPost && strings.HasPrefix(r.URL.Path, "/api/v3/orgs/"):
			owner := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/api/v3/orgs/"), "/repos")
			var payload map[string]any
			require.NoError(t, json.NewDecoder(r.Body).Decode(&payload))
			name := payload["name"].(string)
			w.WriteHeader(http.StatusCreated)
			_ = json.NewEncoder(w).Encode(map[string]any{"id": 1, "name": name, "full_name": owner + "/" + name, "default_branch": "main"})
		default:
			http.NotFound(w, r)
		}
	})
}
