Feature: Overlay filesystem sandbox lifecycle
  As a developer
  I want a sandbox that isolates plan changes using overlay filesystem semantics
  So that changes are captured in an upper layer and merged on commit

  # --- Creation ---

  Scenario: Create an overlay sandbox
    Given an ovl test directory is initialised
    When an ovl sandbox is created for plan "plan-001"
    Then the ovl sandbox should be in the "created" state
    And the ovl sandbox context should reference plan "plan-001"
    And the ovl sandbox context should have strategy metadata "overlay"
    And the ovl sandbox merged path should exist

  Scenario: Creating an overlay sandbox with empty plan_id raises ValueError
    Given an ovl test directory is initialised
    When an ovl sandbox is created with empty plan_id
    Then an ovl ValueError should be raised with message "plan_id cannot be empty"

  Scenario: Creating an overlay sandbox with empty resource_id raises ValueError
    Given an ovl test directory is initialised
    When an ovl sandbox is prepared with empty resource_id
    Then an ovl ValueError should be raised with message "resource_id cannot be empty"

  Scenario: Creating an overlay sandbox with empty original_path raises ValueError
    Given an ovl test directory is initialised
    When an ovl sandbox is prepared with empty original_path
    Then an ovl ValueError should be raised with message "original_path cannot be empty"

  Scenario: Creating an overlay sandbox on a non-existent directory raises SandboxCreationError
    Given an ovl test directory is initialised
    Given an ovl non-existent directory
    When an ovl sandbox is created on the non-existent directory for plan "plan-001"
    Then an ovl SandboxCreationError should be raised

  # --- Directory structure ---

  Scenario: Overlay sandbox creates correct directory structure
    Given an ovl test directory is initialised
    When an ovl sandbox is created for plan "plan-001"
    Then the ovl sandbox should have upper, work, and merged directories

  # --- Path resolution ---

  Scenario: Resolve a path in the overlay merged directory
    Given an ovl test directory is initialised
    When an ovl sandbox is created for plan "plan-001"
    And the ovl path "data/file.txt" is resolved
    Then the ovl resolved path should be inside the merged directory
    And the ovl sandbox should be in the "active" state

  Scenario: Path traversal is rejected
    Given an ovl test directory is initialised
    When an ovl sandbox is created for plan "plan-001"
    And the ovl path "../etc/passwd" is resolved
    Then an ovl ValueError should be raised with message "Path traversal not allowed"

  Scenario: Resolving a path on a cleaned-up sandbox raises SandboxStateError
    Given an ovl test directory is initialised
    When an ovl sandbox is created for plan "plan-001"
    And the ovl sandbox is cleaned up
    And the ovl path "file.txt" is resolved on a cleaned-up sandbox
    Then an ovl SandboxStateError should be raised

  # --- Commit ---

  Scenario: Commit with no changes produces empty result
    Given an ovl test directory is initialised
    When an ovl sandbox is created for plan "plan-001"
    And the ovl sandbox is committed
    Then the ovl commit result should indicate success
    And the ovl commit result should have 0 changed files
    And the ovl commit result should have 0 added files
    And the ovl commit result should have 0 deleted files

  Scenario: Commit with a new file syncs it to original
    Given an ovl test directory is initialised
    When an ovl sandbox is created for plan "plan-001"
    And an ovl file "new_file.txt" is created in the sandbox with content "hello world"
    And the ovl sandbox is committed
    Then the ovl commit result should indicate success
    And the ovl commit result should have 1 added files
    And the ovl file "new_file.txt" should exist in the original directory with content "hello world"

  Scenario: Commit with a modified file syncs the change
    Given an ovl test directory is initialised
    When an ovl sandbox is created for plan "plan-001"
    And the ovl existing file "existing.txt" is modified in the sandbox with content "updated"
    And the ovl sandbox is committed
    Then the ovl commit result should indicate success
    And the ovl commit result should have 1 changed files
    And the ovl file "existing.txt" in the original should have content "updated"

  Scenario: Commit with a deleted file removes it from original
    Given an ovl test directory is initialised
    When an ovl sandbox is created for plan "plan-001"
    And the ovl existing file "existing.txt" is deleted from the sandbox
    And the ovl sandbox is committed
    Then the ovl commit result should indicate success
    And the ovl commit result should have 1 deleted files
    And the ovl file "existing.txt" should not exist in the original directory

  Scenario: Commit on a cleaned-up sandbox raises SandboxStateError
    Given an ovl test directory is initialised
    When an ovl sandbox is created for plan "plan-001"
    And the ovl sandbox is cleaned up
    And the ovl sandbox commit is attempted on cleaned-up sandbox
    Then an ovl SandboxStateError should be raised

  # --- Rollback ---

  Scenario: Rollback restores the sandbox to original state
    Given an ovl test directory is initialised
    When an ovl sandbox is created for plan "plan-001"
    And an ovl file "temp.txt" is created in the sandbox with content "temporary"
    And the ovl path "temp.txt" is resolved
    And the ovl sandbox is rolled back
    Then the ovl sandbox should be in the "rolled_back" state
    And the ovl file "temp.txt" should not exist in the sandbox

  Scenario: Rollback on a non-active sandbox raises SandboxStateError
    Given an ovl test directory is initialised
    When an ovl sandbox is created for plan "plan-001"
    And the ovl sandbox rollback is attempted on created sandbox
    Then an ovl SandboxStateError should be raised

  # --- Cleanup ---

  Scenario: Cleanup removes the overlay directory
    Given an ovl test directory is initialised
    When an ovl sandbox is created for plan "plan-001"
    And the ovl sandbox is cleaned up
    Then the ovl sandbox should be in the "cleaned_up" state
    And the ovl sandbox base path should not exist

  Scenario: Cleanup is idempotent
    Given an ovl test directory is initialised
    When an ovl sandbox is created for plan "plan-001"
    And the ovl sandbox is cleaned up
    And the ovl sandbox is cleaned up again
    Then the ovl sandbox should be in the "cleaned_up" state

  # --- Protocol properties ---

  Scenario: Overlay sandbox has a unique ULID identifier
    Given an ovl test directory is initialised
    When an ovl sandbox is instantiated
    Then the ovl sandbox_id should be a valid ULID
    And the ovl sandbox status should be "pending"
    And the ovl sandbox context should be None

  # --- Fallback mode ---

  Scenario: Fallback mode works when OverlayFS is unavailable
    Given an ovl test directory is initialised
    When an ovl sandbox is created for plan "plan-fallback"
    Then the ovl sandbox should use userspace fallback

  # --- Coverage boost: edge cases ---

  Scenario: Commit with a message stores it in metadata
    Given an ovl test directory is initialised
    When an ovl sandbox is created for plan "plan-msg"
    And an ovl file "new.txt" is created in the sandbox with content "data"
    And the ovl sandbox is committed with message "snapshot before deploy"
    Then the ovl commit result should indicate success
    And the ovl commit metadata should contain message "snapshot before deploy"

  Scenario: get_path raises SandboxStateError when merged_dir is None
    Given an ovl sandbox with merged_dir forced to None
    When ovl get_path is called with "file.txt" expecting an error
    Then an ovl SandboxStateError should be raised with message "merged directory not set"

  Scenario: Commit wraps OSError into SandboxCommitError
    Given an ovl test directory is initialised
    And an ovl sandbox is created and activated for plan "plan-commiterr"
    And ovl shutil.copy2 is patched to raise OSError
    When the ovl sandbox commit is attempted
    Then an ovl SandboxCommitError should be raised
    And the ovl sandbox should be in the "errored" state

  Scenario: Rollback wraps OSError into SandboxRollbackError
    Given an ovl test directory is initialised
    And an ovl sandbox is created and activated for plan "plan-rberr"
    And ovl shutil.rmtree is patched to raise OSError for rollback
    When the ovl sandbox rollback is attempted
    Then an ovl SandboxRollbackError should be raised
    And the ovl sandbox should be in the "errored" state

  Scenario: Create wraps OSError into SandboxCreationError
    Given an ovl test directory is initialised
    And ovl shutil.copytree is patched to raise OSError
    When an ovl sandbox create is attempted for plan "plan-createerr"
    Then an ovl SandboxCreationError should be raised
    And the ovl sandbox should be in the "errored" state

  # --- Atomic rollback from COMMITTED ---

  Scenario: Rollback from COMMITTED resets the merged directory
    Given an ovl test directory is initialised
    When an ovl sandbox is created for plan "plan-rb-committed"
    And an ovl file "changed.txt" is created in the sandbox with content "modified"
    And the ovl sandbox is committed
    Then the ovl sandbox should be in the "committed" state
    When the ovl sandbox is rolled back from committed
    Then the ovl sandbox should be in the "rolled_back" state
    And the ovl original file "existing.txt" should contain "original content"
    And the ovl merged directory should not contain stale file "changed.txt"

  Scenario: Rollback from COMMITTED with no changes is a no-op
    Given an ovl test directory is initialised
    When an ovl sandbox is created for plan "plan-rb-noop"
    And the ovl sandbox is committed
    Then the ovl sandbox should be in the "committed" state
    When the ovl sandbox is rolled back from committed
    Then the ovl sandbox should be in the "rolled_back" state
    And the ovl original file "existing.txt" should contain "original content"

  # --- Status transitions ---

  Scenario: Status transitions follow protocol
    Given an ovl test directory is initialised
    When an ovl sandbox is created for plan "plan-001"
    Then the ovl sandbox should be in the "created" state
    When the ovl path "existing.txt" is resolved
    Then the ovl sandbox should be in the "active" state
