@extensibility @component_resolver
Feature: Pluggable Component Resolution with Scope Chain
  As a CleverAgents developer
  I want a ComponentResolver that resolves pluggable components through a
  3-level scope chain (plan > project > global)
  So that extension points can be consistently overridden at different scopes

  # ---------------------------------------------------------------------------
  # Global default resolution
  # ---------------------------------------------------------------------------

  @scope_chain @global
  Scenario: Global default resolution when no overrides exist
    Given a fresh ComponentResolver instance
    And a global default implementation for a test protocol
    When I resolve the test protocol with no plan or project context
    Then the resolved component should be the global default
    And the resolution scope should be "global"

  @scope_chain @global
  Scenario: Global default registered at startup is available
    Given a fresh ComponentResolver instance
    And a global default implementation for a test protocol
    Then has_global should return True for the test protocol

  @scope_chain @global
  Scenario: Resolving unregistered component raises error
    Given a fresh ComponentResolver instance
    When I attempt to resolve an unregistered protocol
    Then a ComponentNotFoundError should be raised

  # ---------------------------------------------------------------------------
  # Project-level override
  # ---------------------------------------------------------------------------

  @scope_chain @project
  Scenario: Project override takes precedence over global
    Given a fresh ComponentResolver instance
    And a global default implementation for a test protocol
    And a project-level override for project "my-project" for the test protocol
    When I resolve the test protocol for project "my-project"
    Then the resolved component should be the project override
    And the resolution scope should be "project"

  @scope_chain @project
  Scenario: Project without override falls through to global
    Given a fresh ComponentResolver instance
    And a global default implementation for a test protocol
    When I resolve the test protocol for project "other-project"
    Then the resolved component should be the global default
    And the resolution scope should be "global"

  @scope_chain @project
  Scenario: Project override registered via register_project
    Given a fresh ComponentResolver instance
    And a project-level override for project "my-project" for the test protocol
    Then has_project should return True for "my-project" and the test protocol

  @scope_chain @project
  Scenario: Empty project_id raises error
    Given a fresh ComponentResolver instance
    When I attempt to register a project override with empty project_id
    Then a ComponentRegistrationError should be raised

  # ---------------------------------------------------------------------------
  # Plan-level override (highest priority)
  # ---------------------------------------------------------------------------

  @scope_chain @plan
  Scenario: Plan override takes highest precedence
    Given a fresh ComponentResolver instance
    And a global default implementation for a test protocol
    And a project-level override for project "my-project" for the test protocol
    And a plan-level override for plan "plan-001" for the test protocol
    When I resolve the test protocol for plan "plan-001" and project "my-project"
    Then the resolved component should be the plan override
    And the resolution scope should be "plan"

  @scope_chain @plan
  Scenario: Plan without override falls through to project then global
    Given a fresh ComponentResolver instance
    And a global default implementation for a test protocol
    And a project-level override for project "my-project" for the test protocol
    When I resolve the test protocol for plan "plan-002" and project "my-project"
    Then the resolved component should be the project override
    And the resolution scope should be "project"

  @scope_chain @plan
  Scenario: Empty plan_id raises error
    Given a fresh ComponentResolver instance
    When I attempt to register a plan override with empty plan_id
    Then a ComponentRegistrationError should be raised

  # ---------------------------------------------------------------------------
  # Fallthrough on missing scope
  # ---------------------------------------------------------------------------

  @scope_chain @fallthrough
  Scenario: Missing plan and project falls through to global
    Given a fresh ComponentResolver instance
    And a global default implementation for a test protocol
    When I resolve the test protocol for plan "unknown-plan" and project "unknown-project"
    Then the resolved component should be the global default
    And the resolution scope should be "global"

  @scope_chain @fallthrough
  Scenario: Only plan registered without global still resolves from plan
    Given a fresh ComponentResolver instance
    And a plan-level override for plan "plan-only" for the test protocol
    When I resolve the test protocol for plan "plan-only"
    Then the resolved component should be the plan override
    And the resolution scope should be "plan"

  # ---------------------------------------------------------------------------
  # Caching
  # ---------------------------------------------------------------------------

  @scope_chain @caching
  Scenario: Resolution is cached per scope
    Given a fresh ComponentResolver instance
    And a global default implementation for a test protocol
    When I resolve the test protocol twice with no context
    Then the cache size should be 1
    And both resolutions should return the same result

  @scope_chain @caching
  Scenario: Cache is invalidated on new registration
    Given a fresh ComponentResolver instance
    And a global default implementation for a test protocol
    When I resolve the test protocol with no plan or project context
    And I register a project-level override for project "p1" for the test protocol
    Then the cache size should be 0

  # ---------------------------------------------------------------------------
  # Config.toml extension loading
  # ---------------------------------------------------------------------------

  @scope_chain @config
  Scenario: Load project extensions from config mapping
    Given a fresh ComponentResolver instance
    And an extensions config mapping with a known module path
    When I load project extensions for project "ext-project"
    Then the extension should be registered at the project scope

  # ---------------------------------------------------------------------------
  # Plan metadata extension loading
  # ---------------------------------------------------------------------------

  @scope_chain @config
  Scenario: Load plan extensions from metadata mapping
    Given a fresh ComponentResolver instance
    And a plan metadata mapping with a known module path
    When I load plan extensions for plan "ext-plan"
    Then the extension should be registered at the plan scope

  # ---------------------------------------------------------------------------
  # Removal
  # ---------------------------------------------------------------------------

  @scope_chain @removal
  Scenario: Remove global implementation
    Given a fresh ComponentResolver instance
    And a global default implementation for a test protocol
    When I remove the global implementation for the test protocol
    Then has_global should return False for the test protocol

  @scope_chain @removal
  Scenario: Remove project implementation
    Given a fresh ComponentResolver instance
    And a project-level override for project "rm-project" for the test protocol
    When I remove the project implementation for "rm-project" and the test protocol
    Then has_project should return False for "rm-project" and the test protocol

  @scope_chain @removal
  Scenario: Remove plan implementation
    Given a fresh ComponentResolver instance
    And a plan-level override for plan "rm-plan" for the test protocol
    When I remove the plan implementation for "rm-plan" and the test protocol
    Then has_plan should return False for "rm-plan" and the test protocol

  # ---------------------------------------------------------------------------
  # Introspection
  # ---------------------------------------------------------------------------

  @scope_chain @introspection
  Scenario: List global types
    Given a fresh ComponentResolver instance
    And a global default implementation for a test protocol
    Then list_global_types should include the test protocol name

  @scope_chain @introspection
  Scenario: List project types
    Given a fresh ComponentResolver instance
    And a project-level override for project "intro-project" for the test protocol
    Then list_project_types for "intro-project" should include the test protocol name

  @scope_chain @introspection
  Scenario: List plan types
    Given a fresh ComponentResolver instance
    And a plan-level override for plan "intro-plan" for the test protocol
    Then list_plan_types for "intro-plan" should include the test protocol name

  # ---------------------------------------------------------------------------
  # Clear
  # ---------------------------------------------------------------------------

  @scope_chain @clear
  Scenario: Clear removes all registrations
    Given a fresh ComponentResolver instance
    And a global default implementation for a test protocol
    And a project-level override for project "c-project" for the test protocol
    And a plan-level override for plan "c-plan" for the test protocol
    When I clear the resolver
    Then has_global should return False for the test protocol
    And has_project should return False for "c-project" and the test protocol
    And has_plan should return False for "c-plan" and the test protocol

  # ---------------------------------------------------------------------------
  # Extension point catalog
  # ---------------------------------------------------------------------------

  @scope_chain @catalog
  Scenario: Register and list extension points
    Given a fresh ComponentResolver instance
    When I register an extension point for the test protocol
    Then list_extension_points should include the test protocol

  # ---------------------------------------------------------------------------
  # None implementation rejected
  # ---------------------------------------------------------------------------

  @scope_chain @validation
  Scenario: Registering None as global raises error
    Given a fresh ComponentResolver instance
    When I attempt to register None as a global implementation
    Then a ComponentRegistrationError should be raised

  @scope_chain @validation
  Scenario: Registering None as project implementation raises error
    Given a fresh ComponentResolver instance
    When I attempt to register None as a project implementation
    Then a ComponentRegistrationError should be raised

  @scope_chain @validation
  Scenario: Registering None as plan implementation raises error
    Given a fresh ComponentResolver instance
    When I attempt to register None as a plan implementation
    Then a ComponentRegistrationError should be raised

  # ---------------------------------------------------------------------------
  # Security: Module prefix restriction
  # ---------------------------------------------------------------------------

  @scope_chain @security
  Scenario: Import from disallowed module prefix raises error
    Given a fresh ComponentResolver instance
    When I attempt to import a component from "evil_module:Hacker"
    Then a ComponentRegistrationError should be raised

  @scope_chain @security
  Scenario: Import with invalid format raises error
    Given a fresh ComponentResolver instance
    When I attempt to import a component from "no_colon_here"
    Then a ComponentRegistrationError should be raised

  @scope_chain @security
  Scenario: Import builtin component path resolves through application services
    Given a fresh ComponentResolver instance
    When I import a component from "builtin:DepthReductionCompressor"
    Then the imported component class should be "DepthReductionCompressor"

  @scope_chain @security
  Scenario: Import builtin service outside the allowlist is rejected
    Given a fresh ComponentResolver instance
    When I attempt to import a component from "builtin:ConfigService"
    Then a ComponentRegistrationError should be raised

  # ---------------------------------------------------------------------------
  # Edge cases: unknown extension names and failed imports
  # ---------------------------------------------------------------------------

  @scope_chain @config @edge
  Scenario: Load project extensions with unknown type name skips gracefully
    Given a fresh ComponentResolver instance
    And an extensions config with an unknown extension type name
    When I load project extensions with unknown type for project "edge-proj"
    Then the loaded extensions list should be empty

  @scope_chain @config @edge
  Scenario: Load project extensions with bad module path logs warning
    Given a fresh ComponentResolver instance
    And an extensions config with a bad module path
    When I load project extensions with bad path for project "bad-proj"
    Then the loaded extensions list should be empty

  @scope_chain @config @edge
  Scenario: Load plan extensions with unknown type name skips gracefully
    Given a fresh ComponentResolver instance
    And a plan metadata with an unknown extension type name
    When I load plan extensions with unknown type for plan "edge-plan"
    Then the loaded extensions list should be empty

  @scope_chain @config @edge
  Scenario: Load plan extensions with bad module path logs warning
    Given a fresh ComponentResolver instance
    And a plan metadata with a bad module path
    When I load plan extensions with bad path for plan "bad-plan"
    Then the loaded extensions list should be empty

  # ---------------------------------------------------------------------------
  # Edge cases: remove from non-existent scope
  # ---------------------------------------------------------------------------

  @scope_chain @removal @edge
  Scenario: Remove project from non-existent project returns False
    Given a fresh ComponentResolver instance
    When I remove the project implementation for "nonexistent" and the test protocol
    Then the component resolver removal result should be False

  @scope_chain @removal @edge
  Scenario: Remove plan from non-existent plan returns False
    Given a fresh ComponentResolver instance
    When I remove the plan implementation for "nonexistent" and the test protocol
    Then the component resolver removal result should be False

  # ---------------------------------------------------------------------------
  # Edge cases: list types for non-existent scopes
  # ---------------------------------------------------------------------------

  @scope_chain @introspection @edge
  Scenario: List project types for non-existent project returns empty
    Given a fresh ComponentResolver instance
    Then list_project_types for "nonexistent" should be empty

  @scope_chain @introspection @edge
  Scenario: List plan types for non-existent plan returns empty
    Given a fresh ComponentResolver instance
    Then list_plan_types for "nonexistent" should be empty

  # ---------------------------------------------------------------------------
  # Edge case: _ScopeRegistry.remove returns False for missing type
  # ---------------------------------------------------------------------------

  @scope_chain @removal @edge
  Scenario: Remove non-existent type from global returns False
    Given a fresh ComponentResolver instance
    When I remove the global implementation for the test protocol
    Then the component resolver removal result should be False

  # ---------------------------------------------------------------------------
  # Edge case: invalidate_cache explicitly
  # ---------------------------------------------------------------------------

  @scope_chain @caching @edge
  Scenario: Explicit invalidate_cache clears all cached entries
    Given a fresh ComponentResolver instance
    And a global default implementation for a test protocol
    When I resolve the test protocol with no plan or project context
    And I explicitly invalidate the cache
    Then the cache size should be 0
