package roles

import (
	"context"
	"path/filepath"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

func newRoleStoreForTest(t *testing.T) *Store {
	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(&Role{}, &RoleVersion{}, &RolePackageManifestItem{}, &RoleGrant{}, &RoleRealization{}, &RoleRealizationItem{}, &AgentRoleBinding{}, &RoleOperationReceipt{}))
	return New(db)
}

func TestListRolesScopesToCreatorGrantsAndWorkspaceVisibility(t *testing.T) {
	ctx := context.Background()
	store := newRoleStoreForTest(t)
	now := time.Date(2026, 6, 24, 1, 2, 3, 0, time.UTC)
	for _, role := range []Role{
		{ID: "role_owner", WorkspaceID: "wsp_1", Slug: "owner-role", Name: "Owner Role", TagsJSON: "[]", RuntimeHintsJSON: "{}", AttachmentsJSON: "[]", Definition: "owner", Visibility: "private", IsActive: true, CreatedBy: "usr_owner", UpdatedBy: "usr_owner", CreatedAt: now, UpdatedAt: now},
		{ID: "role_granted", WorkspaceID: "wsp_1", Slug: "granted-role", Name: "Granted Role", TagsJSON: "[]", RuntimeHintsJSON: "{}", AttachmentsJSON: "[]", Definition: "granted", Visibility: "private", IsActive: true, CreatedBy: "usr_other", UpdatedBy: "usr_other", CreatedAt: now, UpdatedAt: now},
		{ID: "role_shared", WorkspaceID: "wsp_1", Slug: "shared-role", Name: "Shared Role", TagsJSON: "[]", RuntimeHintsJSON: "{}", AttachmentsJSON: "[]", Definition: "shared", Visibility: "workspace", IsActive: true, CreatedBy: "usr_other", UpdatedBy: "usr_other", CreatedAt: now.Add(time.Minute), UpdatedAt: now.Add(time.Minute)},
		{ID: "role_unseen", WorkspaceID: "wsp_1", Slug: "unseen-role", Name: "Unseen Role", TagsJSON: "[]", RuntimeHintsJSON: "{}", AttachmentsJSON: "[]", Definition: "unseen", Visibility: "private", IsActive: true, CreatedBy: "usr_other", UpdatedBy: "usr_other", CreatedAt: now, UpdatedAt: now},
	} {
		_, err := store.CreateRole(ctx, role)
		require.NoError(t, err)
	}
	_, err := store.UpsertRoleGrant(ctx, RoleGrant{ID: "rgr_viewer", WorkspaceID: "wsp_1", RoleID: "role_granted", SubjectType: "user", SubjectID: "usr_viewer", Role: "view", CreatedBy: "usr_other", UpdatedBy: "usr_other", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)

	ownerRoles, ownerTotal, err := store.ListRoles(ctx, "wsp_1", "usr_owner", "all", "", 1, 20)
	require.NoError(t, err)
	require.EqualValues(t, 2, ownerTotal)
	require.Equal(t, []string{"role_shared", "role_owner"}, []string{ownerRoles[0].ID, ownerRoles[1].ID})

	viewerRoles, viewerTotal, err := store.ListRoles(ctx, "wsp_1", "usr_viewer", "all", "", 1, 20)
	require.NoError(t, err)
	require.EqualValues(t, 2, viewerTotal)
	require.Equal(t, []string{"role_shared", "role_granted"}, []string{viewerRoles[0].ID, viewerRoles[1].ID})

	outsiderRoles, outsiderTotal, err := store.ListRoles(ctx, "wsp_1", "usr_outsider", "all", "", 1, 20)
	require.NoError(t, err)
	require.EqualValues(t, 1, outsiderTotal)
	require.Equal(t, []string{"role_shared"}, []string{outsiderRoles[0].ID})

	workspaceRoles, workspaceTotal, err := store.ListRoles(ctx, "wsp_1", "usr_outsider", "workspace", "", 1, 20)
	require.NoError(t, err)
	require.EqualValues(t, 1, workspaceTotal)
	require.Equal(t, "role_shared", workspaceRoles[0].ID)

	privateRoles, privateTotal, err := store.ListRoles(ctx, "wsp_1", "usr_viewer", "private", "", 1, 20)
	require.NoError(t, err)
	require.EqualValues(t, 1, privateTotal)
	require.Equal(t, "role_granted", privateRoles[0].ID)

	createdRoles, createdTotal, err := store.ListRoles(ctx, "wsp_1", "usr_owner", "created_by_me", "", 1, 20)
	require.NoError(t, err)
	require.EqualValues(t, 1, createdTotal)
	require.Equal(t, "role_owner", createdRoles[0].ID)
}

func TestAuthoritativeRoleRowsAndSoftDelete(t *testing.T) {
	ctx := context.Background()
	store := newRoleStoreForTest(t)
	now := time.Date(2026, 6, 26, 1, 2, 3, 0, time.UTC)
	role, err := store.CreateRole(ctx, Role{ID: "role_1", WorkspaceID: "wsp_1", OwnerType: "user", OwnerID: "usr_1", SourceKind: "manual", Visibility: "private", ActiveVersionID: "rlv_1", Slug: "role-1", Name: "Role 1", Description: "desc", TagsJSON: "[]", RuntimeHintsJSON: "{}", AttachmentsJSON: "[]", Definition: "definition", IsActive: true, CreatedBy: "usr_1", UpdatedBy: "usr_1", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	require.Equal(t, "usr_1", role.OwnerID)

	db := store.DB()
	require.NoError(t, db.Create(&RoleVersion{ID: "rlv_1", RoleID: role.ID, WorkspaceID: role.WorkspaceID, Version: 1, Status: "published", Definition: "definition", RuntimeHintsJSON: "{}", ProvenanceJSON: "{}", NonPortableSummaryJSON: "[]", CreatedBy: "usr_1", CreatedAt: now}).Error)
	require.NoError(t, db.Create(&RolePackageManifestItem{ID: "rpmi_def", RoleVersionID: "rlv_1", RoleID: role.ID, WorkspaceID: role.WorkspaceID, ItemType: "role_definition_asset", Name: "IDENTITY.md", Description: "identity", Required: true, RealizationAction: "materialize", SourceKind: "inline", Content: "definition", SortOrder: 0, MetadataJSON: "{}"}).Error)
	require.NoError(t, db.Create(&RolePackageManifestItem{ID: "rpmi_mem", RoleVersionID: "rlv_1", RoleID: role.ID, WorkspaceID: role.WorkspaceID, ItemType: "memory", Name: "Seed memory", Description: "memory seed", Required: false, RealizationAction: "fork", SourceKind: "inline", Content: "notes", SortOrder: 1, MetadataJSON: `{"advancedSharedMemory":false}`}).Error)
	_, err = store.UpsertRoleGrant(ctx, RoleGrant{ID: "rgr_1", WorkspaceID: role.WorkspaceID, RoleID: role.ID, SubjectType: "user", SubjectID: "usr_2", Role: "view", CreatedBy: "usr_1", UpdatedBy: "usr_1", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	require.NoError(t, db.Create(&RoleRealization{ID: "rrl_1", WorkspaceID: role.WorkspaceID, AgentID: "agt_1", RoleID: role.ID, RoleVersionID: "rlv_1", Status: "realized", DryRun: false, CreatedBy: "usr_1", CreatedAt: now}).Error)
	require.NoError(t, db.Create(&RoleRealizationItem{ID: "rri_1", RealizationID: "rrl_1", ManifestItemID: "rpmi_mem", ManifestItemType: "memory", ManifestItemName: "Seed memory", Action: "fork", Status: "realized", ResourceID: "mem_1", Message: "created", Required: false, SortOrder: 1}).Error)
	require.NoError(t, db.Create(&AgentRoleBinding{ID: "arb_1", WorkspaceID: role.WorkspaceID, AgentID: "agt_1", BindingKind: "my_role", RoleID: role.ID, RoleVersionID: "rlv_1", ActiveDefinitionPath: "memory/system/identity.md", RealizationID: "rrl_1", CreatedBy: "usr_1", UpdatedBy: "usr_1", CreatedAt: now, UpdatedAt: now}).Error)

	var versions []RoleVersion
	require.NoError(t, db.Where("role_id = ?", role.ID).Find(&versions).Error)
	require.Len(t, versions, 1)
	require.Equal(t, 1, versions[0].Version)

	list, total, err := store.ListRoles(ctx, "wsp_1", "usr_2", "all", "", 1, 20)
	require.NoError(t, err)
	require.Len(t, list, 1)
	require.EqualValues(t, 1, total)

	deletedAt := now.Add(time.Hour)
	require.NoError(t, db.Model(&Role{}).Where("id = ?", role.ID).Update("deleted_at", &deletedAt).Error)
	list, total, err = store.ListRoles(ctx, "wsp_1", "usr_2", "all", "", 1, 20)
	require.NoError(t, err)
	require.Empty(t, list)
	require.Zero(t, total)

	var itemCount int64
	require.NoError(t, db.Model(&RolePackageManifestItem{}).Where("role_version_id = ?", "rlv_1").Count(&itemCount).Error)
	require.EqualValues(t, 2, itemCount)
}

func TestUpdateRoleVisibilityUpdatesMetadataOnly(t *testing.T) {
	ctx := context.Background()
	store := newRoleStoreForTest(t)
	now := time.Date(2026, 6, 26, 1, 2, 3, 0, time.UTC)
	role, err := store.CreateRole(ctx, Role{ID: "role_1", WorkspaceID: "wsp_1", OwnerType: "user", OwnerID: "usr_1", SourceKind: "manual", Visibility: "private", ActiveVersionID: "rlv_1", Slug: "role-1", Name: "Role 1", Description: "desc", TagsJSON: "[]", RuntimeHintsJSON: "{}", AttachmentsJSON: "[]", Definition: "definition", IsActive: true, CreatedBy: "usr_1", UpdatedBy: "usr_1", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	require.NoError(t, store.DB().Create(&RoleVersion{ID: "rlv_1", RoleID: role.ID, WorkspaceID: role.WorkspaceID, Version: 1, Status: "published", Definition: "definition", RuntimeHintsJSON: "{}", ProvenanceJSON: "{}", NonPortableSummaryJSON: "[]", CreatedBy: "usr_1", CreatedAt: now}).Error)

	updatedAt := now.Add(time.Hour)
	updated, err := store.UpdateRoleVisibility(ctx, "wsp_1", role.ID, "workspace", "usr_1", updatedAt)
	require.NoError(t, err)
	require.Equal(t, "workspace", updated.Visibility)
	require.Equal(t, "usr_1", updated.UpdatedBy)
	require.Equal(t, updatedAt, updated.UpdatedAt)

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

func TestListRoleVersionsUsesBoundedStableLookahead(t *testing.T) {
	ctx := context.Background()
	store := newRoleStoreForTest(t)
	now := time.Date(2026, 8, 21, 3, 0, 0, 0, time.UTC)
	for _, version := range []RoleVersion{
		{ID: "rlv_a", RoleID: "role_1", WorkspaceID: "wsp_1", Version: 1, Status: "published", Definition: "legacy", RuntimeHintsJSON: "{}", ProvenanceJSON: "{}", NonPortableSummaryJSON: "[]", CreatedBy: "usr_1", CreatedAt: now.Add(-time.Minute)},
		{ID: "rlv_b", RoleID: "role_1", WorkspaceID: "wsp_1", Version: 2, Status: "published", Definition: "native b", RuntimeHintsJSON: "{}", ProvenanceJSON: "{}", NonPortableSummaryJSON: "[]", ContentID: "ctn_1", ContentVersionID: "ctv_b", CreatedBy: "usr_1", CreatedAt: now},
		{ID: "rlv_c", RoleID: "role_1", WorkspaceID: "wsp_1", Version: 3, Status: "published", Definition: "native c", RuntimeHintsJSON: "{}", ProvenanceJSON: "{}", NonPortableSummaryJSON: "[]", ContentID: "ctn_1", ContentVersionID: "ctv_c", CreatedBy: "usr_1", CreatedAt: now},
		{ID: "rlv_d", RoleID: "role_1", WorkspaceID: "wsp_1", Version: 4, Status: "published", Definition: "native d", RuntimeHintsJSON: "{}", ProvenanceJSON: "{}", NonPortableSummaryJSON: "[]", ContentID: "ctn_1", ContentVersionID: "ctv_d", CreatedBy: "usr_1", CreatedAt: now.Add(time.Minute)},
		{ID: "rlv_other_lineage", RoleID: "role_1", WorkspaceID: "wsp_1", Version: 5, Status: "published", Definition: "other lineage", RuntimeHintsJSON: "{}", ProvenanceJSON: "{}", NonPortableSummaryJSON: "[]", ContentID: "ctn_other", ContentVersionID: "ctv_other_lineage", CreatedBy: "usr_1", CreatedAt: now.Add(2 * time.Hour)},
		{ID: "rlv_other", RoleID: "role_2", WorkspaceID: "wsp_1", Version: 1, Status: "published", Definition: "other", RuntimeHintsJSON: "{}", ProvenanceJSON: "{}", NonPortableSummaryJSON: "[]", ContentID: "ctn_2", ContentVersionID: "ctv_other", CreatedBy: "usr_1", CreatedAt: now.Add(time.Hour)},
	} {
		require.NoError(t, store.DB().Create(&version).Error)
	}

	first, err := store.ListRoleVersions(ctx, "wsp_1", "role_1", "ctn_1", 2, 0)
	require.NoError(t, err)
	require.Equal(t, []string{"rlv_d", "rlv_c", "rlv_b"}, []string{first[0].ID, first[1].ID, first[2].ID})
	second, err := store.ListRoleVersions(ctx, "wsp_1", "role_1", "ctn_1", 2, 2)
	require.NoError(t, err)
	require.Equal(t, []string{"rlv_b"}, []string{second[0].ID})
	_, err = store.ListRoleVersions(ctx, "wsp_1", "role_1", "ctn_1", MaxRoleVersionPageSize+1, 0)
	require.Error(t, err)
	_, err = store.ListRoleVersions(ctx, "wsp_1", "role_1", "ctn_1", 1, MaxRoleVersionPageOffset+1)
	require.Error(t, err)
	_, err = store.ListRoleVersions(ctx, "wsp_1", "role_1", "", 1, 0)
	require.Error(t, err)
}

func TestRoleSlugExistsIncludesSoftDeletedRows(t *testing.T) {
	ctx := context.Background()
	store := newRoleStoreForTest(t)
	now := time.Date(2026, 6, 26, 1, 2, 3, 0, time.UTC)
	role, err := store.CreateRole(ctx, Role{ID: "role_1", WorkspaceID: "wsp_1", OwnerType: "user", OwnerID: "usr_1", SourceKind: "manual", Visibility: "private", ActiveVersionID: "rlv_1", Slug: "role-1", Name: "Role 1", Description: "desc", TagsJSON: "[]", RuntimeHintsJSON: "{}", AttachmentsJSON: "[]", Definition: "definition", IsActive: true, CreatedBy: "usr_1", UpdatedBy: "usr_1", CreatedAt: now, UpdatedAt: now})
	require.NoError(t, err)
	require.NoError(t, store.DeleteRole(ctx, role.WorkspaceID, role.ID, now.Add(time.Hour)))

	exists, err := store.RoleSlugExists(ctx, "wsp_1", "role-1", "")
	require.NoError(t, err)
	require.True(t, exists)
}

func TestCreateRoleMapsSlugConflict(t *testing.T) {
	ctx := context.Background()
	store := newRoleStoreForTest(t)
	now := time.Date(2026, 6, 26, 1, 2, 3, 0, time.UTC)
	row := Role{ID: "role_1", WorkspaceID: "wsp_1", OwnerType: "user", OwnerID: "usr_1", SourceKind: "manual", Visibility: "private", ActiveVersionID: "rlv_1", Slug: "role-1", Name: "Role 1", Description: "desc", TagsJSON: "[]", RuntimeHintsJSON: "{}", AttachmentsJSON: "[]", Definition: "definition", IsActive: true, CreatedBy: "usr_1", UpdatedBy: "usr_1", CreatedAt: now, UpdatedAt: now}
	_, err := store.CreateRole(ctx, row)
	require.NoError(t, err)

	row.ID = "role_2"
	_, err = store.CreateRole(ctx, row)
	require.ErrorIs(t, err, ErrRoleSlugConflict)
}

func TestSaveRoleRealizationUpsertsCurrentAgentBinding(t *testing.T) {
	ctx := context.Background()
	store := newRoleStoreForTest(t)
	now := time.Date(2026, 6, 27, 1, 2, 3, 0, time.UTC)

	require.NoError(t, store.SaveRoleRealization(ctx, RealizationWrite{
		Realization: RoleRealization{ID: "rrl_1", WorkspaceID: "wsp_1", AgentID: "agt_1", RoleID: "role_1", RoleVersionID: "rlv_1", Status: "realized", DryRun: false, CreatedBy: "usr_1", CreatedAt: now},
		Items:       []RoleRealizationItem{{ID: "rri_1", RealizationID: "rrl_1", ManifestItemID: "rpmi_1", ManifestItemType: "memory", ManifestItemName: "Identity", Action: "create", Status: "realized", ResourceID: "mem_1", Message: "created", Required: true, SortOrder: 1}},
		Binding:     &AgentRoleBinding{ID: "arb_1", WorkspaceID: "wsp_1", AgentID: "agt_1", BindingKind: "my_role", RoleID: "role_1", RoleVersionID: "rlv_1", ActiveDefinitionPath: "memory/system/identity.md", RealizationID: "rrl_1", CreatedBy: "usr_1", UpdatedBy: "usr_1", CreatedAt: now, UpdatedAt: now},
	}))

	require.NoError(t, store.SaveRoleRealization(ctx, RealizationWrite{
		Realization: RoleRealization{ID: "rrl_2", WorkspaceID: "wsp_1", AgentID: "agt_1", RoleID: "role_2", RoleVersionID: "rlv_2", Status: "partial", DryRun: false, CreatedBy: "usr_1", CreatedAt: now.Add(time.Minute)},
		Binding:     &AgentRoleBinding{ID: "arb_2", WorkspaceID: "wsp_1", AgentID: "agt_1", BindingKind: "preset_derived", RoleID: "role_2", RoleVersionID: "rlv_2", SourcePresetRoleID: "research-agent", SourcePresetVersionID: "3", ActiveDefinitionPath: "memory/system/identity.md", RealizationID: "rrl_2", CreatedBy: "usr_1", UpdatedBy: "usr_1", CreatedAt: now.Add(time.Minute), UpdatedAt: now.Add(time.Minute)},
	}))

	var realizations []RoleRealization
	require.NoError(t, store.DB().Order("created_at ASC").Find(&realizations).Error)
	require.Len(t, realizations, 2)

	var items []RoleRealizationItem
	require.NoError(t, store.DB().Find(&items).Error)
	require.Len(t, items, 1)

	var binding AgentRoleBinding
	require.NoError(t, store.DB().Where("workspace_id = ? AND agent_id = ?", "wsp_1", "agt_1").First(&binding).Error)
	require.Equal(t, "preset_derived", binding.BindingKind)
	require.Equal(t, "role_2", binding.RoleID)
	require.Equal(t, "rlv_2", binding.RoleVersionID)
	require.Equal(t, "research-agent", binding.SourcePresetRoleID)
	require.Equal(t, "3", binding.SourcePresetVersionID)
	require.Equal(t, "memory/system/identity.md", binding.ActiveDefinitionPath)
	require.Equal(t, "rrl_2", binding.RealizationID)
}

func TestSaveRoleRealizationRollsBackOnItemFailure(t *testing.T) {
	ctx := context.Background()
	store := newRoleStoreForTest(t)
	now := time.Date(2026, 6, 27, 1, 2, 3, 0, time.UTC)

	err := store.SaveRoleRealization(ctx, RealizationWrite{
		Realization: RoleRealization{ID: "rrl_rollback", WorkspaceID: "wsp_1", AgentID: "agt_1", RoleID: "role_1", RoleVersionID: "rlv_1", Status: "realized", DryRun: false, CreatedBy: "usr_1", CreatedAt: now},
		Items: []RoleRealizationItem{
			{ID: "rri_duplicate", RealizationID: "rrl_rollback", ManifestItemType: "memory", ManifestItemName: "one", Status: "realized", SortOrder: 0},
			{ID: "rri_duplicate", RealizationID: "rrl_rollback", ManifestItemType: "memory", ManifestItemName: "two", Status: "realized", SortOrder: 1},
		},
	})
	require.Error(t, err)

	var realizationCount int64
	require.NoError(t, store.DB().Model(&RoleRealization{}).Where("id = ?", "rrl_rollback").Count(&realizationCount).Error)
	require.Zero(t, realizationCount)
}
