@extensibility @plugin_architecture
Feature: Plugin Architecture Framework with module:ClassName resolution
  As a CleverAgents developer
  I want a plugin architecture that loads custom implementations via
  module:ClassName resolution and entry-point discovery
  So that tools, strategies, backends, and pipeline components can be
  extended without modifying core code

  # ---------------------------------------------------------------------------
  # PluginState enum
  # ---------------------------------------------------------------------------

  @plugin_state
  Scenario: PluginState enum has all required values
    Given the PluginState enum is available
    Then it should have values "discovered", "activated", "executing", "deactivated", "errored"

  @plugin_state
  Scenario: PluginState values are strings
    Given the PluginState enum is available
    Then each PluginState value should be a string

  # ---------------------------------------------------------------------------
  # ExtensionPoint model
  # ---------------------------------------------------------------------------

  @extension_point
  Scenario: ExtensionPoint creation with required fields
    Given I create an ExtensionPoint with name "test-ep" and a protocol type
    Then the ExtensionPoint name should be "test-ep"
    And the ExtensionPoint should have a protocol_type

  @extension_point
  Scenario: ExtensionPoint is frozen
    Given I create an ExtensionPoint with name "frozen-ep" and a protocol type
    Then attempting to mutate the ExtensionPoint name should raise an error

  @extension_point
  Scenario: ExtensionPoint rejects empty name
    When I attempt to create an ExtensionPoint with an empty name
    Then a plugin validation error should be raised

  # ---------------------------------------------------------------------------
  # PluginDescriptor model
  # ---------------------------------------------------------------------------

  @plugin_descriptor
  Scenario: PluginDescriptor creation with defaults
    Given I create a PluginDescriptor with name "my-plugin"
    Then the descriptor state should be "discovered"
    And the descriptor version should be "0.0.0"
    And the descriptor dependencies should be empty

  @plugin_descriptor
  Scenario: PluginDescriptor with full metadata
    Given I create a PluginDescriptor with full metadata
    Then the descriptor should have name "full-plugin"
    And the descriptor should have version "1.2.3"
    And the descriptor should have author "TestAuthor"
    And the descriptor should have module_path "cleveragents.test"
    And the descriptor should have class_name "TestClass"

  @plugin_descriptor
  Scenario: PluginDescriptor state is mutable
    Given I create a PluginDescriptor with name "mutable-plugin"
    When I set the descriptor state to "activated"
    Then the descriptor state should be "activated"

  @plugin_descriptor
  Scenario: PluginDescriptor rejects empty name
    When I attempt to create a PluginDescriptor with an empty name
    Then a plugin validation error should be raised

  # ---------------------------------------------------------------------------
  # Plugin exceptions
  # ---------------------------------------------------------------------------

  @plugin_exceptions
  Scenario: PluginError is base exception
    Then PluginLoadError should be a subclass of PluginError
    And PluginNotFoundError should be a subclass of PluginError
    And ProtocolMismatchError should be a subclass of PluginError

  @plugin_exceptions
  Scenario: PluginLoadError carries message
    When I raise a PluginLoadError with message "test error"
    Then the exception message should contain "test error"

  @plugin_exceptions
  Scenario: PluginNotFoundError carries message
    When I raise a PluginNotFoundError with message "not found"
    Then the exception message should contain "not found"

  @plugin_exceptions
  Scenario: ProtocolMismatchError carries message
    When I raise a ProtocolMismatchError with message "mismatch"
    Then the exception message should contain "mismatch"

  # ---------------------------------------------------------------------------
  # PluginLoader — load_class success and failure
  # ---------------------------------------------------------------------------

  @plugin_loader @success
  Scenario: PluginLoader loads a valid class
    Given a PluginLoader with default prefixes
    When I load class "InMemoryTextBackend" from module "cleveragents.domain.models.acms.stubs"
    Then the loaded class should not be None
    And the loaded class name should be "InMemoryTextBackend"

  @plugin_loader @success
  Scenario: PluginLoader loads another valid class
    Given a PluginLoader with default prefixes
    When I load class "InMemoryVectorBackend" from module "cleveragents.domain.models.acms.stubs"
    Then the loaded class should not be None

  @plugin_loader @failure
  Scenario: PluginLoader rejects module outside allowed prefix
    Given a PluginLoader with default prefixes
    When I attempt to load class "Path" from module "pathlib"
    Then a PluginLoadError should be raised
    And the plugin error message should contain "not in the allowed prefix list"

  @plugin_loader @failure
  Scenario: PluginLoader fails on nonexistent module
    Given a PluginLoader with default prefixes
    When I attempt to load class "Foo" from module "cleveragents.nonexistent_module_xyz"
    Then a PluginLoadError should be raised
    And the plugin error message should contain "Cannot import module"

  @plugin_loader @failure
  Scenario: PluginLoader fails on nonexistent class
    Given a PluginLoader with default prefixes
    When I attempt to load class "NonExistentClassXyz" from module "cleveragents.domain.models.acms.stubs"
    Then a PluginLoadError should be raised
    And the plugin error message should contain "not found in module"

  @plugin_loader @security
  Scenario: PluginLoader with custom prefixes allows matching module
    Given a PluginLoader with allowed prefixes "cleveragents.,mypackage."
    When I load class "InMemoryTextBackend" from module "cleveragents.domain.models.acms.stubs"
    Then the loaded class should not be None

  @plugin_loader @security
  Scenario: PluginLoader with empty prefix allowlist allows anything
    Given a PluginLoader with empty prefix allowlist
    When I load class "Path" from module "pathlib"
    Then the loaded class should not be None

  @plugin_loader
  Scenario: PluginLoader allowed_prefixes property
    Given a PluginLoader with default prefixes
    Then the loader allowed_prefixes should contain "cleveragents."

  @plugin_loader @failure
  Scenario: PluginLoader rejects non-class attribute
    Given a PluginLoader with default prefixes
    When I attempt to load class "logger" from module "cleveragents.infrastructure.plugins.loader"
    Then a PluginLoadError should be raised
    And the plugin error message should contain "is not a class"

  # ---------------------------------------------------------------------------
  # Protocol validation
  # ---------------------------------------------------------------------------

  @protocol_validation @success
  Scenario: validate_protocol passes for conforming class
    Given a class that implements TextBackend protocol
    When I validate it against the TextBackend protocol
    Then the validation should return True

  @protocol_validation @failure
  Scenario: validate_protocol fails for non-conforming class
    Given a class that does not implement any protocol
    When I attempt to validate it against the TextBackend protocol
    Then a ProtocolMismatchError should be raised

  # ---------------------------------------------------------------------------
  # Entry-point discovery
  # ---------------------------------------------------------------------------

  @entry_points
  Scenario: load_from_entry_points returns empty for unknown group
    Given a PluginLoader with default prefixes
    When I discover plugins from entry point group "cleveragents.nonexistent_group_xyz"
    Then the discovered plugin list should be empty

  @entry_points @mock
  Scenario: load_from_entry_points discovers mocked entry points
    Given a PluginLoader with default prefixes
    And a mocked entry point group "cleveragents.plugins" with entry "test-ep=cleveragents.domain.models.acms.stubs:InMemoryTextBackend"
    When I discover plugins from the mocked entry point group
    Then the discovered plugin list should have 1 entry
    And the first descriptor name should be "test-ep"

  @entry_points @security @tdd_issue @tdd_issue_7476
  Scenario: Entry point with disallowed prefix is skipped without loading module
    Given a PluginLoader with default prefixes
    And a mocked entry point group "cleveragents.plugins" with raw entry "malicious=maliciouspkg.attack:Exploit"
    When I discover plugins from the mocked entry point group
    Then the discovered plugin list should be empty
    And the mocked entry point load should not be called
    And a security warning should be emitted for disallowed entry point "malicious"

  # ---------------------------------------------------------------------------
  # PluginManager — lifecycle
  # ---------------------------------------------------------------------------

  @plugin_manager @registration
  Scenario: PluginManager registers and retrieves a plugin
    Given a fresh PluginManager instance
    And a PluginDescriptor for "test-plugin" with module "cleveragents.domain.models.acms.stubs" and class "InMemoryTextBackend"
    When I register the plugin descriptor
    Then get_plugin should return the descriptor for "test-plugin"
    And the plugin state should be "discovered"

  @plugin_manager @registration
  Scenario: PluginManager rejects duplicate registration
    Given a fresh PluginManager instance
    And a PluginDescriptor for "dup-plugin" with module "cleveragents.domain.models.acms.stubs" and class "InMemoryTextBackend"
    When I register the plugin descriptor
    And I attempt to register the same descriptor again
    Then a PluginError should be raised

  @plugin_manager @listing
  Scenario: PluginManager lists all registered plugins
    Given a fresh PluginManager instance
    And I register plugins named "alpha", "beta", "gamma"
    Then list_plugins should return 3 descriptors

  @plugin_manager @not_found
  Scenario: PluginManager raises on unknown plugin
    Given a fresh PluginManager instance
    When I attempt to get plugin "nonexistent"
    Then a PluginNotFoundError should be raised

  @plugin_manager @activate
  Scenario: PluginManager activates a registered plugin
    Given a fresh PluginManager instance
    And a PluginDescriptor for "act-plugin" with module "cleveragents.domain.models.acms.stubs" and class "InMemoryTextBackend"
    When I register the plugin descriptor
    And I activate the plugin "act-plugin"
    Then the plugin state should be "activated"
    And get_plugin_class for "act-plugin" should not be None
    And get_plugin_instance for "act-plugin" should not be None

  @plugin_manager @activate
  Scenario: PluginManager rejects activating already-activated plugin
    Given a fresh PluginManager instance
    And a PluginDescriptor for "double-act" with module "cleveragents.domain.models.acms.stubs" and class "InMemoryTextBackend"
    When I register the plugin descriptor
    And I activate the plugin "double-act"
    And I attempt to activate the plugin "double-act" again
    Then a PluginError should be raised

  @plugin_manager @activate @failure
  Scenario: PluginManager transitions to ERRORED on activation failure
    Given a fresh PluginManager instance
    And a PluginDescriptor for "bad-plugin" with module "cleveragents.nonexistent_module_xyz" and class "BadClass"
    When I register the plugin descriptor
    And I attempt to activate the plugin "bad-plugin"
    Then a PluginLoadError should be raised
    And the plugin "bad-plugin" state should be "errored"

  @plugin_manager @activate @failure
  Scenario: PluginManager fails activation with empty module_path
    Given a fresh PluginManager instance
    And a PluginDescriptor for "no-path" with empty module_path
    When I register the plugin descriptor
    And I attempt to activate the plugin "no-path"
    Then a PluginLoadError should be raised

  @plugin_manager @deactivate
  Scenario: PluginManager deactivates an activated plugin
    Given a fresh PluginManager instance
    And a PluginDescriptor for "deact-plugin" with module "cleveragents.domain.models.acms.stubs" and class "InMemoryTextBackend"
    When I register the plugin descriptor
    And I activate the plugin "deact-plugin"
    And I deactivate the plugin "deact-plugin"
    Then the plugin state should be "deactivated"
    And get_plugin_class for "deact-plugin" should be None
    And get_plugin_instance for "deact-plugin" should be None

  @plugin_manager @deactivate
  Scenario: PluginManager rejects deactivating a discovered plugin
    Given a fresh PluginManager instance
    And a PluginDescriptor for "disc-plugin" with module "cleveragents.domain.models.acms.stubs" and class "InMemoryTextBackend"
    When I register the plugin descriptor
    And I attempt to deactivate the plugin "disc-plugin"
    Then a PluginError should be raised

  @plugin_manager @deactivate
  Scenario: PluginManager can deactivate an errored plugin
    Given a fresh PluginManager instance
    And a PluginDescriptor for "err-plugin" with module "cleveragents.nonexistent_module_xyz" and class "BadClass"
    When I register the plugin descriptor
    And I attempt to activate the plugin "err-plugin" ignoring error
    And I deactivate the plugin "err-plugin"
    Then the plugin state should be "deactivated"

  # ---------------------------------------------------------------------------
  # Config-driven registration
  # ---------------------------------------------------------------------------

  @plugin_manager @config
  Scenario: PluginManager registers from config dict
    Given a fresh PluginManager instance
    When I register a plugin from config with custom_module "cleveragents.domain.models.acms.stubs" and custom_class "InMemoryTextBackend"
    Then the registered plugin descriptor should not be None
    And the plugin should be in the manager registry

  @plugin_manager @config
  Scenario: PluginManager ignores config without plugin keys
    Given a fresh PluginManager instance
    When I register a plugin from config with no custom_module or custom_class
    Then the registered plugin descriptor should be None

  @plugin_manager @config
  Scenario: PluginManager returns existing for duplicate config
    Given a fresh PluginManager instance
    When I register a plugin from config with custom_module "cleveragents.domain.models.acms.stubs" and custom_class "InMemoryTextBackend"
    And I register the same plugin from config again
    Then list_plugins should return 1 descriptor

  @plugin_manager @config
  Scenario: PluginManager batch registers from config list
    Given a fresh PluginManager instance
    When I register plugins from a config list of 3 entries
    Then list_plugins should return 3 descriptors

  # ---------------------------------------------------------------------------
  # Extension point management
  # ---------------------------------------------------------------------------

  @plugin_manager @extension_points
  Scenario: PluginManager registers and lists extension points
    Given a fresh PluginManager instance
    When I register an extension point named "text-backend"
    Then list_extension_points should return 1 entry

  # ---------------------------------------------------------------------------
  # Clear / cleanup
  # ---------------------------------------------------------------------------

  @plugin_manager @clear
  Scenario: PluginManager clear removes all plugins
    Given a fresh PluginManager instance
    And I register plugins named "a", "b", "c"
    When I clear the plugin manager
    Then list_plugins should return 0 descriptors

  # ---------------------------------------------------------------------------
  # Thread safety
  # ---------------------------------------------------------------------------

  @thread_safety
  Scenario: PluginManager supports concurrent registration
    Given a fresh PluginManager instance
    When 10 threads register plugins concurrently
    Then list_plugins should return 10 descriptors

  @thread_safety
  Scenario: PluginManager supports concurrent activate and deactivate
    Given a fresh PluginManager with 5 registered plugins
    When 5 threads activate plugins concurrently
    Then all 5 plugins should be in "activated" state

  # ---------------------------------------------------------------------------
  # DI container integration
  # ---------------------------------------------------------------------------

  @di_container
  Scenario: DI container provides PluginManager singleton
    When I resolve PluginManager from the DI container
    Then the resolved PluginManager should not be None
    And resolving PluginManager again should return the same instance

  # ---------------------------------------------------------------------------
  # Discovery integration
  # ---------------------------------------------------------------------------

  @discover
  Scenario: PluginManager discover with no entry points
    Given a fresh PluginManager instance
    When I call discover with group "cleveragents.nonexistent_ep_group_xyz"
    Then the discovered list should be empty

  @discover
  Scenario: PluginManager discover registers newly found plugins
    Given a fresh PluginManager instance
    And a mocked entry point group for discover
    When I call discover through the manager
    Then newly discovered plugins should be in the registry
