Feature: Decision recording and snapshot store
  As a developer
  I want a DecisionService that records decisions with auto-sequencing and snapshots
  So that plan decision trees are tracked with full context for replay

  Background:
    Given a decision service

  # --- Recording ---

  Scenario: Record a root decision with auto-sequencing
    When I record a prompt_definition decision for plan "P1" with question "What is the plan prompt?"
    Then the dsvc decision should be recorded successfully
    And the dsvc decision sequence number should be 0
    And the dsvc decision type should be "prompt_definition"
    And the dsvc decision should be a root decision
    And the dsvc decision should have a valid ULID as decision_id
    And the dsvc decision context snapshot hash should not be empty

  Scenario: Record multiple decisions with monotonic sequencing
    When I record a prompt_definition decision for plan "P1" with question "Plan prompt"
    And I record a strategy_choice decision for plan "P1" with question "Which approach?"
    And I record an implementation_choice decision for plan "P1" with question "How to implement?"
    Then the dsvc plan "P1" should have 3 decisions
    And the dsvc decisions should have sequence numbers 0 1 2

  Scenario: Record a decision with explicit context snapshot
    Given a context snapshot with hash "sha256:abc123" and ref "s3://bucket/ctx"
    When I record a strategy_choice decision for plan "P1" with the explicit snapshot
    Then the dsvc decision context snapshot hash should be "sha256:abc123"
    And the dsvc decision context snapshot ref should be "s3://bucket/ctx"

  Scenario: Record a decision with confidence score
    When I record a strategy_choice decision for plan "P1" with confidence 0.85
    Then the dsvc decision confidence score should be 0.85

  Scenario: Record a correction decision
    When I record a prompt_definition decision for plan "P1" with question "Original"
    And I record a correction decision for plan "P1" correcting the first decision
    Then the dsvc correction decision is_correction should be true
    And the dsvc correction decision corrects_decision_id should match the first decision

  Scenario: Record a decision with alternatives
    When I record a strategy_choice decision for plan "P1" with alternatives "Option A" "Option B" "Option C"
    Then the dsvc decision should have 3 alternatives considered

  Scenario: Record a decision with artifacts
    When I record an implementation_choice decision for plan "P1" with artifacts
    Then the dsvc decision should have 2 artifacts produced

  Scenario: Recording with empty plan_id raises ValidationError
    When I try to record a decision with empty plan_id
    Then a dsvc validation error should be raised
    And the dsvc error should mention "plan_id"

  Scenario: Recording with empty question raises ValidationError
    When I try to record a decision with empty question
    Then a dsvc validation error should be raised
    And the dsvc error should mention "question"

  Scenario: Recording with empty chosen_option raises ValidationError
    When I try to record a decision with empty chosen_option
    Then a dsvc validation error should be raised
    And the dsvc error should mention "chosen_option"

  Scenario: Recording with invalid decision_type raises ValueError
    When I try to record a decision with invalid type "nonexistent_type"
    Then a dsvc value error should be raised
    And the dsvc error should mention "nonexistent_type"

  Scenario: Recording with confidence_score at boundaries
    When I record a strategy_choice decision for plan "P1" with confidence 0.0
    Then the dsvc decision confidence score should be 0.0
    When I record a strategy_choice decision for plan "P1" with confidence 1.0
    Then the dsvc decision confidence score should be 1.0

  Scenario: Recording with negative confidence_score raises ValidationError
    When I try to record a decision with confidence -0.1
    Then a dsvc validation error should be raised

  Scenario: Recording with confidence_score above 1.0 raises ValidationError
    When I try to record a decision with confidence 1.1
    Then a dsvc validation error should be raised

  Scenario: Auto-capture generates a snapshot hash
    When I record a strategy_choice decision for plan "P1" with question "Test auto-capture"
    Then the dsvc decision context snapshot hash should start with "sha256:"

  # --- Retrieval ---

  Scenario: Get a decision by ID
    When I record a prompt_definition decision for plan "P1" with question "Test get"
    And I get the decision by its ID
    Then the dsvc retrieved decision should match the recorded decision

  Scenario: Get a non-existent decision raises DecisionNotFoundError
    When I try to get decision "01NONEXISTENT00000000000000"
    Then a dsvc decision not found error should be raised

  Scenario: List decisions for a plan returns all decisions ordered
    When I record a prompt_definition decision for plan "P1" with question "First"
    And I record a strategy_choice decision for plan "P1" with question "Second"
    And I record an implementation_choice decision for plan "P1" with question "Third"
    And I list decisions for plan "P1"
    Then the dsvc decision list should have 3 entries
    And the dsvc decision list should be ordered by sequence number

  Scenario: List decisions for empty plan returns empty list
    When I list decisions for plan "P_EMPTY"
    Then the dsvc decision list should have 0 entries

  Scenario: List decisions by type filters correctly
    When I record a prompt_definition decision for plan "P1" with question "Root"
    And I record a strategy_choice decision for plan "P1" with question "Strategy 1"
    And I record a strategy_choice decision for plan "P1" with question "Strategy 2"
    And I record an implementation_choice decision for plan "P1" with question "Impl"
    And I filter decisions for plan "P1" by type "strategy_choice"
    Then the dsvc decision list should have 2 entries

  # --- Tree operations ---

  Scenario: Get tree returns BFS order from root
    When I record a prompt_definition decision for plan "P1" with question "Root"
    And I record a strategy_choice decision for plan "P1" with parent as child "Child 1"
    And I record a strategy_choice decision for plan "P1" with parent as child "Child 2"
    And I record an implementation_choice decision for plan "P1" with second parent as child "Grandchild"
    And I get the tree for plan "P1"
    Then the dsvc tree should have 4 decisions
    And the dsvc first tree decision should be a root decision
    And the dsvc tree should be in BFS level order

  Scenario: Get tree for empty plan returns empty list
    When I get the tree for plan "P_EMPTY"
    Then the dsvc tree should have 0 decisions

  Scenario: Get path to root navigates upward
    When I record a prompt_definition decision for plan "P1" with question "Root"
    And I record a strategy_choice decision for plan "P1" with parent as child "Level 1"
    And I record an implementation_choice decision for plan "P1" with second parent as child "Level 2"
    And I get the path to root from the last decision
    Then the dsvc path should have 3 decisions
    And the dsvc first path decision should be the last recorded decision
    And the dsvc last path decision should be the root

  Scenario: Get path to root for non-existent decision raises error
    When I try to get path to root for decision "01NONEXISTENT00000000000000"
    Then a dsvc decision not found error should be raised

  # --- Superseded decisions ---

  Scenario: Mark a decision as superseded
    When I record a prompt_definition decision for plan "P1" with question "Original"
    And I record a strategy_choice decision for plan "P1" with question "Replacement"
    And I mark the first decision as superseded by the second
    Then the dsvc first decision should be superseded
    And the dsvc first decision superseded_by should match the second decision

  Scenario: Get superseded decisions for a plan
    When I record a prompt_definition decision for plan "P1" with question "Original"
    And I record a strategy_choice decision for plan "P1" with question "Replacement"
    And I mark the first decision as superseded by the second
    And I get superseded decisions for plan "P1"
    Then the dsvc superseded list should have 1 entry

  Scenario: Mark non-existent decision raises DecisionNotFoundError
    When I try to mark decision "01NONEXISTENT00000000000000" as superseded
    Then a dsvc decision not found error should be raised

  # --- Delete ---

  Scenario: Delete a decision
    When I record a prompt_definition decision for plan "P1" with question "To delete"
    And I delete the recorded decision
    Then the dsvc decision should be deleted
    And the dsvc plan "P1" should have 0 decisions

  Scenario: Delete non-existent decision raises DecisionNotFoundError
    When I try to delete decision "01NONEXISTENT00000000000000"
    Then a dsvc decision not found error should be raised

  # --- Snapshot store ---

  Scenario: Snapshot is stored when decision is recorded
    When I record a strategy_choice decision for plan "P1" with question "Snapshot test"
    And I get the snapshot for the recorded decision
    Then the dsvc snapshot should not be None

  Scenario: Get snapshots for plan returns all snapshots
    When I record a prompt_definition decision for plan "P1" with question "First"
    And I record a strategy_choice decision for plan "P1" with question "Second"
    And I get snapshots for plan "P1"
    Then the dsvc snapshots dict should have 2 entries

  Scenario: Snapshot hash deduplication index works
    Given a context snapshot with hash "sha256:samehash" and ref "ref1"
    When I record a strategy_choice decision for plan "P1" with the explicit snapshot
    And I record a strategy_choice decision for plan "P2" with the same explicit snapshot
    And I query the snapshot store by hash "sha256:samehash"
    Then the dsvc hash query should return 2 decision IDs

  Scenario: Snapshot is removed when decision is deleted
    When I record a prompt_definition decision for plan "P1" with question "Ephemeral"
    And I delete the recorded decision
    And I get the snapshot for the deleted decision
    Then the dsvc snapshot should be None

  # --- Statistics ---

  Scenario: Count decisions for a plan
    When I record a prompt_definition decision for plan "P1" with question "One"
    And I record a strategy_choice decision for plan "P1" with question "Two"
    Then the dsvc decision count for plan "P1" should be 2

  Scenario: Get next sequence returns the expected value
    Then the dsvc next sequence for plan "P_NEW" should be 0
    When I record a prompt_definition decision for plan "P_NEW" with question "First"
    Then the dsvc next sequence for plan "P_NEW" should be 1

  # --- DuplicateDecisionError ---

  Scenario: DuplicateDecisionError contains decision_id
    Given a DuplicateDecisionError for decision "DUP123"
    Then the dsvc duplicate error decision_id should be "DUP123"
    And the dsvc duplicate error message should contain "DUP123"

  # --- SequenceConflictError ---

  Scenario: SequenceConflictError contains plan_id and sequence
    Given a SequenceConflictError for plan "P1" sequence 5
    Then the dsvc sequence error plan_id should be "P1"
    And the dsvc sequence error sequence_number should be 5
    And the dsvc sequence error message should contain "P1"

  # --- SnapshotStore standalone ---

  Scenario: SnapshotStore remove returns False for unknown decision
    Given a standalone snapshot store
    When I remove snapshot for decision "UNKNOWN"
    Then the dsvc snapshot remove result should be False

  Scenario: SnapshotStore get returns None for unknown decision
    Given a standalone snapshot store
    When I get snapshot for decision "UNKNOWN"
    Then the dsvc standalone snapshot should be None

  Scenario: SnapshotStore get_by_hash returns empty for unknown hash
    Given a standalone snapshot store
    When I query the standalone store by hash "sha256:nope"
    Then the dsvc standalone hash query should return 0 decision IDs

  Scenario: DecisionService string type coercion works
    When I record a decision for plan "P1" with string type "strategy_choice"
    Then the dsvc decision type should be "strategy_choice"

  # --- Edge-case coverage ---

  Scenario: Snapshot stored without a hash skips hash index
    Given a standalone snapshot store
    And a context snapshot with no hash
    When I store the hashless snapshot for decision "D1"
    And I get standalone snapshot for decision "D1"
    Then the dsvc standalone retrieved snapshot should not be None
    And the dsvc standalone retrieved snapshot hash should be empty

  Scenario: Snapshot store remove cleans up hash index
    Given a standalone snapshot store
    And a context snapshot with hash "sha256:removeme" and ref "ref"
    When I store the explicit snapshot for decision "D_REM"
    And I remove snapshot for decision "D_REM"
    Then the dsvc snapshot remove result should be True
    And the dsvc standalone hash query for "sha256:removeme" should return 0 decision IDs

  Scenario: Snapshot store remove last entry deletes hash bucket
    Given a standalone snapshot store
    And a context snapshot with hash "sha256:onlyone" and ref "ref"
    When I store the explicit snapshot for decision "D_ONLY"
    And I remove snapshot for decision "D_ONLY"
    Then the dsvc snapshot remove result should be True
    And the dsvc standalone hash query for "sha256:onlyone" should return 0 decision IDs

  Scenario: Snapshot store remove snapshot with no hash
    Given a standalone snapshot store
    And a context snapshot with no hash
    When I store the hashless snapshot for decision "D_NOHASH"
    And I remove snapshot for decision "D_NOHASH"
    Then the dsvc snapshot remove result should be True

  Scenario: Snapshot store remove with multiple entries keeps remaining
    Given a standalone snapshot store
    And a context snapshot with hash "sha256:shared" and ref "ref"
    When I store the explicit snapshot for decision "D_KEEP1"
    And I store the explicit snapshot for decision "D_KEEP2"
    And I remove snapshot for decision "D_KEEP1"
    Then the dsvc snapshot remove result should be True
    And the dsvc standalone hash query for "sha256:shared" should return 1 decision IDs

  Scenario: Get tree when no decision is a root returns all decisions
    When I record a decision for plan "P1" with a forced non-root parent
    And I get the tree for plan "P1"
    Then the dsvc tree should have 1 decisions

  Scenario: List by type with string coercion filters correctly
    When I record a prompt_definition decision for plan "P1" with question "Type1"
    And I record a strategy_choice decision for plan "P1" with question "Type2"
    And I filter decisions for plan "P1" by type "prompt_definition"
    Then the dsvc decision list should have 1 entries

  Scenario: Recording with whitespace-only plan_id raises ValidationError
    When I try to record a decision with whitespace plan_id
    Then a dsvc validation error should be raised
    And the dsvc error should mention "plan_id"

  Scenario: Recording with whitespace-only question raises ValidationError
    When I try to record a decision with whitespace question
    Then a dsvc validation error should be raised
    And the dsvc error should mention "question"

  Scenario: Recording with whitespace-only chosen_option raises ValidationError
    When I try to record a decision with whitespace chosen_option
    Then a dsvc validation error should be raised
    And the dsvc error should mention "chosen_option"

  Scenario: SnapshotStore list_for_plan skips missing snapshots
    Given a standalone snapshot store
    When I list snapshots for decisions with missing entries
    Then the dsvc standalone plan snapshots should have 0 entries

  # --- Persisted-mode (database-backed) ---

  Scenario: Persisted record and get decision round-trips through database
    Given a persisted decision service
    When I record a prompt_definition decision for plan "P1" with question "Persisted root"
    And I get the decision by its ID
    Then the dsvc retrieved decision should match the recorded decision

  Scenario: Persisted get non-existent decision raises DecisionNotFoundError
    Given a persisted decision service
    When I try to get decision "01NONEXISTENT00000000000000"
    Then a dsvc decision not found error should be raised

  Scenario: Persisted list decisions returns ordered results from database
    Given a persisted decision service
    When I record a prompt_definition decision for plan "P1" with question "First persisted"
    And I record a strategy_choice decision for plan "P1" with question "Second persisted"
    And I list decisions for plan "P1"
    Then the dsvc decision list should have 2 entries
    And the dsvc decision list should be ordered by sequence number

  Scenario: Persisted list by type filters through database
    Given a persisted decision service
    When I record a prompt_definition decision for plan "P1" with question "Root"
    And I record a strategy_choice decision for plan "P1" with question "Strategy"
    And I record a strategy_choice decision for plan "P1" with question "Strategy 2"
    And I filter decisions for plan "P1" by type "strategy_choice"
    Then the dsvc decision list should have 2 entries

  Scenario: Persisted get path to root navigates through database
    Given a persisted decision service
    When I record a prompt_definition decision for plan "P1" with question "Root"
    And I record a strategy_choice decision for plan "P1" with parent as child "Level 1"
    And I record an implementation_choice decision for plan "P1" with second parent as child "Level 2"
    And I get the path to root from the last decision
    Then the dsvc path should have 3 decisions
    And the dsvc last path decision should be the root

  Scenario: Persisted get superseded returns superseded decisions from database
    Given a persisted decision service
    When I record a prompt_definition decision for plan "P1" with question "Original"
    And I record a strategy_choice decision for plan "P1" with question "Replacement"
    And I mark the first decision as superseded by the second
    And I get superseded decisions for plan "P1"
    Then the dsvc superseded list should have 1 entry

  Scenario: Persisted mark superseded updates database and cache
    Given a persisted decision service
    When I record a prompt_definition decision for plan "P1" with question "Old"
    And I record a strategy_choice decision for plan "P1" with question "New"
    And I mark the first decision as superseded by the second
    Then the dsvc first decision should be superseded
    And the dsvc first decision superseded_by should match the second decision

  Scenario: Persisted delete removes decision from database
    Given a persisted decision service
    When I record a prompt_definition decision for plan "P1" with question "Ephemeral"
    And I delete the recorded decision
    Then the dsvc decision should be deleted
    And the dsvc plan "P1" should have 0 decisions

  Scenario: Persisted duplicate decision raises DuplicateDecisionError
    Given a persisted decision service
    When I record a prompt_definition decision for plan "P1" with question "Original"
    And I try to store a duplicate of the recorded decision
    Then a dsvc duplicate decision error should be raised

  Scenario: Persisted sequence resumes after service restart
    Given a persisted decision service
    When I record a prompt_definition decision for plan "P1" with question "Before restart"
    And I record a strategy_choice decision for plan "P1" with question "Also before restart"
    Then the dsvc decisions should have sequence numbers 0 1
    When I recreate the decision service with the same database
    And I record a strategy_choice decision for plan "P1" with question "After restart"
    Then the dsvc decision sequence number should be 2
    And the dsvc next sequence for plan "P1" should be 3

  # --- Full context snapshot (issue #9056) ---

  Scenario: Strategize phase records decisions with full context snapshots
    Given a plan lifecycle service with decision service wired
    And an action "local/test-action" for strategize snapshot test
    And a plan created from "local/test-action" with project "proj-snapshot"
    When I start strategize for the snapshot test plan
    Then the strategize decision should have a non-empty hot_context_hash
    And the strategize decision should have a non-empty hot_context_ref
    And the strategize decision hot_context_ref should start with "plan:"
    And the strategize decision should have a non-empty actor_state_ref
    And the strategize decision should have relevant_resources populated

  Scenario: Strategize context snapshot hash is content-addressable
    Given a plan lifecycle service with decision service wired
    And an action "local/test-action-hash" for strategize snapshot test
    And a plan created from "local/test-action-hash" with project "proj-hash"
    When I start strategize for the snapshot test plan
    Then the strategize decision hot_context_hash should start with "sha256:"
    And the strategize decision hot_context_hash should be 71 characters long

  Scenario: Strategize context snapshot without projects has empty relevant_resources
    Given a plan lifecycle service with decision service wired
    And an action "local/no-project-action" for strategize snapshot test
    And a plan created from "local/no-project-action" without projects
    When I start strategize for the snapshot test plan
    Then the strategize decision should have a non-empty hot_context_hash
    And the strategize decision should have empty relevant_resources
