package roles

import (
	"context"
	"testing"

	"github.com/stretchr/testify/require"
	rolesstore "github.com/tidbcloud/AgentTeam/internal/store/resource/roles"
)

func TestCreateRoleOperationReplaysResultAndRejectsDigestConflict(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})
	ctx := context.Background()
	operation := MutationOperation{OperationKey: "op-create-1", RequestDigest: digestForTest("create-v1")}
	input := Mutation{Name: "Reviewer", Definition: "review production changes"}

	created, err := svc.CreateRoleOperation(ctx, "wsp_1", "usr_1", operation, input)
	require.NoError(t, err)
	replayed, err := svc.CreateRoleOperation(ctx, "wsp_1", "usr_1", operation, input)
	require.NoError(t, err)
	require.Equal(t, created.ID, replayed.ID)
	require.Equal(t, created.RoleVersionID, replayed.RoleVersionID)

	_, err = svc.CreateRoleOperation(ctx, "wsp_1", "usr_1", MutationOperation{
		OperationKey: operation.OperationKey, RequestDigest: digestForTest("different-create"),
	}, Mutation{Name: "Other", Definition: "other"})
	require.ErrorIs(t, err, ErrRoleMutationConflict)

	var roleCount, versionCount int64
	require.NoError(t, svc.store.DB().Model(&rolesstore.Role{}).Count(&roleCount).Error)
	require.NoError(t, svc.store.DB().Model(&rolesstore.RoleVersion{}).Count(&versionCount).Error)
	require.EqualValues(t, 1, roleCount)
	require.EqualValues(t, 1, versionCount)
}

func TestMutationOperationRequiresIdentityAndIsWorkspaceScoped(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})
	ctx := context.Background()
	input := Mutation{Name: "Reviewer", Definition: "review"}

	for _, operation := range []MutationOperation{
		{OperationKey: " ", RequestDigest: digestForTest("create")},
		{OperationKey: "op-create", RequestDigest: " "},
	} {
		_, err := svc.CreateRoleOperation(ctx, "wsp_1", "usr_1", operation, input)
		require.ErrorIs(t, err, ErrInvalidRoleMutationOperation)
	}

	operation := MutationOperation{OperationKey: "op-workspace-scoped", RequestDigest: digestForTest("same")}
	first, err := svc.CreateRoleOperation(ctx, "wsp_1", "usr_1", operation, input)
	require.NoError(t, err)
	second, err := svc.CreateRoleOperation(ctx, "wsp_2", "usr_1", operation, input)
	require.NoError(t, err)
	require.NotEqual(t, first.ID, second.ID)
	require.Equal(t, "wsp_1", first.WorkspaceID)
	require.Equal(t, "wsp_2", second.WorkspaceID)
}

func TestUpdateRoleOperationPublishesContentAndVisibilityAtomically(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})
	ctx := context.Background()
	role, err := svc.CreateRole(ctx, "wsp_1", "usr_1", Mutation{
		Name: "Reviewer", Definition: "review v1",
	})
	require.NoError(t, err)
	v1 := role.RoleVersionID
	name, definition, visibility := "Senior reviewer", "review v2", RoleVisibilityWorkspace
	operation := MutationOperation{OperationKey: "op-update-1", RequestDigest: digestForTest("update-v2")}

	updated, err := svc.UpdateRoleOperation(ctx, "wsp_1", "usr_1", role.ID, operation, v1, RoleUpdatePatch{
		Name: &name, Definition: &definition, Visibility: &visibility,
	})
	require.NoError(t, err)
	require.NotEqual(t, v1, updated.RoleVersionID)
	require.Equal(t, "Senior reviewer", updated.Name)
	require.Equal(t, "review v2", updated.Definition)
	require.Equal(t, RoleVisibilityWorkspace, updated.Visibility)

	replayed, err := svc.UpdateRoleOperation(ctx, "wsp_1", "usr_1", role.ID, operation, v1, RoleUpdatePatch{
		Name: &name, Definition: &definition, Visibility: &visibility,
	})
	require.NoError(t, err)
	require.Equal(t, updated.RoleVersionID, replayed.RoleVersionID)

	staleDefinition := "review stale definition"
	staleContents := cloneRolePackageContents(svc.contentStore.(*testRolePackageContentStore).contents)
	var staleVersionsBefore, staleManifestBefore []string
	require.NoError(t, svc.store.DB().Model(&rolesstore.RoleVersion{}).
		Where("role_id = ?", role.ID).Order("id").Pluck("id", &staleVersionsBefore).Error)
	require.NoError(t, svc.store.DB().Model(&rolesstore.RolePackageManifestItem{}).
		Where("role_id = ?", role.ID).Order("id").Pluck("id", &staleManifestBefore).Error)
	_, err = svc.UpdateRoleOperation(ctx, "wsp_1", "usr_1", role.ID, MutationOperation{
		OperationKey: "op-update-stale", RequestDigest: digestForTest("stale-content"),
	}, v1, RoleUpdatePatch{Definition: &staleDefinition})
	require.ErrorIs(t, err, ErrRoleVersionConflict)

	_, err = svc.UpdateRoleOperation(ctx, "wsp_1", "usr_1", role.ID, MutationOperation{
		OperationKey: operation.OperationKey, RequestDigest: digestForTest("conflicting-update"),
	}, v1, RoleUpdatePatch{Visibility: roleStringForOperationTest(RoleVisibilityPrivate)})
	require.ErrorIs(t, err, ErrRoleMutationConflict)

	var versions []rolesstore.RoleVersion
	require.NoError(t, svc.store.DB().Where("role_id = ?", role.ID).Order("version ASC").Find(&versions).Error)
	require.Len(t, versions, 2, "receipt conflict must roll back any new immutable version")

	current, err := svc.GetRole(ctx, "wsp_1", "usr_1", role.ID)
	require.NoError(t, err)
	require.Equal(t, updated.RoleVersionID, current.RoleVersionID)
	require.Equal(t, RoleVisibilityWorkspace, current.Visibility)
	require.Equal(t, "review v2", current.Definition)

	var staleVersionsAfter, staleManifestAfter []string
	require.NoError(t, svc.store.DB().Model(&rolesstore.RoleVersion{}).
		Where("role_id = ?", role.ID).Order("id").Pluck("id", &staleVersionsAfter).Error)
	require.NoError(t, svc.store.DB().Model(&rolesstore.RolePackageManifestItem{}).
		Where("role_id = ?", role.ID).Order("id").Pluck("id", &staleManifestAfter).Error)
	require.Equal(t, staleVersionsBefore, staleVersionsAfter, "stale definition must not publish a version")
	require.Equal(t, staleManifestBefore, staleManifestAfter, "stale definition must not publish a manifest")
	require.Equal(t, staleContents, svc.contentStore.(*testRolePackageContentStore).contents,
		"stale definition must not persist role-package content")
}

func TestMutationOperationKeyIsSharedAcrossActions(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})
	ctx := context.Background()
	operationKey := "op-shared-across-actions"
	created, err := svc.CreateRoleOperation(ctx, "wsp_1", "usr_1", MutationOperation{
		OperationKey: operationKey, RequestDigest: digestForTest("create"),
	}, Mutation{Name: "Reviewer", Definition: "review"})
	require.NoError(t, err)

	_, err = svc.DeleteRoleOperation(ctx, "wsp_1", "usr_1", created.ID, MutationOperation{
		OperationKey: operationKey, RequestDigest: digestForTest("create"),
	}, created.RoleVersionID)
	require.ErrorIs(t, err, ErrRoleMutationConflict)
	_, err = svc.GetRole(ctx, "wsp_1", "usr_1", created.ID)
	require.NoError(t, err, "cross-action operation-key conflict must not delete the Role")
}

func TestVisibilityOnlyOperationAdvancesGenerationAndStaleUpdateHasNoEffect(t *testing.T) {
	svc := newRoleServiceForTest(t, Options{})
	ctx := context.Background()
	role, err := svc.CreateRole(ctx, "wsp_1", "usr_1", Mutation{
		Name: "Reviewer", Description: "kept description", Definition: "review v1",
		Attachments: []Attachment{{
			Type: AttachmentPortableKnowledge, Name: "Rubric", Content: "keep this rubric", Required: true,
		}},
	})
	require.NoError(t, err)
	v1 := role.RoleVersionID
	contentBefore := cloneRolePackageContents(svc.contentStore.(*testRolePackageContentStore).contents)

	updated, err := svc.UpdateRoleOperation(
		ctx, "wsp_1", "usr_1", role.ID,
		MutationOperation{OperationKey: "op-visibility-1", RequestDigest: digestForTest("workspace")},
		v1, RoleUpdatePatch{Visibility: roleStringForOperationTest(RoleVisibilityWorkspace)},
	)
	require.NoError(t, err)
	require.NotEqual(t, v1, updated.RoleVersionID)
	require.Equal(t, RoleVisibilityWorkspace, updated.Visibility)
	require.Equal(t, role.Name, updated.Name)
	require.Equal(t, role.Description, updated.Description)
	require.Equal(t, role.Definition, updated.Definition)
	require.Equal(t, role.RolePackageManifest, updated.RolePackageManifest)
	require.Equal(t, contentBefore, svc.contentStore.(*testRolePackageContentStore).contents,
		"visibility-only update must reuse existing role-package content")

	var published rolesstore.RoleVersion
	require.NoError(t, svc.store.DB().Where("id = ?", updated.RoleVersionID).First(&published).Error)
	require.Equal(t, role.Definition, published.Definition)
	var oldManifest, newManifest []rolesstore.RolePackageManifestItem
	require.NoError(t, svc.store.DB().Where("role_version_id = ?", v1).Order("sort_order").Find(&oldManifest).Error)
	require.NoError(t, svc.store.DB().Where("role_version_id = ?", updated.RoleVersionID).Order("sort_order").Find(&newManifest).Error)
	require.Len(t, newManifest, len(oldManifest))
	for index := range oldManifest {
		require.Equal(t, oldManifest[index].ItemType, newManifest[index].ItemType)
		require.Equal(t, oldManifest[index].Name, newManifest[index].Name)
		require.Equal(t, oldManifest[index].ContentRef, newManifest[index].ContentRef)
		require.Equal(t, oldManifest[index].ContentHash, newManifest[index].ContentHash)
	}

	_, err = svc.UpdateRoleOperation(
		ctx, "wsp_1", "usr_1", role.ID,
		MutationOperation{OperationKey: "op-visibility-stale", RequestDigest: digestForTest("private")},
		v1, RoleUpdatePatch{Visibility: roleStringForOperationTest(RoleVisibilityPrivate)},
	)
	require.ErrorIs(t, err, ErrRoleVersionConflict)

	current, err := svc.GetRole(ctx, "wsp_1", "usr_1", role.ID)
	require.NoError(t, err)
	require.Equal(t, updated.RoleVersionID, current.RoleVersionID)
	require.Equal(t, RoleVisibilityWorkspace, current.Visibility)
	var versionCount int64
	require.NoError(t, svc.store.DB().Model(&rolesstore.RoleVersion{}).Where("role_id = ?", role.ID).Count(&versionCount).Error)
	require.EqualValues(t, 2, versionCount)

	// An exact retry remains stale: expectedRoleVersionId is part of the
	// canonical payload and cannot be replaced under the same operation key.
	_, err = svc.UpdateRoleOperation(
		ctx, "wsp_1", "usr_1", role.ID,
		MutationOperation{OperationKey: "op-visibility-stale", RequestDigest: digestForTest("private")},
		v1, RoleUpdatePatch{Visibility: roleStringForOperationTest(RoleVisibilityPrivate)},
	)
	require.ErrorIs(t, err, ErrRoleVersionConflict)

	// A new request identity may target the new generation.
	retried, err := svc.UpdateRoleOperation(
		ctx, "wsp_1", "usr_1", role.ID,
		MutationOperation{OperationKey: "op-visibility-current", RequestDigest: digestForTest("private-current")},
		updated.RoleVersionID, RoleUpdatePatch{Visibility: roleStringForOperationTest(RoleVisibilityPrivate)},
	)
	require.NoError(t, err)
	require.NotEqual(t, updated.RoleVersionID, retried.RoleVersionID)
	require.Equal(t, RoleVisibilityPrivate, retried.Visibility)
}

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

	_, err = svc.DeleteRoleOperation(
		ctx, "wsp_1", "usr_1", role.ID,
		MutationOperation{OperationKey: "op-delete-stale", RequestDigest: digestForTest("stale")},
		"rlv_stale",
	)
	require.ErrorIs(t, err, ErrRoleVersionConflict)
	_, err = svc.GetRole(ctx, "wsp_1", "usr_1", role.ID)
	require.NoError(t, err, "stale delete must not hide the Role")

	operation := MutationOperation{OperationKey: "op-delete-1", RequestDigest: digestForTest("delete")}
	deleted, err := svc.DeleteRoleOperation(ctx, "wsp_1", "usr_1", role.ID, operation, role.RoleVersionID)
	require.NoError(t, err)
	require.Equal(t, role.ID, deleted.RoleID)
	require.Equal(t, role.RoleVersionID, deleted.RoleVersionID)
	_, err = svc.GetRole(ctx, "wsp_1", "usr_1", role.ID)
	require.Error(t, err)

	replayed, err := svc.DeleteRoleOperation(ctx, "wsp_1", "usr_1", role.ID, operation, role.RoleVersionID)
	require.NoError(t, err)
	require.Equal(t, deleted, replayed)

	_, err = svc.DeleteRoleOperation(ctx, "wsp_1", "usr_1", role.ID, MutationOperation{
		OperationKey: operation.OperationKey, RequestDigest: digestForTest("different-delete"),
	}, role.RoleVersionID)
	require.ErrorIs(t, err, ErrRoleMutationConflict)
}

func TestApplyRoleOperationRequiresActiveVersionAndDoesNotRepeatMaterialization(t *testing.T) {
	skills := &fakeSkillBinder{}
	svc := newRoleServiceForTest(t, Options{
		Skills: skills, Ownership: &testAgentOwnershipSource{owned: map[string]bool{"agt_1": true}},
	})
	ctx := context.Background()
	role, err := svc.CreateRole(ctx, "wsp_1", "usr_1", Mutation{
		Name: "Reviewer", Definition: "review v1",
		Attachments: []Attachment{{
			Type: AttachmentSkill, Name: "Review skill", ResourceID: "skill_1",
			Realization: RealizationBind, Required: true,
		}},
	})
	require.NoError(t, err)
	v1 := role.RoleVersionID
	description := "v2"
	updated, err := svc.UpdateRoleOperation(
		ctx, "wsp_1", "usr_1", role.ID,
		MutationOperation{OperationKey: "op-update-before-apply", RequestDigest: digestForTest("v2")},
		v1, RoleUpdatePatch{Description: &description},
	)
	require.NoError(t, err)

	_, err = svc.ApplyRoleOperation(
		ctx, "wsp_1", "usr_1", role.ID,
		MutationOperation{OperationKey: "op-apply-missing-version", RequestDigest: digestForTest("missing-version")},
		RealizationRequest{AgentID: "agt_1", Apply: true},
	)
	require.ErrorIs(t, err, ErrRoleVersionConflict)
	require.Empty(t, skills.calls)

	_, err = svc.ApplyRoleOperation(
		ctx, "wsp_1", "usr_1", role.ID,
		MutationOperation{OperationKey: "op-apply-old", RequestDigest: digestForTest("old")},
		RealizationRequest{AgentID: "agt_1", RoleVersionID: v1, Apply: true},
	)
	require.ErrorIs(t, err, ErrRoleVersionConflict)
	require.Empty(t, skills.calls)

	operation := MutationOperation{OperationKey: "op-apply-1", RequestDigest: digestForTest("apply-current")}
	input := RealizationRequest{AgentID: "agt_1", RoleVersionID: updated.RoleVersionID, Apply: true}
	applied, err := svc.ApplyRoleOperation(ctx, "wsp_1", "usr_1", role.ID, operation, input)
	require.NoError(t, err)
	replayed, err := svc.ApplyRoleOperation(ctx, "wsp_1", "usr_1", role.ID, operation, input)
	require.NoError(t, err)
	requireStableApplyReceiptReplay(t, applied, replayed)
	require.Len(t, skills.calls, 1)

	_, err = svc.ApplyRoleOperation(ctx, "wsp_1", "usr_1", role.ID, MutationOperation{
		OperationKey: operation.OperationKey, RequestDigest: digestForTest("different-apply"),
	}, input)
	require.ErrorIs(t, err, ErrRoleMutationConflict)
	require.Len(t, skills.calls, 1)

	var realizationCount int64
	require.NoError(t, svc.store.DB().Model(&rolesstore.RoleRealization{}).Where("role_id = ?", role.ID).Count(&realizationCount).Error)
	require.EqualValues(t, 1, realizationCount)
}

func digestForTest(value string) string {
	return testSHA256Hex(value)
}

func roleStringForOperationTest(value string) *string {
	return &value
}

func cloneRolePackageContents(contents map[string]string) map[string]string {
	cloned := make(map[string]string, len(contents))
	for key, value := range contents {
		cloned[key] = value
	}
	return cloned
}
