Feature: CheckpointManager full coverage
  As a developer
  I want thorough tests for every method, branch, and edge case in checkpoint.py
  So that line-rate and branch-rate reach 1.0

  # ---------------------------------------------------------------------------
  # SandboxCheckpoint Pydantic model
  # ---------------------------------------------------------------------------

  Scenario: SandboxCheckpoint model can be constructed with all fields
    Given I build a SandboxCheckpoint with valid fields
    Then the constructed checkpoint model should expose all field values
    And the constructed checkpoint model should be frozen

  Scenario: SandboxCheckpoint model uses empty dict as default metadata
    Given I build a SandboxCheckpoint without explicit metadata
    Then the constructed checkpoint metadata should be an empty dict

  # ---------------------------------------------------------------------------
  # Checkpointable protocol
  # ---------------------------------------------------------------------------

  Scenario: An object satisfying Checkpointable passes isinstance check
    Given a mock object with sandbox_id and context properties
    Then the mock object should satisfy the Checkpointable protocol

  Scenario: An object missing sandbox_id fails Checkpointable isinstance check
    Given a mock object without a sandbox_id property
    Then the mock object should not satisfy the Checkpointable protocol

  # ---------------------------------------------------------------------------
  # CheckpointManager.__init__
  # ---------------------------------------------------------------------------

  Scenario: CheckpointManager initialises with empty state
    Given a freshly created CheckpointManager
    Then the manager internal checkpoints dict should be empty
    And the manager should have a threading lock

  # ---------------------------------------------------------------------------
  # create_checkpoint – context is None (metadata-only)
  # ---------------------------------------------------------------------------

  Scenario: Creating a checkpoint when sandbox context is None produces a metadata-only snapshot
    Given a freshly created CheckpointManager
    And a mock sandbox whose context is None
    When I invoke create_checkpoint with plan "plan-m1" phase "pre_execute" and no metadata
    Then the returned checkpoint should have a non-empty snapshot_path directory
    And the returned checkpoint sandbox_id should equal the mock sandbox id

  Scenario: Creating a checkpoint with explicit metadata converts values to strings
    Given a freshly created CheckpointManager
    And a mock sandbox whose context is None
    When I invoke create_checkpoint with plan "plan-m2" phase "post_execute" and numeric metadata
    Then the returned checkpoint metadata values should all be strings

  Scenario: Creating a checkpoint with None metadata defaults to empty dict
    Given a freshly created CheckpointManager
    And a mock sandbox whose context is None
    When I invoke create_checkpoint with plan "plan-m3" phase "pre_apply" and None metadata
    Then the returned checkpoint metadata should be an empty dict

  # ---------------------------------------------------------------------------
  # create_checkpoint – context with sandbox_path
  # ---------------------------------------------------------------------------

  Scenario: Creating a checkpoint with a real sandbox path snapshots the directory
    Given a freshly created CheckpointManager
    And a temporary sandbox directory with sample files
    And a mock sandbox whose context points to the temporary directory
    When I invoke create_checkpoint with plan "plan-s1" phase "pre_execute" and no metadata
    Then the snapshot directory should contain copies of the sample files

  Scenario: Creating a checkpoint with a real sandbox path stores sandbox_path in metadata
    Given a freshly created CheckpointManager
    And a temporary sandbox directory with sample files
    And a mock sandbox whose context points to the temporary directory
    When I invoke create_checkpoint with plan "plan-s2" phase "pre_execute" and no metadata
    Then the returned checkpoint metadata should contain the sandbox path

  Scenario: Rollback succeeds without manually supplying sandbox_path in metadata
    Given a freshly created CheckpointManager
    And a temporary sandbox directory with sample files
    And a mock sandbox whose context points to the temporary directory
    And a checkpoint was created for the temporary sandbox without explicit sandbox_path metadata
    And the temporary sandbox files are then modified
    When I invoke rollback_to on the created checkpoint
    Then the rollback result should be true
    And the temporary sandbox should contain the original files

  # ---------------------------------------------------------------------------
  # _snapshot_directory – branches
  # ---------------------------------------------------------------------------

  Scenario: _snapshot_directory with None path creates an empty snapshot dir
    Given a freshly created CheckpointManager
    When I call _snapshot_directory with a None sandbox_path
    Then the returned snapshot should be an existing empty directory

  Scenario: _snapshot_directory with non-existent path creates an empty snapshot dir
    Given a freshly created CheckpointManager
    When I call _snapshot_directory with a non-existent sandbox_path
    Then the returned snapshot should be an existing empty directory

  Scenario: _snapshot_directory with a valid path copies the tree
    Given a freshly created CheckpointManager
    And a temporary sandbox directory with sample files
    When I call _snapshot_directory with the temporary sandbox_path
    Then the returned snapshot should contain the sample files

  Scenario: _snapshot_directory raises SandboxError when copytree fails
    Given a freshly created CheckpointManager
    And a temporary sandbox directory with sample files
    When I call _snapshot_directory with a path that triggers OSError
    Then a SandboxError should have been raised

  # ---------------------------------------------------------------------------
  # _cleanup_snapshot – branches
  # ---------------------------------------------------------------------------

  Scenario: _cleanup_snapshot with empty string is a no-op
    Given a freshly created CheckpointManager
    When I call _cleanup_snapshot with an empty string
    Then no error should occur from cleanup_snapshot

  Scenario: _cleanup_snapshot removes parent directory when it exists
    Given a freshly created CheckpointManager
    And a temporary snapshot directory tree for cleanup
    When I call _cleanup_snapshot with the temporary snapshot path
    Then the temporary snapshot parent should no longer exist

  Scenario: _cleanup_snapshot with non-existent parent is a no-op
    Given a freshly created CheckpointManager
    When I call _cleanup_snapshot with a non-existent path
    Then no error should occur from cleanup_snapshot

  # ---------------------------------------------------------------------------
  # rollback_to – all branches
  # ---------------------------------------------------------------------------

  Scenario: Rollback returns false when snapshot_path is empty string
    Given a freshly created CheckpointManager
    And a SandboxCheckpoint with empty snapshot_path
    When I invoke rollback_to on the crafted checkpoint
    Then the rollback result should be false

  Scenario: Rollback returns false when snapshot_path does not exist on disk
    Given a freshly created CheckpointManager
    And a SandboxCheckpoint with a non-existent snapshot_path
    When I invoke rollback_to on the crafted checkpoint
    Then the rollback result should be false

  Scenario: Rollback returns false when sandbox_path is missing from metadata
    Given a freshly created CheckpointManager
    And a SandboxCheckpoint whose snapshot exists but metadata has no sandbox_path
    When I invoke rollback_to on the crafted checkpoint
    Then the rollback result should be false

  Scenario: Rollback returns false when sandbox_path directory does not exist
    Given a freshly created CheckpointManager
    And a SandboxCheckpoint whose snapshot exists but sandbox_path points nowhere
    When I invoke rollback_to on the crafted checkpoint
    Then the rollback result should be false

  Scenario: Rollback succeeds and restores files and subdirectories
    Given a freshly created CheckpointManager
    And a temporary sandbox directory with sample files
    And a mock sandbox whose context points to the temporary directory
    And a checkpoint was created for the temporary sandbox
    And the temporary sandbox files are then modified
    When I invoke rollback_to on the created checkpoint
    Then the rollback result should be true
    And the temporary sandbox should contain the original files

  Scenario: Rollback handles OSError during restore gracefully
    Given a freshly created CheckpointManager
    And a SandboxCheckpoint whose snapshot and sandbox exist but restore will fail
    When I invoke rollback_to on the crafted checkpoint
    Then the rollback result should be false

  # ---------------------------------------------------------------------------
  # list_checkpoints – branches
  # ---------------------------------------------------------------------------

  Scenario: Listing checkpoints for unknown sandbox returns empty list
    Given a freshly created CheckpointManager
    When I invoke list_checkpoints for sandbox "unknown-sb-99"
    Then the checkpoint list should be empty

  Scenario: Listing checkpoints returns all created checkpoints in order
    Given a freshly created CheckpointManager
    And a mock sandbox whose context is None
    And I create two checkpoints for the mock sandbox
    When I invoke list_checkpoints for the mock sandbox
    Then the checkpoint list should have 2 entries in creation order

  # ---------------------------------------------------------------------------
  # delete_checkpoint – branches
  # ---------------------------------------------------------------------------

  Scenario: Deleting a known checkpoint returns true and removes it
    Given a freshly created CheckpointManager
    And a mock sandbox whose context is None
    And a single checkpoint is created for the mock sandbox
    When I invoke delete_checkpoint with the created checkpoint id
    Then the checkpoint delete result should be true
    And listing checkpoints for the mock sandbox should return 0 entries

  Scenario: Deleting an unknown checkpoint id returns false
    Given a freshly created CheckpointManager
    When I invoke delete_checkpoint with id "no-such-cp-id"
    Then the checkpoint delete result should be false
