package registry

import (
	"errors"
	"strings"
	"testing"
)

// TestValidateAppScopeSubset enforces only the scope-driven co-constraints that
// mirror the manifest rules in developer_manifest.go: a retained scope must keep
// its declared dependencies. It does NOT re-check R ⊆ S (callers enforce that
// separately) and does not mutate the capability.
func TestValidateAppScopeSubset(t *testing.T) {
	t.Parallel()

	// mentionApp mirrors a validated mention-only App capability: the mention
	// event is declared together with its required scopes.
	mentionApp := StoredAppRuntimeCapability{
		Supported: true,
		Events:    []string{AppEventMessageMention},
		Scopes:    []string{AppMentionScope(), AppMessageWriteScope()},
	}

	// notificationApp mirrors a validated announcement-only App capability: the
	// notifications capability is declared without the action event.
	notificationApp := StoredAppRuntimeCapability{
		Supported: true,
		Events:    []string{},
		Scopes:    []string{AppNotificationsWriteScope(), AppMessageWriteScope()},
		Notifications: StoredAppNotificationCapability{
			Supported: true,
			Targets:   []string{AppNotificationTargetHumanDM},
			Content:   []string{AppNotificationContentMessage},
		},
	}

	// actionApp mirrors a validated interactive-card action App capability: the
	// action event, interactive_card content, and all three scopes are declared.
	actionApp := StoredAppRuntimeCapability{
		Supported: true,
		Events:    []string{AppEventNotificationAction},
		Scopes: []string{
			AppNotificationActionsReadScope(),
			AppNotificationsWriteScope(),
			AppMessageWriteScope(),
		},
		Notifications: StoredAppNotificationCapability{
			Supported: true,
			Targets:   []string{AppNotificationTargetHumanDM},
			Content:   []string{AppNotificationContentInteractiveCard},
		},
	}

	// mentionAppWithoutMentionEvent is an inconsistent pinned capability
	// (mentions.read is declared without the agentteam.message.mention event). It can
	// never be produced by ValidateDeveloperManifest, so it is constructed by
	// hand to exercise the defensive co-constraint branch in isolation.
	mentionAppWithoutMentionEvent := StoredAppRuntimeCapability{
		Supported: true,
		Events:    []string{},
		Scopes:    []string{AppMentionScope(), AppMessageWriteScope()},
	}

	// notificationScopeWithoutCapability carries app.notifications.write without
	// declaring the notifications capability, exercising the defensive branch.
	notificationScopeWithoutCapability := StoredAppRuntimeCapability{
		Supported: true,
		Events:    []string{},
		Scopes:    []string{AppNotificationsWriteScope(), AppMessageWriteScope()},
	}

	// actionScopeWithoutActionEvent carries app.notification_actions.read without
	// the agentteam.app.notification.action event.
	actionScopeWithoutActionEvent := StoredAppRuntimeCapability{
		Supported: true,
		Events:    []string{},
		Scopes:    []string{AppNotificationActionsReadScope(), AppMessageWriteScope()},
		Notifications: StoredAppNotificationCapability{
			Supported: true,
			Targets:   []string{AppNotificationTargetHumanDM},
			Content:   []string{AppNotificationContentInteractiveCard},
		},
	}

	// actionScopeWithoutCardContent declares the action event and the action
	// scope but lacks the interactive_card notification content.
	actionScopeWithoutCardContent := StoredAppRuntimeCapability{
		Supported: true,
		Events:    []string{AppEventNotificationAction},
		Scopes:    []string{AppNotificationActionsReadScope(), AppNotificationsWriteScope(), AppMessageWriteScope()},
		Notifications: StoredAppNotificationCapability{
			Supported: true,
			Targets:   []string{AppNotificationTargetHumanDM},
			Content:   []string{AppNotificationContentMessage},
		},
	}

	tests := []struct {
		name       string
		capability StoredAppRuntimeCapability
		scopes     []string
		wantErr    string // empty expects no error; otherwise an expected error substring
	}{
		// Valid subsets — narrowing must keep each retained scope's dependencies.
		{
			name:       "mention app only messages.write",
			capability: mentionApp,
			scopes:     []string{AppMessageWriteScope()},
		},
		{
			name:       "mention app full pinned set",
			capability: mentionApp,
			scopes:     []string{AppMentionScope(), AppMessageWriteScope()},
		},
		{
			name:       "messages.write plus non-app profile.read is accepted",
			capability: mentionApp,
			scopes:     []string{AppMessageWriteScope(), "profile.read"},
		},
		{
			name:       "notification app full pinned set",
			capability: notificationApp,
			scopes:     []string{AppNotificationsWriteScope(), AppMessageWriteScope()},
		},
		{
			name:       "notification app drops notifications.write keeping messages.write",
			capability: notificationApp,
			scopes:     []string{AppMessageWriteScope()},
		},
		{
			name:       "action app full pinned set",
			capability: actionApp,
			scopes:     []string{AppNotificationActionsReadScope(), AppNotificationsWriteScope(), AppMessageWriteScope()},
		},
		{
			name:       "action app drops action scope keeping notifications and messages",
			capability: actionApp,
			scopes:     []string{AppNotificationsWriteScope(), AppMessageWriteScope()},
		},
		{
			name:       "action app only messages.write",
			capability: actionApp,
			scopes:     []string{AppMessageWriteScope()},
		},
		{
			name:       "empty scope set narrows to nothing",
			capability: mentionApp,
			scopes:     []string{},
		},

		// Co-constraint violations — a retained scope must keep its dependencies.
		{
			name:       "mentions.read kept but messages.write dropped",
			capability: mentionApp,
			scopes:     []string{AppMentionScope()},
			wantErr:    "requires messages.write",
		},
		{
			name:       "mentions.read retained without mention event",
			capability: mentionAppWithoutMentionEvent,
			scopes:     []string{AppMentionScope(), AppMessageWriteScope()},
			wantErr:    "requires the agentteam.message.mention event",
		},
		{
			name:       "notifications.write kept but messages.write dropped",
			capability: notificationApp,
			scopes:     []string{AppNotificationsWriteScope()},
			wantErr:    "requires messages.write",
		},
		{
			name:       "notifications.write retained without notifications capability",
			capability: notificationScopeWithoutCapability,
			scopes:     []string{AppNotificationsWriteScope(), AppMessageWriteScope()},
			wantErr:    "requires the notifications capability",
		},
		{
			name:       "notification_actions.read retained without action event",
			capability: actionScopeWithoutActionEvent,
			scopes:     []string{AppNotificationActionsReadScope(), AppMessageWriteScope()},
			wantErr:    "requires the agentteam.app.notification.action event",
		},
		{
			name:       "notification_actions.read retained without interactive_card content",
			capability: actionScopeWithoutCardContent,
			scopes:     []string{AppNotificationActionsReadScope(), AppNotificationsWriteScope(), AppMessageWriteScope()},
			wantErr:    "requires the interactive_card notification content",
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			err := ValidateAppScopeSubset(tt.capability, tt.scopes)
			if tt.wantErr == "" {
				if err != nil {
					t.Fatalf("ValidateAppScopeSubset returned unexpected error: %v", err)
				}
				return
			}
			if err == nil {
				t.Fatalf("ValidateAppScopeSubset returned nil, want error containing %q", tt.wantErr)
			}
			if !errors.Is(err, ErrBadRequest) {
				t.Fatalf("ValidateAppScopeSubset error = %v, want an error that unwraps to ErrBadRequest", err)
			}
			if !strings.Contains(err.Error(), tt.wantErr) {
				t.Fatalf("ValidateAppScopeSubset error = %q, want it to contain %q", err.Error(), tt.wantErr)
			}
		})
	}
}

// AppMentionScope returns the canonical mention-read scope string used by Bot
// capabilities. It mirrors the app-package constant to keep the registry tests
// self-contained.
func AppMentionScope() string { return "app.mentions.read" }

// AppNotificationsWriteScope returns the canonical notification-write scope.
func AppNotificationsWriteScope() string { return "app.notifications.write" }

// AppNotificationActionsReadScope returns the canonical action-read scope.
func AppNotificationActionsReadScope() string { return "app.notification_actions.read" }

// AppMessageWriteScope returns the canonical message-write scope.
func AppMessageWriteScope() string { return "messages.write" }
