@phase1 @e2e_helpers @wf04
Feature: WF04 Snapshot Helper Utilities
  As a developer maintaining the WF04 E2E test infrastructure
  I want unit tests for the snapshot helper's utility functions
  So that timestamp normalisation, enum serialisation, decision counting,
  and snapshot building are verified without running a full E2E test

  # --- _iso() timestamp normalisation ---

  @iso_timestamp
  Scenario: _iso returns empty string for None
    Given a None timestamp value
    When _iso is called
    Then the iso result should be an empty string

  @iso_timestamp
  Scenario: _iso returns empty string for non-datetime truthy value
    Given a non-datetime truthy value "not-a-date"
    When _iso is called
    Then the iso result should be an empty string

  @iso_timestamp
  Scenario: _iso normalises naive datetime to UTC
    Given a naive datetime 2026-03-15T10:30:00
    When _iso is called
    Then the iso result should be exactly "2026-03-15T10:30:00+00:00"

  @iso_timestamp
  Scenario: _iso normalises aware datetime to UTC
    Given an aware datetime 2026-03-15T10:30:00 in timezone UTC+05:00
    When _iso is called
    Then the iso result should be exactly "2026-03-15T05:30:00+00:00"

  @iso_timestamp
  Scenario: _iso produces consistent format for mixed timezone inputs
    Given a naive datetime 2026-03-15T12:00:00
    And an aware datetime 2026-03-15T12:00:00 in timezone UTC
    When _iso is called on both
    Then both iso results should be identical

  # --- _enum_value() serialisation ---

  @enum_value
  Scenario: _enum_value returns .value for enum-like objects
    Given an enum-like object with value "completed"
    When _enum_value is called
    Then the enum result should be "completed"

  @enum_value
  Scenario: _enum_value returns str for plain objects
    Given a plain string "hello"
    When _enum_value is called
    Then the enum result should be "hello"

  # --- count_decision_nodes() ---

  @decision_count
  Scenario: count_decision_nodes returns 0 for empty input
    Given an empty list tree
    When count_decision_nodes is called
    Then the decision node count should be 0

  @decision_count
  Scenario: count_decision_nodes counts single root node
    Given a tree with one decision node
    When count_decision_nodes is called
    Then the decision node count should be 1

  @decision_count
  Scenario: count_decision_nodes counts nested children
    Given a tree with a root and 2 child decision nodes
    When count_decision_nodes is called
    Then the decision node count should be 3

  @decision_count
  Scenario: count_decision_nodes skips nodes without decision_id
    Given a tree with mixed decision and non-decision nodes
    When count_decision_nodes is called
    Then the decision node count should be 2

  @decision_count
  Scenario: count_decision_nodes handles list input
    Given a list of 2 independent decision trees
    When count_decision_nodes is called
    Then the decision node count should be 2

  @decision_count
  Scenario: count_decision_nodes truncates at max_depth
    Given a tree with depth 5 and one decision node per level
    When count_decision_nodes is called with max_depth 3
    Then the decision node count should be 3

  # --- _build_snapshot() with mocked lifecycle service ---

  @snapshot_build
  Scenario: _build_snapshot raises ValueError for missing plan
    Given a mocked lifecycle service that returns None for plan lookup
    When _build_snapshot is called with plan_id "nonexistent"
    Then the snapshot error should be ValueError containing "not found"

  @snapshot_build
  Scenario: _build_snapshot returns correct structure for plan without subplans
    Given a mocked lifecycle service with a plan that has no subplans
    When _build_snapshot is called
    Then the snapshot should have subplan_count 0
    And the snapshot subplans list should be empty
    And the snapshot plan_id should match the mocked plan ID
    And the snapshot project_scopes should be an empty list
    And the snapshot validation_summary should be None

  @snapshot_build
  Scenario: _build_snapshot maps resources to projects for subplans
    Given a mocked lifecycle service with a plan that has 2 subplans and project scopes
    When _build_snapshot is called
    Then the snapshot should have subplan_count 2
    And subplan "SUB01" should map to project "proj-a"
    And subplan "SUB02" should map to project "proj-b"
    And each subplan should have mapped_projects populated
    And each subplan should have an unmapped_resources field
    And subplan "SUB01" status should be "completed"
    And subplan "SUB01" child_phase should be "completed"
    And subplan "SUB01" started_at should be non-empty
    And subplan "SUB01" child_validation_summary required_passed should be 1

  @snapshot_build
  Scenario: _build_snapshot includes unmapped_resources for unknown resource IDs
    Given a mocked lifecycle service with a subplan targeting an unmapped resource
    When _build_snapshot is called
    Then the subplan unmapped_resources should contain the unknown resource ID

  @snapshot_build
  Scenario: _build_snapshot defaults child fields when child plan is None
    Given a mocked lifecycle service with a subplan whose child plan is None
    When _build_snapshot is called
    Then the subplan child_phase should be an empty string
    And the subplan child_state should be an empty string
    And the subplan child_updated_at should be an empty string
    And the subplan execute_started_at should be an empty string
    And the subplan execute_completed_at should be an empty string
    And the subplan apply_started_at should be an empty string
    And the subplan applied_at should be an empty string
