// Copyright (c) 2024 Warden Project
// SPDX-License-Identifier: MPL-2.0

package jwt

import (
	"net/http"
	"testing"
	"time"

	sdklogical "github.com/openbao/openbao/sdk/v2/logical"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/stephnangue/warden/framework"
	"github.com/stephnangue/warden/logical"
)

// =============================================================================
// Role Management Tests
// =============================================================================

func TestPathRole_Create(t *testing.T) {
	b, ctx := createTestBackendWithStorage(t)

	fieldData := &framework.FieldData{
		Raw: map[string]any{
			"name":            "test-role",
			"bound_audiences": []string{"aud1", "aud2"},
			"bound_subject":   "test-subject",
			"token_policies":  []string{"policy1", "policy2"},
			"token_ttl":       3600,
			"user_claim":      "email",
			"cred_spec_name":  "aws-dev",
		},
		Schema: map[string]*framework.FieldSchema{
			"name":            {Type: framework.TypeString},
			"bound_audiences": {Type: framework.TypeCommaStringSlice},
			"bound_subject":   {Type: framework.TypeString},
			"token_policies":  {Type: framework.TypeCommaStringSlice},
			"token_ttl":       {Type: framework.TypeDurationSecond, Default: 3600},
			"user_claim":      {Type: framework.TypeString, Default: "sub"},
			"cred_spec_name":  {Type: framework.TypeString},
		},
	}

	resp, err := b.handleRoleCreate(ctx, &logical.Request{}, fieldData)
	require.NoError(t, err)
	assert.Equal(t, http.StatusCreated, resp.StatusCode)

	// Verify role was stored
	role, err := b.getRole(ctx, "test-role")
	require.NoError(t, err)
	require.NotNil(t, role)
	assert.Equal(t, "test-role", role.Name)
	assert.Equal(t, []string{"aud1", "aud2"}, role.BoundAudiences)
	assert.Equal(t, "test-subject", role.BoundSubject)
	assert.Equal(t, []string{"policy1", "policy2"}, role.TokenPolicies)
	assert.Equal(t, time.Hour.String(), role.TokenTTL)
	assert.Equal(t, "jwt_role", role.TokenType)
	assert.Equal(t, "email", role.UserClaim)
	assert.Equal(t, "aws-dev", role.CredSpecName)
}

func TestPathRole_CreateDuplicate(t *testing.T) {
	b, ctx := createTestBackendWithStorage(t)

	// Create first role
	role := &JWTRole{
		Name:          "existing-role",
		TokenPolicies: []string{"policy1"},
		TokenTTL:      time.Hour.String(),
	}
	err := b.setRole(ctx, role)
	require.NoError(t, err)

	// Try to create duplicate
	fieldData := &framework.FieldData{
		Raw: map[string]any{
			"name": "existing-role",
		},
		Schema: map[string]*framework.FieldSchema{
			"name":            {Type: framework.TypeString},
			"bound_audiences": {Type: framework.TypeCommaStringSlice},
			"bound_subject":   {Type: framework.TypeString},
			"token_policies":  {Type: framework.TypeCommaStringSlice},
			"token_ttl":       {Type: framework.TypeDurationSecond, Default: 3600},
			"user_claim":      {Type: framework.TypeString, Default: "sub"},
			"cred_spec_name":  {Type: framework.TypeString},
		},
	}

	resp, err := b.handleRoleCreate(ctx, &logical.Request{}, fieldData)
	require.NoError(t, err)
	assert.Equal(t, http.StatusConflict, resp.StatusCode)
}

func TestPathRole_Read(t *testing.T) {
	b, ctx := createTestBackendWithStorage(t)

	// Create role
	role := &JWTRole{
		Name:           "read-test-role",
		BoundAudiences: []string{"aud1"},
		BoundSubject:   "sub1",
		TokenPolicies:  []string{"policy1"},
		TokenTTL:       (2 * time.Hour).String(),
		UserClaim:      "preferred_username",
		CredSpecName:   "aws-prod",
	}
	err := b.setRole(ctx, role)
	require.NoError(t, err)

	fieldData := &framework.FieldData{
		Raw: map[string]any{
			"name": "read-test-role",
		},
		Schema: map[string]*framework.FieldSchema{
			"name": {Type: framework.TypeString},
		},
	}

	resp, err := b.handleRoleRead(ctx, &logical.Request{}, fieldData)
	require.NoError(t, err)
	assert.Equal(t, http.StatusOK, resp.StatusCode)
	assert.Equal(t, "read-test-role", resp.Data["name"])
	assert.Equal(t, "sub1", resp.Data["bound_subject"])
	assert.Equal(t, "preferred_username", resp.Data["user_claim"])
	assert.Equal(t, "aws-prod", resp.Data["cred_spec_name"])
}

func TestPathRole_ReadNotFound(t *testing.T) {
	b, ctx := createTestBackendWithStorage(t)

	fieldData := &framework.FieldData{
		Raw: map[string]any{
			"name": "nonexistent-role",
		},
		Schema: map[string]*framework.FieldSchema{
			"name": {Type: framework.TypeString},
		},
	}

	resp, err := b.handleRoleRead(ctx, &logical.Request{}, fieldData)
	require.NoError(t, err)
	assert.Equal(t, http.StatusNotFound, resp.StatusCode)
}

func TestPathRole_Update(t *testing.T) {
	b, ctx := createTestBackendWithStorage(t)

	// Create initial role
	role := &JWTRole{
		Name:           "update-test-role",
		BoundAudiences: []string{"aud1"},
		TokenPolicies:  []string{"policy1"},
		TokenTTL:       time.Hour.String(),
		UserClaim:      "sub",
	}
	err := b.setRole(ctx, role)
	require.NoError(t, err)

	// Update role
	fieldData := &framework.FieldData{
		Raw: map[string]any{
			"name":           "update-test-role",
			"token_policies": []string{"policy1", "policy2", "policy3"},
			"token_ttl":      7200,
			"cred_spec_name": "new-aws-spec",
		},
		Schema: map[string]*framework.FieldSchema{
			"name":            {Type: framework.TypeString},
			"bound_audiences": {Type: framework.TypeCommaStringSlice},
			"bound_subject":   {Type: framework.TypeString},
			"token_policies":  {Type: framework.TypeCommaStringSlice},
			"token_ttl":       {Type: framework.TypeDurationSecond},
			"user_claim":      {Type: framework.TypeString},
			"cred_spec_name":  {Type: framework.TypeString},
		},
	}

	resp, err := b.handleRoleUpdate(ctx, &logical.Request{}, fieldData)
	require.NoError(t, err)
	assert.Equal(t, http.StatusOK, resp.StatusCode)

	// Verify updates
	updatedRole, err := b.getRole(ctx, "update-test-role")
	require.NoError(t, err)
	assert.Len(t, updatedRole.TokenPolicies, 3)
	assert.Equal(t, (2 * time.Hour).String(), updatedRole.TokenTTL)
	assert.Equal(t, "new-aws-spec", updatedRole.CredSpecName)
	// Verify unchanged fields
	assert.Len(t, updatedRole.BoundAudiences, 1)
}

// TestPathRole_UpdateCreatesIfNotExists verifies the upsert pattern -
// UpdateOperation creates the role if it doesn't exist.
func TestPathRole_UpdateCreatesIfNotExists(t *testing.T) {
	b, ctx := createTestBackendWithStorage(t)

	fieldData := &framework.FieldData{
		Raw: map[string]any{
			"name":      "new-role-via-update",
			"token_ttl": 3600,
		},
		Schema: map[string]*framework.FieldSchema{
			"name":      {Type: framework.TypeString},
			"token_ttl": {Type: framework.TypeDurationSecond},
		},
	}

	resp, err := b.handleRoleUpdate(ctx, &logical.Request{}, fieldData)
	require.NoError(t, err)
	assert.Equal(t, http.StatusCreated, resp.StatusCode)
	assert.Contains(t, resp.Data["message"], "created")

	// Verify the role was created
	createdRole, err := b.getRole(ctx, "new-role-via-update")
	require.NoError(t, err)
	require.NotNil(t, createdRole)
	assert.Equal(t, "new-role-via-update", createdRole.Name)
	assert.Equal(t, time.Hour.String(), createdRole.TokenTTL)
	assert.Equal(t, "jwt_role", createdRole.TokenType)
	// Verify defaults were applied
	assert.Equal(t, "sub", createdRole.UserClaim)
}

func TestPathRole_Delete(t *testing.T) {
	b, ctx := createTestBackendWithStorage(t)

	// Create role
	role := &JWTRole{
		Name:          "delete-test-role",
		TokenPolicies: []string{"policy1"},
		TokenTTL:      time.Hour.String(),
	}
	err := b.setRole(ctx, role)
	require.NoError(t, err)

	// Verify exists
	existingRole, _ := b.getRole(ctx, "delete-test-role")
	require.NotNil(t, existingRole)

	fieldData := &framework.FieldData{
		Raw: map[string]any{
			"name": "delete-test-role",
		},
		Schema: map[string]*framework.FieldSchema{
			"name": {Type: framework.TypeString},
		},
	}

	resp, err := b.handleRoleDelete(ctx, &logical.Request{}, fieldData)
	require.NoError(t, err)
	assert.Equal(t, http.StatusOK, resp.StatusCode)

	// Verify deleted
	deletedRole, _ := b.getRole(ctx, "delete-test-role")
	assert.Nil(t, deletedRole)
}

func TestPathRole_List(t *testing.T) {
	b, ctx := createTestBackendWithStorage(t)

	// Create multiple roles
	roles := []string{"role-a", "role-b", "role-c"}
	for _, name := range roles {
		role := &JWTRole{
			Name:          name,
			TokenPolicies: []string{"policy1"},
			TokenTTL:      time.Hour.String(),
		}
		err := b.setRole(ctx, role)
		require.NoError(t, err)
	}

	fieldData := &framework.FieldData{
		Raw:    map[string]any{},
		Schema: map[string]*framework.FieldSchema{},
	}

	resp, err := b.handleRoleList(ctx, &logical.Request{}, fieldData)
	require.NoError(t, err)
	assert.Equal(t, http.StatusOK, resp.StatusCode)

	keys, ok := resp.Data["keys"].([]string)
	require.True(t, ok)
	assert.Len(t, keys, 3)
}

func TestPathRole_ListEmpty(t *testing.T) {
	b, ctx := createTestBackendWithStorage(t)

	fieldData := &framework.FieldData{
		Raw:    map[string]any{},
		Schema: map[string]*framework.FieldSchema{},
	}

	resp, err := b.handleRoleList(ctx, &logical.Request{}, fieldData)
	require.NoError(t, err)
	assert.Equal(t, http.StatusOK, resp.StatusCode)

	keys, ok := resp.Data["keys"].([]string)
	require.True(t, ok)
	assert.Len(t, keys, 0)
}

func TestRole_StorageRoundTrip(t *testing.T) {
	b, ctx := createTestBackendWithStorage(t)

	original := &JWTRole{
		Name:           "roundtrip-role",
		BoundAudiences: []string{"aud1", "aud2"},
		BoundSubject:   "test-subject",
		TokenPolicies:  []string{"policy1", "policy2"},
		TokenTTL:       (2*time.Hour + 30*time.Minute).String(),
		UserClaim:      "preferred_username",
		CredSpecName:   "aws-test-spec",
	}

	// Store
	err := b.setRole(ctx, original)
	require.NoError(t, err)

	// Retrieve
	retrieved, err := b.getRole(ctx, "roundtrip-role")
	require.NoError(t, err)

	// Compare
	assert.Equal(t, original.Name, retrieved.Name)
	assert.Equal(t, original.BoundAudiences, retrieved.BoundAudiences)
	assert.Equal(t, original.BoundSubject, retrieved.BoundSubject)
	assert.Equal(t, original.TokenPolicies, retrieved.TokenPolicies)
	assert.Equal(t, original.TokenTTL, retrieved.TokenTTL)
	assert.Equal(t, original.UserClaim, retrieved.UserClaim)
	assert.Equal(t, original.CredSpecName, retrieved.CredSpecName)
}

func TestRole_Defaults(t *testing.T) {
	b, ctx := createTestBackendWithStorage(t)

	fieldData := &framework.FieldData{
		Raw: map[string]any{
			"name": "defaults-role",
		},
		Schema: map[string]*framework.FieldSchema{
			"name":            {Type: framework.TypeString},
			"bound_audiences": {Type: framework.TypeCommaStringSlice},
			"bound_subject":   {Type: framework.TypeString},
			"token_policies":  {Type: framework.TypeCommaStringSlice},
			"token_ttl":       {Type: framework.TypeDurationSecond, Default: 3600},
			"user_claim":      {Type: framework.TypeString, Default: "sub"},
			"cred_spec_name":  {Type: framework.TypeString},
		},
	}

	_, err := b.handleRoleCreate(ctx, &logical.Request{}, fieldData)
	require.NoError(t, err)

	role, err := b.getRole(ctx, "defaults-role")
	require.NoError(t, err)

	// Verify defaults were applied
	assert.Equal(t, "sub", role.UserClaim)
	assert.Equal(t, time.Hour.String(), role.TokenTTL)
}

// =============================================================================
// Path Structure Tests
// =============================================================================

func TestPathRole_Pattern(t *testing.T) {
	b, _ := createTestBackendWithStorage(t)
	path := b.pathRole()

	assert.Equal(t, "role/"+framework.GenericNameRegex("name"), path.Pattern)
}

func TestPathRole_Fields(t *testing.T) {
	b, _ := createTestBackendWithStorage(t)
	path := b.pathRole()

	// Check all expected fields exist
	expectedFields := []string{
		"name", "bound_audiences", "bound_subject", "bound_claims",
		"bound_uri_patterns", "uri_claim", "token_policies",
		"token_ttl", "user_claim", "cred_spec_name",
		"max_age",
	}

	for _, field := range expectedFields {
		_, exists := path.Fields[field]
		assert.True(t, exists, "Field %s should exist", field)
	}
}

func TestPathRole_Operations(t *testing.T) {
	b, _ := createTestBackendWithStorage(t)
	path := b.pathRole()

	// Check operations exist
	_, hasCreate := path.Operations[logical.CreateOperation]
	_, hasRead := path.Operations[logical.ReadOperation]
	_, hasUpdate := path.Operations[logical.UpdateOperation]
	_, hasDelete := path.Operations[logical.DeleteOperation]

	assert.True(t, hasCreate, "Should have create operation")
	assert.True(t, hasRead, "Should have read operation")
	assert.True(t, hasUpdate, "Should have update operation")
	assert.True(t, hasDelete, "Should have delete operation")
}

func TestPathRoleList_Pattern(t *testing.T) {
	b, _ := createTestBackendWithStorage(t)
	path := b.pathRoleList()

	assert.Equal(t, "role/?$", path.Pattern)
}

func TestPathRoleList_Operations(t *testing.T) {
	b, _ := createTestBackendWithStorage(t)
	path := b.pathRoleList()

	_, hasList := path.Operations[logical.ListOperation]
	assert.True(t, hasList, "Should have list operation")
}

func TestPathRole_MaxAgeValidation(t *testing.T) {
	b, ctx := createTestBackendWithStorage(t)

	tests := []struct {
		name      string
		maxAge    string
		wantErr   bool
		errSubstr string
	}{
		{"valid 30m", "30m", false, ""},
		{"valid 1h", "1h", false, ""},
		{"valid 24h", "24h", false, ""},
		{"invalid format", "not-a-duration", true, "invalid max_age"},
		{"negative", "-5m", true, "must be a positive"},
		{"zero", "0s", true, "must be a positive"},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			fieldData := &framework.FieldData{
				Raw: map[string]any{
					"name":    "max-age-test-" + tc.name,
					"max_age": tc.maxAge,
				},
				Schema: map[string]*framework.FieldSchema{
					"name":       {Type: framework.TypeString},
					"max_age":    {Type: framework.TypeString},
					"token_ttl":  {Type: framework.TypeDurationSecond, Default: 3600},
					"user_claim": {Type: framework.TypeString, Default: "sub"},
				},
			}

			resp, err := b.handleRoleCreate(ctx, &logical.Request{}, fieldData)
			require.NoError(t, err)
			if tc.wantErr {
				assert.NotNil(t, resp.Err, "expected error for max_age=%q", tc.maxAge)
				if tc.errSubstr != "" {
					assert.Contains(t, resp.Err.Error(), tc.errSubstr)
				}
			} else {
				assert.Nil(t, resp.Err, "expected no error for max_age=%q", tc.maxAge)
				assert.Equal(t, http.StatusCreated, resp.StatusCode)
			}
		})
	}
}

func TestPathRole_MaxAgeRoundTrip(t *testing.T) {
	b, ctx := createTestBackendWithStorage(t)

	role := &JWTRole{
		Name:      "max-age-roundtrip",
		TokenTTL:  time.Hour.String(),
		UserClaim: "sub",
		MaxAge:    "30m",
	}
	err := b.setRole(ctx, role)
	require.NoError(t, err)

	retrieved, err := b.getRole(ctx, "max-age-roundtrip")
	require.NoError(t, err)
	assert.Equal(t, "30m", retrieved.MaxAge)

	maxAge, err := retrieved.ParseMaxAge()
	require.NoError(t, err)
	assert.Equal(t, 30*time.Minute, maxAge)
}

func TestHandleRoleUpdate_AllFields(t *testing.T) {
	b, ctx := createTestBackendWithStorage(t)

	// Create initial role
	role := &JWTRole{
		Name:      "full-update-role",
		TokenTTL:  time.Hour.String(),
		UserClaim: "sub",
		TokenType: "jwt_role",
	}
	err := b.setRole(ctx, role)
	require.NoError(t, err)

	fd := &framework.FieldData{
		Raw: map[string]any{
			"name":                "full-update-role",
			"bound_audiences":     []string{"new-aud"},
			"bound_subject":       "new-sub",
			"bound_claims":        map[string]any{"tenant": "acme"},
			"bound_uri_patterns":  []string{"spiffe://*"},
			"uri_claim":           "custom_claim",
			"token_policies":      []string{"new-policy"},
			"token_ttl":           7200,
			"user_claim":          "email",
			"cred_spec_name":      "aws-spec",
			"groups_claim":        "groups",
			"group_policy_prefix": "grp-",
			"max_age":             "1h",
		},
		Schema: map[string]*framework.FieldSchema{
			"name":                {Type: framework.TypeString},
			"bound_audiences":     {Type: framework.TypeCommaStringSlice},
			"bound_subject":       {Type: framework.TypeString},
			"bound_claims":        {Type: framework.TypeMap},
			"bound_uri_patterns":  {Type: framework.TypeCommaStringSlice},
			"uri_claim":           {Type: framework.TypeString},
			"token_policies":      {Type: framework.TypeCommaStringSlice},
			"token_ttl":           {Type: framework.TypeDurationSecond},
			"user_claim":          {Type: framework.TypeString},
			"cred_spec_name":      {Type: framework.TypeString},
			"groups_claim":        {Type: framework.TypeString},
			"group_policy_prefix": {Type: framework.TypeString},
			"max_age":             {Type: framework.TypeString},
		},
	}

	resp, err := b.handleRoleUpdate(ctx, &logical.Request{}, fd)
	require.NoError(t, err)
	assert.Equal(t, http.StatusOK, resp.StatusCode)

	updated, _ := b.getRole(ctx, "full-update-role")
	assert.Equal(t, []string{"new-aud"}, updated.BoundAudiences)
	assert.Equal(t, "new-sub", updated.BoundSubject)
	assert.Equal(t, "custom_claim", updated.URIClaim)
	assert.Equal(t, "email", updated.UserClaim)
	assert.Equal(t, "aws-spec", updated.CredSpecName)
	assert.Equal(t, "groups", updated.GroupsClaim)
	assert.Equal(t, "grp-", updated.GroupPolicyPrefix)
	assert.Equal(t, "1h", updated.MaxAge)
}

// =============================================================================
// validateRole Tests
// =============================================================================

func TestValidateRole_InvalidMaxAge(t *testing.T) {
	b, _ := createTestBackendWithStorage(t)

	role := &JWTRole{
		Name:   "bad-max-age",
		MaxAge: "invalid",
	}
	err := b.validateRole(role)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "invalid max_age")
}

func TestValidateRole_NegativeMaxAge(t *testing.T) {
	b, _ := createTestBackendWithStorage(t)

	role := &JWTRole{
		Name:   "neg-max-age",
		MaxAge: "-5m",
	}
	err := b.validateRole(role)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "must be a positive")
}

func TestValidateRole_InvalidTTL(t *testing.T) {
	b, _ := createTestBackendWithStorage(t)

	role := &JWTRole{
		Name:     "bad-ttl",
		TokenTTL: "not-a-duration",
	}
	err := b.validateRole(role)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "invalid token_ttl")
}

func TestValidateRole_DefaultsApplied(t *testing.T) {
	b, _ := createTestBackendWithStorage(t)

	role := &JWTRole{Name: "defaults"}
	err := b.validateRole(role)
	require.NoError(t, err)
	assert.Equal(t, "sub", role.UserClaim)
	assert.Equal(t, "jwt_role", role.TokenType)
	assert.Equal(t, time.Hour.String(), role.TokenTTL)
}

func TestValidateRole_URIPatternsSetDefaultClaim(t *testing.T) {
	b, _ := createTestBackendWithStorage(t)

	role := &JWTRole{
		Name:             "uri-defaults",
		BoundURIPatterns: []string{"spiffe://*"},
	}
	err := b.validateRole(role)
	require.NoError(t, err)
	assert.Equal(t, "sub", role.URIClaim)
}

// =============================================================================
// Initialize with storage
// =============================================================================

func TestBuildRoleFromFieldData_AllFields(t *testing.T) {
	b, _ := createTestBackendWithStorage(t)

	fd := &framework.FieldData{
		Raw: map[string]any{
			"name":                "full-role",
			"bound_audiences":     []string{"aud1"},
			"bound_subject":       "sub1",
			"bound_claims":        map[string]any{"k": "v"},
			"bound_uri_patterns":  []string{"spiffe://*"},
			"uri_claim":           "custom",
			"token_policies":      []string{"p1"},
			"token_ttl":           3600,
			"user_claim":          "email",
			"cred_spec_name":      "spec",
			"groups_claim":        "groups",
			"group_policy_prefix": "grp-",
			"max_age":             "30m",
		},
		Schema: map[string]*framework.FieldSchema{
			"name":                {Type: framework.TypeString},
			"bound_audiences":     {Type: framework.TypeCommaStringSlice},
			"bound_subject":       {Type: framework.TypeString},
			"bound_claims":        {Type: framework.TypeMap},
			"bound_uri_patterns":  {Type: framework.TypeCommaStringSlice},
			"uri_claim":           {Type: framework.TypeString},
			"token_policies":      {Type: framework.TypeCommaStringSlice},
			"token_ttl":           {Type: framework.TypeDurationSecond},
			"user_claim":          {Type: framework.TypeString},
			"cred_spec_name":      {Type: framework.TypeString},
			"groups_claim":        {Type: framework.TypeString},
			"group_policy_prefix": {Type: framework.TypeString},
			"max_age":             {Type: framework.TypeString},
		},
	}

	role := b.buildRoleFromFieldData("full-role", fd)
	assert.Equal(t, "full-role", role.Name)
	assert.Equal(t, []string{"aud1"}, role.BoundAudiences)
	assert.Equal(t, "sub1", role.BoundSubject)
	assert.Equal(t, "custom", role.URIClaim)
	assert.Equal(t, "email", role.UserClaim)
	assert.Equal(t, "spec", role.CredSpecName)
	assert.Equal(t, "groups", role.GroupsClaim)
	assert.Equal(t, "grp-", role.GroupPolicyPrefix)
	assert.Equal(t, "30m", role.MaxAge)
}

func TestPathRole_DescriptionRoundTrip(t *testing.T) {
	b, ctx := createTestBackendWithStorage(t)

	schema := map[string]*framework.FieldSchema{
		"name":        {Type: framework.TypeString},
		"description": {Type: framework.TypeString},
	}

	createFD := &framework.FieldData{
		Raw: map[string]any{
			"name":        "desc-role",
			"description": "read-only prod KV secrets",
		},
		Schema: schema,
	}
	resp, err := b.handleRoleCreate(ctx, &logical.Request{}, createFD)
	require.NoError(t, err)
	require.Equal(t, http.StatusCreated, resp.StatusCode)

	readFD := &framework.FieldData{
		Raw:    map[string]any{"name": "desc-role"},
		Schema: map[string]*framework.FieldSchema{"name": {Type: framework.TypeString}},
	}
	resp, err = b.handleRoleRead(ctx, &logical.Request{}, readFD)
	require.NoError(t, err)
	assert.Equal(t, "read-only prod KV secrets", resp.Data["description"])

	updateFD := &framework.FieldData{
		Raw: map[string]any{
			"name":        "desc-role",
			"description": "updated text",
		},
		Schema: schema,
	}
	_, err = b.handleRoleUpdate(ctx, &logical.Request{}, updateFD)
	require.NoError(t, err)

	role, err := b.getRole(ctx, "desc-role")
	require.NoError(t, err)
	assert.Equal(t, "updated text", role.Description)
}

func TestJWTRole_PreUpgradeDecodesWithEmptyDescription(t *testing.T) {
	// A role persisted before the description field existed has no
	// `description` key in its JSON. Ensure it decodes cleanly.
	b, ctx := createTestBackendWithStorage(t)

	raw := []byte(`{"name":"legacy","bound_audiences":["aud1"],"token_policies":["p"],"token_ttl":"1h0m0s","user_claim":"sub"}`)
	require.NoError(t, b.storageView.Put(ctx, &sdklogical.StorageEntry{
		Key:   rolePrefix + "legacy",
		Value: raw,
	}))

	role, err := b.getRole(ctx, "legacy")
	require.NoError(t, err)
	require.NotNil(t, role)
	assert.Equal(t, "", role.Description)
	assert.Equal(t, []string{"aud1"}, role.BoundAudiences)
}

// =============================================================================
// Initialize with persisted config
// =============================================================================
