Feature: Consolidated Change Tracking
  Combined scenarios from: change_tracking, changeset_capture, streamedchange_coverage

  # ============================================================
  # Originally from: change_tracking.feature
  # Feature: ChangeSet Models and Invocation Tracking
  # ============================================================

  Scenario: ChangeType is an alias for ChangeOperation
    Given I import ChangeType and ChangeOperation
    Then ChangeType should be the same class as ChangeOperation
    And ChangeType CREATE should equal "create"

    # ---- ChangeEntry Validators ----


  Scenario: CREATE entry rejects before_hash
    When I create a ChangeEntry with CREATE and before_hash "abc"
    Then a validation error should be raised mentioning "before_hash"


  Scenario: DELETE entry rejects after_hash
    When I create a ChangeEntry with DELETE and after_hash "abc"
    Then a validation error should be raised mentioning "after_hash"


  Scenario: CREATE entry accepts after_hash only
    When I create a valid CREATE ChangeEntry with after_hash "abc123"
    Then the change entry operation should be "create"
    And the change entry after_hash should be "abc123"


  Scenario: MODIFY entry accepts both hashes
    When I create a valid MODIFY ChangeEntry with both hashes
    Then the change entry operation should be "modify"
    And the change entry has_integrity_hashes should be true


  Scenario: RENAME entry without hashes is valid
    When I create a RENAME ChangeEntry without hashes
    Then the change entry operation should be "rename"
    And the change entry has_integrity_hashes should be true


  Scenario: CREATE entry without after_hash has no integrity
    When I create a CREATE ChangeEntry without any hashes
    Then the change entry has_integrity_hashes should be false

    # ---- Path Normalization ----


  Scenario: Normalize absolute path with repo root
    Given a repo root "/home/user/project"
    When I normalize the path "/home/user/project/src/main.py"
    Then the normalized path should be "src/main.py"


  Scenario: Normalize relative path stays unchanged
    Given an empty repo root
    When I normalize the path "src/main.py"
    Then the normalized path should be "src/main.py"


  Scenario: Normalize path outside repo root is unchanged
    Given a repo root "/other/project"
    When I normalize the path "/home/user/project/src/main.py"
    Then the normalized path should be "home/user/project/src/main.py"

    # ---- SpecChangeSet Ordering ----


  Scenario: sorted_entries returns deterministic order
    Given a SpecChangeSet with entries in random order
    When I call sorted_entries
    Then entries should be sorted by resource_id then path then timestamp


  Scenario: grouped_by_resource groups correctly
    Given a SpecChangeSet with entries for multiple resources
    When I call grouped_by_resource
    Then entries should be grouped by resource_id
    And each group should be sorted by path and timestamp


  Scenario: add_change appends to entries
    Given an empty SpecChangeSet
    When I add a change entry to it
    Then the spec changeset should contain 1 entry

    # ---- ToolInvocation Model ----


  Scenario: Create a ToolInvocation with minimal fields
    When I create a ToolInvocation for plan "plan-1" and tool "builtin/file-write"
    Then the invocation should have a ULID invocation_id
    And the invocation plan_id should be "plan-1"
    And the invocation tool_name should be "builtin/file-write"
    And the invocation success should be true


  Scenario: Create a failed ToolInvocation
    When I create a failed ToolInvocation with error "file not found"
    Then the invocation success should be false
    And the invocation error should be "file not found"


  Scenario: ToolInvocation with change_ids
    When I create a ToolInvocation with change_ids
    Then the invocation should have 2 change_ids


  Scenario: ToolInvocation with provider metadata
    When I create a ToolInvocation with provider metadata
    Then the invocation provider_metadata should contain model name

    # ---- InMemoryInvocationTracker ----


  Scenario: Track invocations and retrieve by plan
    Given an InMemoryInvocationTracker
    When I track 3 invocations for plan "plan-A"
    And I track 2 invocations for plan "plan-B"
    Then get_invocations for "plan-A" should return 3
    And get_invocations for "plan-B" should return 2


  Scenario: Retrieve invocations for a specific skill
    Given an InMemoryInvocationTracker
    When I track invocations for different skills
    Then get_invocations_for_skill should filter correctly


  Scenario: Get change IDs across invocations
    Given an InMemoryInvocationTracker
    When I track invocations with change_ids
    Then get_changes should return all change_ids for the plan


  Scenario: Invocations are ordered by sequence number
    Given an InMemoryInvocationTracker
    When I track invocations out of order
    Then get_invocations should return them in sequence order

    # ---- SpecChangeSet summary ----


  Scenario: summary returns correct counts for mixed operations
    Given a SpecChangeSet with mixed operation entries
    When I call summary on the changeset
    Then the summary total should be 4
    And the summary creates should be 1
    And the summary modifies should be 1
    And the summary deletes should be 1
    And the summary renames should be 1
    And the summary paths_changed should be 4
    And the summary resources_involved should be 2

    # ---- add_change plan_id validation ----


  Scenario: add_change rejects entry with mismatched plan_id
    Given an empty SpecChangeSet
    When I add a change entry with a different plan_id
    Then a validation error should be raised mentioning "plan_id"

    # ---- Empty tracker queries ----


  Scenario: get_invocations returns empty for unknown plan
    Given an InMemoryInvocationTracker
    Then get_invocations for "nonexistent-plan" should return 0


  Scenario: get_changes returns empty for unknown plan
    Given an InMemoryInvocationTracker
    Then get_changes for "nonexistent-plan" should return 0 ids


  Scenario: get_invocations_for_skill returns empty for unknown skill
    Given an InMemoryInvocationTracker
    When I track 1 invocations for plan "plan-X"
    Then get_invocations_for_skill "plan-X" "no-such-skill" should return 0

    # ---- get_changes ordering proof ----


  Scenario: get_changes respects sequence order not insertion order
    Given an InMemoryInvocationTracker
    When I track change invocations in reverse sequence order
    Then get_changes should return change_ids in sequence order

    # ---- Path normalization edge cases ----


  Scenario: Normalize path that equals repo root returns dot
    Given a repo root "/home/user/project"
    When I normalize the path "/home/user/project"
    Then the normalized path should be "."


  Scenario: Normalize path with dot-dot components preserves them
    Given a repo root "/repo"
    When I normalize the path "/repo/foo/../bar/file.py"
    Then the normalized path should be "foo/../bar/file.py"


  Scenario: Normalize empty path returns dot
    Given an empty repo root
    When I normalize the path "."
    Then the normalized path should be "."


  # ============================================================
  # Originally from: changeset_capture.feature
  # Feature: ChangeSet capture and domain model
  # ============================================================

  Scenario: ChangeOperation enum has expected members
    Given I import the ChangeOperation enum
    Then it should have members CREATE, MODIFY, DELETE, RENAME

    # ---- ChangeEntry model ----


  Scenario: Create a ChangeEntry with all fields
    Given I import the ChangeEntry model
    When I create a ChangeEntry for a create operation
    Then the entry should have a ULID entry_id
    And the entry should have operation "create"
    And the entry should have a UTC timestamp
    And the before_hash should be None


  Scenario: ChangeEntry for modify operation has before and after hashes
    Given I import the ChangeEntry model
    When I create a ChangeEntry for a modify operation with hashes
    Then the before_hash should not be None
    And the after_hash should not be None


  Scenario: ChangeEntry for delete operation has no after_hash
    Given I import the ChangeEntry model
    When I create a ChangeEntry for a delete operation
    Then the after_hash should be None
    And the before_hash should not be None


  Scenario: ChangeEntry for rename operation
    Given I import the ChangeEntry model
    When I create a ChangeEntry for a rename operation
    Then the entry should have operation "rename"

    # ---- SpecChangeSet model ----


  Scenario: SpecChangeSet summary counts are correct
    Given I import the SpecChangeSet model
    When I create a SpecChangeSet with mixed operations
    Then the creates count should be 1
    And the modifies count should be 2
    And the deletes count should be 1
    And the renames count should be 1
    And paths_changed should have 5 entries
    And resources_involved should have 2 entries


  Scenario: SpecChangeSet summary dict
    Given I import the SpecChangeSet model
    When I create a SpecChangeSet with mixed operations
    Then the summary dict should have correct totals


  Scenario: Empty SpecChangeSet has zero counts
    Given I import the SpecChangeSet model
    When I create an empty SpecChangeSet
    Then the creates count should be 0
    And the modifies count should be 0
    And the deletes count should be 0
    And the renames count should be 0

    # ---- InMemoryChangeSetStore ----


  Scenario: InMemoryChangeSetStore start/record/get flow
    Given I have an InMemoryChangeSetStore
    When I start a changeset for plan "plan-abc"
    And I record a create entry in the changeset
    Then I can get the changeset by ID
    And the store changeset should have 1 entry


  Scenario: InMemoryChangeSetStore get_for_plan
    Given I have an InMemoryChangeSetStore
    When I start a changeset for plan "plan-abc"
    And I start a changeset for plan "plan-abc"
    And I start a changeset for plan "plan-xyz"
    Then get_for_plan "plan-abc" should return 2 changesets
    And get_for_plan "plan-xyz" should return 1 changeset


  Scenario: InMemoryChangeSetStore summarize
    Given I have an InMemoryChangeSetStore
    When I start a changeset for plan "plan-s"
    And I record a create entry in the changeset
    And I record a modify entry in the changeset
    Then the summarized changeset should show 2 total


  Scenario: InMemoryChangeSetStore get returns None for unknown ID
    Given I have an InMemoryChangeSetStore
    Then getting a non-existent changeset returns None


  Scenario: InMemoryChangeSetStore record raises on unknown ID
    Given I have an InMemoryChangeSetStore
    Then recording to a non-existent changeset raises KeyError


  Scenario: InMemoryChangeSetStore summarize returns empty for unknown
    Given I have an InMemoryChangeSetStore
    Then summarizing a non-existent changeset returns empty dict

    # ---- ChangeSetCapture integration ----


  Scenario: ChangeSetCapture records resource_id and tool_name
    Given I have a ChangeSetCapture with resource_id "res-1"
    When I create a ChangeSetEntry via capture
    Then the entry resource_id should be "res-1"
    And the entry tool_name should be set


  Scenario: ChangeSetCapture normalizes paths
    Given I have a ChangeSetCapture with sandbox_root
    When I capture a write to a nested path
    Then the captured path should be repo-relative


  Scenario: ChangeSetCapture converts to spec changeset
    Given I have a ChangeSetCapture with resource_id "res-2"
    When I add several entries via capture
    Then to_spec_changeset should return a SpecChangeSet

    # ---- Multi-resource plan ----


  Scenario: Multi-resource plan captures correctly
    Given I have two ChangeSetCapture instances for different resources
    When each capture records changes for its resource
    Then each changeset should only have its resource entries


  # ============================================================
  # Originally from: streamedchange_coverage.feature
  # Feature: Streamed Change Models Coverage
  # ============================================================

  Scenario: Create StreamedChangeSection with valid line numbers
    Given I import the StreamedChangeSection class
    When I create a StreamedChangeSection with start_line 10
    And I set end_line to 20
    And I set start_line_string to "def function():"
    And I set end_line_string to "return result"
    Then the StreamedChangeSection instance should be created successfully
    And the start_line should be 10
    And the end_line should be 20
    And the start_line_string should be "def function():"
    And the end_line_string should be "return result"


  Scenario: Create StreamedChangeSection with single line change
    Given I import the StreamedChangeSection class
    When I create a StreamedChangeSection with start_line 1
    And I set end_line to 1
    And I set start_line_string to "import os"
    And I set end_line_string to "import os"
    Then the StreamedChangeSection instance should be created successfully
    And the start_line should be 1
    And the end_line should be 1
    And the start_line_string should be "import os"
    And the end_line_string should be "import os"


  Scenario: Create StreamedChangeSection with large line numbers
    Given I import the StreamedChangeSection class
    When I create a StreamedChangeSection with start_line 1000
    And I set end_line to 2000
    And I set start_line_string to "class LargeClass:"
    And I set end_line_string to "# End of class"
    Then the StreamedChangeSection instance should be created successfully
    And the start_line should be 1000
    And the end_line should be 2000


  Scenario: Create StreamedChangeSection using field aliases
    Given I import the StreamedChangeSection class
    When I create a StreamedChangeSection using aliases
    Then the StreamedChangeSection should accept the aliased fields
    And the section values should be properly mapped


  Scenario: Validate StreamedChangeSection configuration
    Given I import the StreamedChangeSection class
    When I create a StreamedChangeSection with valid data
    Then the section configuration should have str_strip_whitespace as true
    And the section configuration should have validate_assignment as true
    And the section configuration should have arbitrary_types_allowed as false
    And the section configuration should have populate_by_name as true
    And the section configuration should have use_enum_values as true


  Scenario: Create StreamedChangeWithLineNums with included boundaries
    Given I import the StreamedChangeWithLineNums class
    And I have a valid StreamedChangeSection for old section
    When I create a StreamedChangeWithLineNums with the old section
    And I set start_line_included to true
    And I set end_line_included to true
    And I set new content to "Updated code block"
    Then the StreamedChangeWithLineNums instance should be created successfully
    And the old section should match the provided section
    And the start_line_included should be true
    And the end_line_included should be true
    And the new content should be "Updated code block"


  Scenario: Create StreamedChangeWithLineNums with excluded boundaries
    Given I import the StreamedChangeWithLineNums class
    And I have a valid StreamedChangeSection for old section
    When I create a StreamedChangeWithLineNums with the old section
    And I set start_line_included to false
    And I set end_line_included to false
    And I set new content to "Replacement text"
    Then the StreamedChangeWithLineNums instance should be created successfully
    And the start_line_included should be false
    And the end_line_included should be false
    And the new content should be "Replacement text"


  Scenario: Create StreamedChangeWithLineNums with mixed boundaries
    Given I import the StreamedChangeWithLineNums class
    And I have a valid StreamedChangeSection for old section
    When I create a StreamedChangeWithLineNums with the old section
    And I set start_line_included to true
    And I set end_line_included to false
    And I set new content to "Partial replacement"
    Then the StreamedChangeWithLineNums instance should be created successfully
    And the start_line_included should be true
    And the end_line_included should be false


  Scenario: Create StreamedChangeWithLineNums with empty new content
    Given I import the StreamedChangeWithLineNums class
    And I have a valid StreamedChangeSection for old section
    When I create a StreamedChangeWithLineNums with the old section
    And I set start_line_included to true
    And I set end_line_included to true
    And I set new content to ""
    Then the StreamedChangeWithLineNums instance should be created successfully
    And the new content should be ""


  Scenario: Create StreamedChangeWithLineNums with multiline new content
    Given I import the StreamedChangeWithLineNums class
    And I have a valid StreamedChangeSection for old section
    When I create a StreamedChangeWithLineNums with the old section
    And I set start_line_included to true
    And I set end_line_included to true
    And I set new content to multiline text
    Then the StreamedChangeWithLineNums instance should be created successfully
    And the new content should contain multiple lines


  Scenario: Create StreamedChangeWithLineNums using field aliases
    Given I import the StreamedChangeWithLineNums class
    And I have a valid StreamedChangeSection for old section
    When I create a StreamedChangeWithLineNums using aliases
    Then the StreamedChangeWithLineNums should accept the aliased fields
    And the change values should be properly mapped


  Scenario: Validate StreamedChangeWithLineNums configuration
    Given I import the StreamedChangeWithLineNums class
    When I create a StreamedChangeWithLineNums with valid data
    Then the change configuration should have str_strip_whitespace as true
    And the change configuration should have validate_assignment as true
    And the change configuration should have arbitrary_types_allowed as false
    And the change configuration should have populate_by_name as true
    And the change configuration should have use_enum_values as true


  Scenario: StreamedChangeSection strips whitespace from strings
    Given I import the StreamedChangeSection class
    When I create a StreamedChangeSection with whitespace in strings
    Then the whitespace should be stripped from string fields
    And the section should be created successfully


  Scenario: StreamedChangeWithLineNums strips whitespace from new content
    Given I import the StreamedChangeWithLineNums class
    And I have a valid StreamedChangeSection for old section
    When I create a StreamedChangeWithLineNums with whitespace in new content
    Then the whitespace should be stripped from new field
    And the change should be created successfully


  Scenario: Complex StreamedChangeWithLineNums with nested section
    Given I import the StreamedChangeWithLineNums class
    When I create a complex StreamedChangeWithLineNums
    Then the nested StreamedChangeSection should be accessible
    And all nested fields should be properly initialized


  Scenario: StreamedChangeSection with special characters in strings
    Given I import the StreamedChangeSection class
    When I create a StreamedChangeSection with special characters
    Then the special characters should be preserved
    And the section should handle unicode properly


  Scenario: Validate StreamedChangeSection field types
    Given I import the StreamedChangeSection class
    When I create a StreamedChangeSection with different field types
    Then the integer fields should be validated
    And the string fields should be validated


  Scenario: Validate StreamedChangeWithLineNums field types
    Given I import the StreamedChangeWithLineNums class
    And I have a valid StreamedChangeSection for old section
    When I create a StreamedChangeWithLineNums with different field types
    Then the boolean fields should be validated
    And the nested object should be validated


  Scenario: StreamedChangeSection with boundary line numbers
    Given I import the StreamedChangeSection class
    When I create a StreamedChangeSection with start_line 0
    And I set end_line to 0
    And I set start_line_string to "# First line"
    And I set end_line_string to "# First line"
    Then the StreamedChangeSection instance should be created successfully
    And the start_line should be 0
    And the end_line should be 0


  Scenario: StreamedChangeWithLineNums represents code deletion
    Given I import the StreamedChangeWithLineNums class
    And I have a StreamedChangeSection representing lines to delete
    When I create a StreamedChangeWithLineNums for deletion
    And I set start_line_included to true
    And I set end_line_included to true
    And I set new content to ""
    Then the StreamedChangeWithLineNums instance should represent a deletion
    And the old section should contain the deleted content
