Feature: Sandbox Manager Lifecycle
  As an execution engine
  I want a manager that tracks sandbox instances across plan executions
  So that resources are isolated during execution and cleaned up reliably

  Background:
    Given a sandbox factory instance
    And a sandbox manager with the factory

  # Initialisation

  Scenario: A new manager starts with no tracked sandboxes
    Then the manager should have no active sandboxes
    And the manager cleanup_on_exit flag should be true

  Scenario: A manager can be created without automatic exit cleanup
    Given a sandbox manager with cleanup_on_exit disabled
    Then the manager cleanup_on_exit flag should be false

  Scenario: Creating a manager without a factory is rejected
    When I create a sandbox manager with None factory
    Then a sandbox ValueError should be raised with message "factory cannot be None"

  # Lazy sandbox creation

  Scenario: Requesting a sandbox for a plan creates and tracks a new sandbox
    When I get or create a sandbox for plan "plan-001" resource "res-001" path "/tmp/repo" strategy "none"
    Then a sandbox should be returned
    And the sandbox should have status "created"
    And the sandbox should be tracked for plan "plan-001"

  Scenario: Requesting the same plan-resource pair returns the existing sandbox
    Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo" strategy "none"
    When I get or create a sandbox for plan "plan-001" resource "res-001" path "/tmp/repo" strategy "none"
    Then the same sandbox instance should be returned

  Scenario: An active sandbox is reused when requested again
    Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo" strategy "none"
    And the sandbox for plan "plan-001" resource "res-001" is activated
    When I get or create a sandbox for plan "plan-001" resource "res-001" path "/tmp/repo" strategy "none"
    Then the same sandbox instance should be returned

  Scenario: A committed sandbox is replaced by a fresh instance on next request
    Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo" strategy "none"
    And the sandbox for plan "plan-001" resource "res-001" is committed
    When I get or create a sandbox for plan "plan-001" resource "res-001" path "/tmp/repo" strategy "none"
    Then a different sandbox instance should be returned

  Scenario: A cleaned-up sandbox is replaced by a fresh instance on next request
    Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo" strategy "none"
    And the sandbox for plan "plan-001" resource "res-001" is cleaned up
    When I get or create a sandbox for plan "plan-001" resource "res-001" path "/tmp/repo" strategy "none"
    Then a different sandbox instance should be returned

  Scenario: A rolled-back sandbox is still usable and returned on next request
    Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo" strategy "none"
    And the sandbox for plan "plan-001" resource "res-001" is rolled back
    When I get or create a sandbox for plan "plan-001" resource "res-001" path "/tmp/repo" strategy "none"
    Then the same sandbox instance should be returned

  Scenario: An errored sandbox is replaced by a fresh instance on next request
    Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo" strategy "none"
    And the sandbox for plan "plan-001" resource "res-001" is in errored state
    When I get or create a sandbox for plan "plan-001" resource "res-001" path "/tmp/repo" strategy "none"
    Then a different sandbox instance should be returned

  # Argument validation on sandbox creation

  Scenario: A sandbox request without a plan identifier is rejected
    When I get or create a sandbox with empty plan_id
    Then a sandbox ValueError should be raised with message "plan_id cannot be empty"

  Scenario: A sandbox request without a resource identifier is rejected
    When I get or create a sandbox with empty resource_id
    Then a sandbox ValueError should be raised with message "resource_id cannot be empty"

  Scenario: A sandbox request without an original path is rejected
    When I get or create a sandbox with empty original_path
    Then a sandbox ValueError should be raised with message "original_path cannot be empty"

  # Multi-resource and multi-plan tracking

  Scenario: Multiple resources under the same plan are tracked independently
    When I get or create a sandbox for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none"
    And I get or create a sandbox for plan "plan-001" resource "res-002" path "/tmp/repo2" strategy "none"
    Then 2 sandboxes should be tracked for plan "plan-001"

  Scenario: Sandboxes from different plans do not interfere with each other
    When I get or create a sandbox for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none"
    And I get or create a sandbox for plan "plan-002" resource "res-001" path "/tmp/repo2" strategy "none"
    Then 1 sandboxes should be tracked for plan "plan-001"
    And 1 sandboxes should be tracked for plan "plan-002"

  # Looking up an existing sandbox

  Scenario: An existing sandbox can be retrieved by plan and resource
    Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo" strategy "none"
    When I get sandbox for plan "plan-001" resource "res-001"
    Then a sandbox should be returned

  Scenario: Looking up a sandbox for an unknown plan returns nothing
    When I get sandbox for plan "plan-999" resource "res-999"
    Then the sandbox result should be None

  Scenario: Looking up a sandbox for a known plan but unknown resource returns nothing
    Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo" strategy "none"
    When I get sandbox for plan "plan-001" resource "res-999"
    Then the sandbox result should be None

  # Listing sandboxes for a plan

  Scenario: All sandboxes for a plan can be listed
    Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none"
    And a sandbox exists for plan "plan-001" resource "res-002" path "/tmp/repo2" strategy "none"
    When I list sandboxes for plan "plan-001"
    Then the sandbox list should contain 2 sandboxes

  Scenario: Listing sandboxes for a plan with none returns an empty list
    When I list sandboxes for plan "plan-999"
    Then the sandbox list should contain 0 sandboxes

  # Batch commit (atomic)

  Scenario: All active sandboxes for a plan are committed together
    Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none"
    And the sandbox for plan "plan-001" resource "res-001" is activated
    And a sandbox exists for plan "plan-001" resource "res-002" path "/tmp/repo2" strategy "none"
    And the sandbox for plan "plan-001" resource "res-002" is activated
    When I commit all sandboxes for plan "plan-001"
    Then 2 commit results should be returned
    And all commit results should be successful

  Scenario: Batch commit skips sandboxes that are already committed
    Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none"
    And the sandbox for plan "plan-001" resource "res-001" is committed
    When I commit all sandboxes for plan "plan-001"
    Then 0 commit results should be returned

  Scenario: Batch commit on a plan with no sandboxes returns an empty result set
    When I commit all sandboxes for plan "plan-999"
    Then 0 commit results should be returned

  Scenario: Batch commit without a plan identifier is rejected
    When I commit all sandboxes with empty plan_id
    Then a sandbox ValueError should be raised with message "plan_id cannot be empty"

  Scenario: A single sandbox failure in atomic commit returns an error result
    Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none"
    And the sandbox for plan "plan-001" resource "res-001" is activated
    And the sandbox for plan "plan-001" resource "res-001" will fail on commit
    When I commit all sandboxes for plan "plan-001"
    Then 1 commit results should be returned
    And the first commit result should have success false

  Scenario: Newly created sandboxes can be committed before activation
    Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none"
    When I commit all sandboxes for plan "plan-001"
    Then 1 commit results should be returned
    And all commit results should be successful

  # Atomic commit — rollback on failure

  Scenario: Atomic commit rolls back already-committed sandboxes when one fails
    Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none"
    And the sandbox for plan "plan-001" resource "res-001" is replaced with a committable mock
    And a sandbox exists for plan "plan-001" resource "res-002" path "/tmp/repo2" strategy "none"
    And the sandbox for plan "plan-001" resource "res-002" is activated
    And the sandbox for plan "plan-001" resource "res-002" will fail on commit
    When I commit all sandboxes for plan "plan-001"
    Then 1 commit results should be returned
    And the first commit result should have success false
    And the first commit result error should mention the failed sandbox
    And the first commit result metadata should list rolled back sandboxes

  Scenario: Atomic commit error reports the sandbox that failed
    Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none"
    And the sandbox for plan "plan-001" resource "res-001" is activated
    And the sandbox for plan "plan-001" resource "res-001" will fail on commit
    When I commit all sandboxes for plan "plan-001"
    Then 1 commit results should be returned
    And the first commit result should have success false
    And the first commit result error should contain "Atomic commit failed"

  Scenario: Atomic commit tolerates rollback errors during recovery
    Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none"
    And the sandbox for plan "plan-001" resource "res-001" will succeed commit then fail rollback
    And a sandbox exists for plan "plan-001" resource "res-002" path "/tmp/repo2" strategy "none"
    And the sandbox for plan "plan-001" resource "res-002" is activated
    And the sandbox for plan "plan-001" resource "res-002" will fail on commit
    When I commit all sandboxes for plan "plan-001"
    Then 1 commit results should be returned
    And the first commit result should have success false
    And the first commit result metadata should list failed rollback sandboxes

  Scenario: Atomic commit succeeds rollback for a no-change sandbox
    Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none"
    And the sandbox for plan "plan-001" resource "res-001" is replaced with a no-change committable mock
    And a sandbox exists for plan "plan-001" resource "res-002" path "/tmp/repo2" strategy "none"
    And the sandbox for plan "plan-001" resource "res-002" is activated
    And the sandbox for plan "plan-001" resource "res-002" will fail on commit
    When I commit all sandboxes for plan "plan-001"
    Then 1 commit results should be returned
    And the first commit result should have success false
    And the no-change sandbox rollback should have succeeded for plan "plan-001" resource "res-001"

  Scenario: Atomic commit wraps non-SandboxError in AtomicCommitError after rollback
    Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none"
    And the sandbox for plan "plan-001" resource "res-001" is replaced with a committable mock
    And a sandbox exists for plan "plan-001" resource "res-002" path "/tmp/repo2" strategy "none"
    And the sandbox for plan "plan-001" resource "res-002" is activated
    And the sandbox for plan "plan-001" resource "res-002" will raise RuntimeError on commit
    When I commit all sandboxes for plan "plan-001" expecting a non-sandbox error
    Then an AtomicCommitError should have been raised with RuntimeError as cause
    And the AtomicCommitError should carry rollback metadata
    And the committable mock for plan "plan-001" resource "res-001" should have been rolled back

  Scenario: Atomic commit rolls back in reverse order
    Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none"
    And the sandbox for plan "plan-001" resource "res-001" is replaced with a committable mock
    And a sandbox exists for plan "plan-001" resource "res-002" path "/tmp/repo2" strategy "none"
    And the sandbox for plan "plan-001" resource "res-002" is replaced with a committable mock
    And a sandbox exists for plan "plan-001" resource "res-003" path "/tmp/repo3" strategy "none"
    And the sandbox for plan "plan-001" resource "res-003" is activated
    And the sandbox for plan "plan-001" resource "res-003" will fail on commit
    When I commit all sandboxes for plan "plan-001"
    Then 1 commit results should be returned
    And the first commit result should have success false
    And the rollback order should be reverse of commit order for plan "plan-001"

  Scenario: Atomic commit orders rollbackable sandboxes before non-rollbackable
    Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none"
    And the sandbox for plan "plan-001" resource "res-001" is replaced with a committable mock
    And a sandbox exists for plan "plan-001" resource "res-002" path "/tmp/repo2" strategy "none"
    And the sandbox for plan "plan-001" resource "res-002" is replaced with a non-rollbackable mock
    And a sandbox exists for plan "plan-001" resource "res-003" path "/tmp/repo3" strategy "none"
    And the sandbox for plan "plan-001" resource "res-003" is activated
    And the sandbox for plan "plan-001" resource "res-003" will fail on commit
    When I commit all sandboxes for plan "plan-001"
    Then 1 commit results should be returned
    And the first commit result should have success false
    And the non-rollbackable mock for plan "plan-001" resource "res-002" should not have been committed

  # Batch rollback

  Scenario: All active sandboxes for a plan are rolled back together
    Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none"
    And the sandbox for plan "plan-001" resource "res-001" is activated
    When I rollback all sandboxes for plan "plan-001"
    Then no sandbox error should be raised

  Scenario: Batch rollback skips sandboxes that are not active
    Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none"
    When I rollback all sandboxes for plan "plan-001"
    Then no sandbox error should be raised

  Scenario: Batch rollback on a plan with no sandboxes succeeds silently
    When I rollback all sandboxes for plan "plan-999"
    Then no sandbox error should be raised

  Scenario: Batch rollback without a plan identifier is rejected
    When I rollback all sandboxes with empty plan_id
    Then a sandbox ValueError should be raised with message "plan_id cannot be empty"

  Scenario: A single sandbox failure during batch rollback does not abort the others
    Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none"
    And the sandbox for plan "plan-001" resource "res-001" is activated
    And the sandbox for plan "plan-001" resource "res-001" will fail on rollback
    When I rollback all sandboxes for plan "plan-001"
    Then no sandbox error should be raised

  # Batch cleanup

  Scenario: All sandboxes for a plan are cleaned up and the plan is untracked
    Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none"
    And a sandbox exists for plan "plan-001" resource "res-002" path "/tmp/repo2" strategy "none"
    When I cleanup all sandboxes for plan "plan-001"
    Then no sandbox error should be raised
    And plan "plan-001" should no longer be tracked

  Scenario: Batch cleanup without a plan identifier is rejected
    When I cleanup all sandboxes with empty plan_id
    Then a sandbox ValueError should be raised with message "plan_id cannot be empty"

  Scenario: Batch cleanup on a plan with no sandboxes succeeds silently
    When I cleanup all sandboxes for plan "plan-999"
    Then no sandbox error should be raised

  Scenario: A single sandbox failure during batch cleanup does not prevent untracking
    Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none"
    And the sandbox for plan "plan-001" resource "res-001" will fail on cleanup
    When I cleanup all sandboxes for plan "plan-001"
    Then no sandbox error should be raised
    And plan "plan-001" should no longer be tracked

  # Abandoned sandbox cleanup

  Scenario: Errored sandboxes are cleaned up as abandoned
    Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none"
    And the sandbox for plan "plan-001" resource "res-001" is in errored state
    When I cleanup abandoned sandboxes
    Then 1 abandoned sandboxes should be cleaned up

  Scenario: Committed sandboxes are cleaned up as abandoned
    Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none"
    And the sandbox for plan "plan-001" resource "res-001" is committed
    When I cleanup abandoned sandboxes
    Then 1 abandoned sandboxes should be cleaned up

  Scenario: Rolled-back sandboxes are cleaned up as abandoned
    Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none"
    And the sandbox for plan "plan-001" resource "res-001" is rolled back
    When I cleanup abandoned sandboxes
    Then 1 abandoned sandboxes should be cleaned up

  Scenario: Active sandboxes are not considered abandoned
    Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none"
    And the sandbox for plan "plan-001" resource "res-001" is activated
    When I cleanup abandoned sandboxes
    Then 0 abandoned sandboxes should be cleaned up

  Scenario: A plan entry is removed once all of its sandboxes have been cleaned up
    Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none"
    And the sandbox for plan "plan-001" resource "res-001" is committed
    When I cleanup abandoned sandboxes
    Then 1 abandoned sandboxes should be cleaned up
    And plan "plan-001" should no longer be tracked

  Scenario: Cleanup errors on individual abandoned sandboxes are contained
    Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none"
    And the sandbox for plan "plan-001" resource "res-001" is in errored state
    And the sandbox for plan "plan-001" resource "res-001" will fail on cleanup
    When I cleanup abandoned sandboxes
    Then 0 abandoned sandboxes should be cleaned up

  Scenario: Abandoned sandbox cleanup operates across all tracked plans
    Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none"
    And the sandbox for plan "plan-001" resource "res-001" is committed
    And a sandbox exists for plan "plan-002" resource "res-002" path "/tmp/repo2" strategy "none"
    And the sandbox for plan "plan-002" resource "res-002" is in errored state
    When I cleanup abandoned sandboxes
    Then 2 abandoned sandboxes should be cleaned up

  # Graceful process exit

  Scenario: The exit handler cleans up all remaining sandboxes
    Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none"
    And a sandbox exists for plan "plan-002" resource "res-002" path "/tmp/repo2" strategy "none"
    When the atexit cleanup handler runs
    Then plan "plan-001" should no longer be tracked
    And plan "plan-002" should no longer be tracked

  Scenario: The exit handler tolerates individual sandbox cleanup failures
    Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none"
    And the sandbox for plan "plan-001" resource "res-001" will fail on cleanup
    When the atexit cleanup handler runs
    Then no sandbox error should be raised

  Scenario: The exit handler swallows exceptions from the cleanup subsystem
    Given a sandbox exists for plan "plan-001" resource "res-001" path "/tmp/repo1" strategy "none"
    And cleanup_all is patched to raise a RuntimeError
    When the atexit cleanup handler runs
    Then no sandbox error should be raised
