Feature: Output Rendering Framework
  The output rendering framework decouples command output data from visual
  presentation through a session/handle/materialiser pipeline.

  # --- OutputSession lifecycle ---

  Scenario: OutputSession opens and closes cleanly
    Given an OutputSession with format "plain"
    When the session is closed
    Then the session state is "closed"

  Scenario: OutputSession works as a context manager
    When I use an OutputSession context manager with format "plain"
    Then the session was closed automatically

  Scenario: OutputSession context manager sets exit_code on exception
    When I use an OutputSession context manager that raises an exception
    Then the session exit code is 1

  # --- PanelHandle ---

  Scenario: PanelHandle set_entry adds key-value pairs
    Given an OutputSession with format "plain"
    When I create a panel handle with title "Details"
    And I set entry "Name" to "test-project"
    And I set entry "Status" to "active"
    And I close the panel handle
    Then the panel has 2 entries
    And the panel entry "Name" has value "test-project"

  Scenario: PanelHandle set_entry updates existing key
    Given an OutputSession with format "plain"
    When I create a panel handle with title "Details"
    And I set entry "Name" to "old-value"
    And I set entry "Name" to "new-value"
    Then the panel entry "Name" has value "new-value"
    And the panel has 1 entries

  Scenario: PanelHandle set_entries batch update
    Given an OutputSession with format "plain"
    When I create a panel handle with title "Details"
    And I batch set entries with keys "A,B,C" and values "1,2,3"
    Then the panel has 3 entries

  Scenario: PanelHandle remove_entry removes a key
    Given an OutputSession with format "plain"
    When I create a panel handle with title "Details"
    And I set entry "Name" to "value"
    And I set entry "Extra" to "remove-me"
    And I remove entry "Extra"
    Then the panel has 1 entries

  Scenario: PanelHandle add_line convenience alias
    Given an OutputSession with format "plain"
    When I create a panel handle with title "Details"
    And I add line "Key" with value "Val"
    Then the panel entry "Key" has value "Val"

  Scenario: Writing to a closed PanelHandle raises ElementClosedError
    Given an OutputSession with format "plain"
    When I create a panel handle with title "Details"
    And I close the panel handle
    Then setting entry "X" to "Y" raises ElementClosedError

  # --- TableHandle ---

  Scenario: TableHandle add_row with dict
    Given an OutputSession with format "plain"
    When I create a table handle with columns "Name,Status"
    And I add a dict row with Name "foo" and Status "active"
    Then the table has 1 rows

  Scenario: TableHandle add_row with list
    Given an OutputSession with format "plain"
    When I create a table handle with columns "Name,Status"
    And I add a list row with values "bar,inactive"
    Then the table has 1 rows

  Scenario: TableHandle add_rows batch
    Given an OutputSession with format "plain"
    When I create a table handle with columns "Name,Status"
    And I add 3 batch rows
    Then the table has 3 rows

  Scenario: TableHandle set_summary
    Given an OutputSession with format "plain"
    When I create a table handle with columns "Name,Count"
    And I add a dict row with Name "a" and Status "1"
    And I set summary with total "1"
    Then the table summary is set

  Scenario: TableHandle set_sort_key
    Given an OutputSession with format "plain"
    When I create a table handle with columns "Name,Status"
    And I set sort key to "Name"
    Then the table sort key is "Name"

  Scenario: Writing to a closed TableHandle raises ElementClosedError
    Given an OutputSession with format "plain"
    When I create a table handle with columns "Name,Status"
    And I close the table handle
    Then adding a row raises ElementClosedError

  # --- StatusHandle ---

  Scenario: StatusHandle creation and update
    Given an OutputSession with format "plain"
    When I create a status handle with message "Working..."
    And I update the status message to "Done"
    And I update the status level to "ok"
    And I set the status detail to "All tasks complete"
    Then the status message is "Done"
    And the status level is "ok"
    And the status detail is "All tasks complete"

  Scenario: StatusHandle rejects invalid level
    Given an OutputSession with format "plain"
    When I create a status handle with message "Test"
    Then setting status level to "invalid" raises ValueError

  # --- ProgressHandle ---

  Scenario: ProgressHandle set_progress updates current and total
    Given an OutputSession with format "plain"
    When I create a progress handle labelled "Uploading"
    And I set progress to 50 of 100
    Then the progress current is 50
    And the progress total is 100

  Scenario: ProgressHandle tick increments by 1
    Given an OutputSession with format "plain"
    When I create a progress handle labelled "Processing"
    And I tick the progress 3 times
    Then the progress current is 3

  Scenario: ProgressHandle increment by delta
    Given an OutputSession with format "plain"
    When I create a progress handle labelled "Processing"
    And I increment progress by 5
    Then the progress current is 5

  Scenario: ProgressHandle set_label changes label
    Given an OutputSession with format "plain"
    When I create a progress handle labelled "Old Label"
    And I set progress label to "New Label"
    Then the progress label is "New Label"

  Scenario: ProgressHandle set_indeterminate enables spinner mode
    Given an OutputSession with format "plain"
    When I create a progress handle labelled "Loading"
    And I set progress to indeterminate mode
    Then the progress is indeterminate

  Scenario: ProgressHandle set_step_status tracks steps
    Given an OutputSession with format "plain"
    When I create a progress handle labelled "Pipeline" with steps "A,B,C"
    And I set step "A" status to "done"
    And I set step "B" status to "active"
    Then step "A" has status "done"
    And step "B" has status "active"
    And step "C" has status "pending"

  Scenario: ProgressHandle rejects negative current
    Given an OutputSession with format "plain"
    When I create a progress handle labelled "Test"
    Then setting progress to -1 raises ValueError

  Scenario: ProgressHandle rejects invalid step status
    Given an OutputSession with format "plain"
    When I create a progress handle labelled "Test"
    Then setting step "X" to status "bogus" raises ValueError

  # --- PlainMaterializer ---

  Scenario: PlainMaterializer renders panel as plain ASCII
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a panel handle with title "Info"
    And I set entry "Name" to "test"
    And I set entry "Value" to "42"
    And I close the panel handle
    And the session is closed
    Then the plain output contains "Info"
    And the plain output contains "Name: test"
    And the plain output contains "Value: 42"
    And the plain output does not contain ANSI codes

  Scenario: PlainMaterializer renders table as plain ASCII
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a table handle with columns "Name,Status"
    And I add a dict row with Name "foo" and Status "active"
    And I close the table handle
    And the session is closed
    Then the plain output contains "Name"
    And the plain output contains "foo"
    And the plain output does not contain ANSI codes

  Scenario: PlainMaterializer renders status as plain ASCII
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a status handle with message "All good"
    And I update the status level to "ok"
    And I close the status handle
    And the session is closed
    Then the plain output contains "[OK]"
    And the plain output contains "All good"

  Scenario: PlainMaterializer renders progress as plain ASCII
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a progress handle labelled "Downloading" with total 100
    And I set progress to 75 of 100
    And I close the progress handle
    And the session is closed
    Then the plain output contains "Downloading"
    And the plain output contains "75/100"

  # --- ColorMaterializer ---

  Scenario: ColorMaterializer renders panel with ANSI codes
    Given an OutputSession with format "color" using ColorMaterializer
    When I create a panel handle with title "Coloured"
    And I set entry "Key" to "val"
    And I close the panel handle
    And the session is closed
    Then the color output contains ANSI codes
    And the color output contains "Coloured"

  Scenario: ColorMaterializer renders status with ANSI codes
    Given an OutputSession with format "color" using ColorMaterializer
    When I create a status handle with message "Warning text"
    And I update the status level to "warn"
    And I close the status handle
    And the session is closed
    Then the color output contains "[WARN]"
    And the color output contains ANSI codes

  # --- TableMaterializer ---

  Scenario: TableMaterializer renders table with box-drawing
    Given an OutputSession with format "table" using TableMaterializer
    When I create a table handle with columns "Name,Status"
    And I add a dict row with Name "svc" and Status "up"
    And I close the table handle
    And the session is closed
    Then the table output contains box-drawing characters
    And the table output contains "svc"

  # --- RichMaterializer ---

  Scenario: RichMaterializer renders output
    Given an OutputSession with format "rich" using RichMaterializer
    When I create a panel handle with title "Rich Panel"
    And I set entry "Hello" to "World"
    And I close the panel handle
    And the session is closed
    Then the rich output contains "Rich Panel"

  # --- JsonMaterializer ---

  Scenario: JsonMaterializer serialises all elements as JSON
    Given an OutputSession with format "json" using JsonMaterializer
    When I create a panel handle with title "JSON Panel"
    And I set entry "Key1" to "Val1"
    And I close the panel handle
    And the session is closed
    Then the json output is valid JSON
    And the json output contains element type "panel"
    And the json output contains "Key1"

  Scenario: JsonMaterializer includes tables in output
    Given an OutputSession with format "json" using JsonMaterializer
    When I create a table handle with columns "Col1,Col2"
    And I add a dict row with Name "Col1" and Status "Val1"
    And I close the table handle
    And the session is closed
    Then the json output is valid JSON
    And the json output contains element type "table"

  # --- YamlMaterializer ---

  Scenario: YamlMaterializer serialises all elements as YAML
    Given an OutputSession with format "yaml" using YamlMaterializer
    When I create a panel handle with title "YAML Panel"
    And I set entry "Alpha" to "Beta"
    And I close the panel handle
    And the session is closed
    Then the yaml output is valid YAML
    And the yaml output contains "Alpha"

  # --- Error envelope ---

  Scenario: JsonMaterializer error envelope
    Given a JsonMaterializer
    When I generate an error envelope with code "NOT_FOUND" and message "Resource not found"
    Then the json error output contains "NOT_FOUND"
    And the json error output contains "Resource not found"

  Scenario: YamlMaterializer error envelope
    Given a YamlMaterializer
    When I generate a yaml error envelope with code "FORBIDDEN" and message "Access denied"
    Then the yaml error output contains "FORBIDDEN"
    And the yaml error output contains "Access denied"

  # --- Concurrent producers ---

  Scenario: Concurrent producers do not interleave in plain mode
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create two panels concurrently and close them out of order
    And the session is closed
    Then the plain output has panel A before panel B

  # --- Format fallback ---

  Scenario: Rich format falls back to plain when no TTY
    Given a non-TTY terminal environment
    When I select a materializer for format "rich"
    Then the selected materializer is PlainMaterializer

  Scenario: Color format falls back to plain when no ANSI support
    Given a terminal without ANSI support
    When I select a materializer for format "color"
    Then the selected materializer is PlainMaterializer

  Scenario: Plain format always works
    Given a non-TTY terminal environment
    When I select a materializer for format "plain"
    Then the selected materializer is PlainMaterializer

  Scenario: JSON format ignores terminal capabilities
    Given a non-TTY terminal environment
    When I select a materializer for format "json"
    Then the selected materializer is JsonMaterializer

  Scenario: Invalid format raises ValueError
    When I attempt to select a materializer for format "invalid"
    Then a ValueError is raised

  # --- Backward compatibility ---

  Scenario: format_output still works with plain format
    When I call format_output with a dict and format "plain"
    Then the output contains key-value pairs

  Scenario: format_output still works with json format
    When I call format_output with a dict and format "json"
    Then the output is valid JSON

  Scenario: format_output_session renders via OutputSession
    When I call format_output_session with a dict and format "plain"
    Then the session output contains the dict keys

  # --- Handle context manager ---

  Scenario: Element handle works as context manager
    Given an OutputSession with format "plain"
    When I use a panel handle as a context manager
    Then the handle is closed after the context exits

  # --- Snapshot ---

  Scenario: Session snapshot captures all elements
    Given an OutputSession with format "plain"
    When I create a panel handle with title "P1"
    And I set entry "K" to "V"
    And I create a table handle with columns "A,B"
    And I add a dict row with Name "A" and Status "B"
    Then the session snapshot has 2 elements

  # --- Materializer selection with explicit flag ---

  Scenario: Explicit format overrides fallback
    Given a non-TTY terminal environment
    When I select a materializer for format "rich" with explicit flag
    Then the selected materializer is RichMaterializer

  # ================================================================
  # Coverage boost: handle validation guards
  # ================================================================

  Scenario: PanelHandle rejects empty key in set_entry
    Given an OutputSession with format "plain"
    When I create a panel handle with title "V"
    Then setting entry with empty key raises ValueError

  Scenario: PanelHandle rejects None entries in set_entries
    Given an OutputSession with format "plain"
    When I create a panel handle with title "V"
    Then batch setting None entries raises ValueError

  Scenario: PanelHandle rejects empty key in remove_entry
    Given an OutputSession with format "plain"
    When I create a panel handle with title "V"
    Then removing entry with empty key raises ValueError

  Scenario: TableHandle rejects None row in add_row
    Given an OutputSession with format "plain"
    When I create a table handle with columns "A,B"
    Then adding a None row raises ValueError

  Scenario: TableHandle rejects None rows in add_rows
    Given an OutputSession with format "plain"
    When I create a table handle with columns "A,B"
    Then adding None rows raises ValueError

  Scenario: TableHandle rejects None summary
    Given an OutputSession with format "plain"
    When I create a table handle with columns "A,B"
    Then setting None summary raises ValueError

  Scenario: TableHandle rejects empty sort key
    Given an OutputSession with format "plain"
    When I create a table handle with columns "A,B"
    Then setting empty sort key raises ValueError

  Scenario: StatusHandle rejects empty message
    Given an OutputSession with format "plain"
    When I create a status handle with message "ok"
    Then setting empty status message raises ValueError

  Scenario: StatusHandle rejects empty level
    Given an OutputSession with format "plain"
    When I create a status handle with message "ok"
    Then setting empty status level raises ValueError

  Scenario: ProgressHandle rejects empty label
    Given an OutputSession with format "plain"
    When I create a progress handle labelled "Loading"
    Then setting empty progress label raises ValueError

  Scenario: ProgressHandle rejects negative delta
    Given an OutputSession with format "plain"
    When I create a progress handle labelled "Loading" with total 10
    Then incrementing by negative delta raises ValueError

  Scenario: ProgressHandle rejects empty step label
    Given an OutputSession with format "plain"
    When I create a progress handle labelled "Loading" with steps "A,B"
    Then setting step status with empty label raises ValueError

  # ================================================================
  # Coverage boost: handle data paths
  # ================================================================

  Scenario: PanelHandle set_entries updates existing entry
    Given an OutputSession with format "plain"
    When I create a panel handle with title "UP"
    And I set entry "K" to "old"
    And I batch set entries with keys "K" and values "new"
    Then the panel entry "K" has value "new"

  Scenario: TableHandle add_rows with list-type rows
    Given an OutputSession with format "plain"
    When I create a table handle with columns "A,B"
    And I batch add list rows with values "x,y" and "m,n"
    Then the table has 2 rows

  Scenario: ProgressHandle increment adds to current
    Given an OutputSession with format "plain"
    When I create a progress handle labelled "Down" with total 100
    And I set progress to 10 of 100
    And I increment progress by 5
    Then the progress current is 15

  Scenario: ProgressHandle set_step_status creates steps list
    Given an OutputSession with format "plain"
    When I create a progress handle labelled "Steps"
    And I set step "Alpha" status to "done"
    Then step "Alpha" has status "done"

  # ================================================================
  # Coverage boost: color materializer rendering
  # ================================================================

  Scenario: Color materializer renders table with ANSI codes
    Given an OutputSession with format "color" using ColorMaterializer
    When I create a table handle with columns "Name,Status"
    And I add a dict row with Name "foo" and Status "ok"
    And I close the table handle
    And the session is closed
    Then the color output contains ANSI codes
    And the color output contains "foo"

  Scenario: Color materializer renders empty table
    Given an OutputSession with format "color" using ColorMaterializer
    When I create a table with no columns
    And I close the table handle
    And the session is closed
    Then the color output contains "empty table"

  Scenario: Color materializer renders status with detail
    Given an OutputSession with format "color" using ColorMaterializer
    When I create a status handle with message "Building"
    And I set the status detail to "step 3 of 10"
    And I close the status handle
    And the session is closed
    Then the color output contains ANSI codes
    And the color output contains "Building"

  Scenario: Color materializer renders progress indicator
    Given an OutputSession with format "color" using ColorMaterializer
    When I create a progress handle labelled "Downloading" with total 50
    And I set progress to 25 of 50
    And I close the progress handle
    And the session is closed
    Then the color output contains ANSI codes
    And the color output contains "Downloading"

  Scenario: Color materializer renders panel with style hints
    Given an OutputSession with format "color" using ColorMaterializer
    When I create a panel handle with title "Results"
    And I set entry with key "passed" value "yes" and style hint "success"
    And I set entry with key "warnings" value "3" and style hint "warning"
    And I set entry with key "errors" value "1" and style hint "error"
    And I close the panel handle
    And the session is closed
    Then the color output contains ANSI codes
    And the color output contains "passed"

  # ================================================================
  # Coverage boost: plain materializer edge cases
  # ================================================================

  Scenario: Plain materializer renders empty table
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a table with no columns
    And I close the table handle
    And the session is closed
    Then the plain output contains "empty table"

  Scenario: Plain materializer renders table with title and summary
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a table handle with columns "Name,Count"
    And I set table title to "Report"
    And I add a dict row with Name "A" and Status "10"
    And I set summary with total "10"
    And I close the table handle
    And the session is closed
    Then the plain output contains "Report"

  Scenario: Plain materializer renders status with detail
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a status handle with message "Processing"
    And I set the status detail to "file 3 of 10"
    And I close the status handle
    And the session is closed
    Then the plain output contains "Processing"
    And the plain output contains "file 3 of 10"

  Scenario: Plain materializer renders indeterminate progress
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a progress handle labelled "Scanning"
    And I set progress to indeterminate mode
    And I close the progress handle
    And the session is closed
    Then the plain output contains "Scanning"

  Scenario: Plain materializer renders current-only progress
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a progress handle labelled "Items"
    And I set progress current to 42
    And I close the progress handle
    And the session is closed
    Then the plain output contains "Items"
    And the plain output contains "42"

  Scenario: Plain materializer renders progress with steps
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a progress handle labelled "Deploy" with steps "Build,Test,Ship"
    And I set step "Build" status to "done"
    And I set step "Test" status to "active"
    And I close the progress handle
    And the session is closed
    Then the plain output contains "Build"
    And the plain output contains "Test"

  # ================================================================
  # Coverage boost: serialization helpers (JSON/YAML)
  # ================================================================

  Scenario: JSON materializer serializes status element
    Given an OutputSession with format "json" using JsonMaterializer
    When I create a status handle with message "Done"
    And I set the status detail to "All passed"
    And I close the status handle
    And the session is closed
    Then the json output is valid JSON
    And the json output contains element type "status"
    And the json output contains "Done"

  Scenario: JSON materializer serializes progress with steps
    Given an OutputSession with format "json" using JsonMaterializer
    When I create a progress handle labelled "Pipeline" with steps "A,B"
    And I set step "A" status to "done"
    And I close the progress handle
    And the session is closed
    Then the json output is valid JSON

  Scenario: JSON materializer serializes panel with style hint and icon
    Given an OutputSession with format "json" using JsonMaterializer
    When I create a panel handle with title "Info"
    And I set entry with key "status" value "ok" and style hint "success"
    And I set entry with icon key "icon_entry" value "check" and icon "check_mark"
    And I close the panel handle
    And the session is closed
    Then the json output is valid JSON
    And the json output contains "style_hint"
    And the json output contains "icon"

  Scenario: JSON error envelope includes details
    Given a JsonMaterializer
    When I generate an error envelope with code "ERR" message "fail" and details "traceback"
    Then the json error output contains "traceback"

  # ================================================================
  # Coverage boost: box-drawing materializer edge cases
  # ================================================================

  Scenario: Box-drawing materializer renders empty table
    Given an OutputSession with format "table" using TableMaterializer
    When I create a table with no columns
    And I close the table handle
    And the session is closed
    Then the strategy output contains "empty table"

  Scenario: Box-drawing materializer renders table with title
    Given an OutputSession with format "table" using TableMaterializer
    When I create a table handle with columns "X,Y"
    And I set table title to "Summary"
    And I add a dict row with Name "X" and Status "1"
    And I close the table handle
    And the session is closed
    Then the strategy output contains "Summary"

  # ================================================================
  # Coverage boost: format selection paths
  # ================================================================

  Scenario: Select yaml materializer
    When I select a materializer for format "yaml"
    Then the selected materializer is YamlMaterializer

  Scenario: Select materializer rejects empty format
    When I attempt to select a materializer with empty format
    Then a ValueError is raised

  Scenario: Rich format falls back to table when no cursor support but TTY
    Given a terminal with ANSI but no cursor support
    When I select a materializer for format "rich"
    Then the selected materializer is TableMaterializer

  Scenario: Color format uses color materializer with ANSI support
    Given a terminal with ANSI support
    When I select a materializer for format "color"
    Then the selected materializer is ColorMaterializer

  Scenario: Table format uses table materializer on TTY
    Given a TTY terminal with ANSI support
    When I select a materializer for format "table"
    Then the selected materializer is TableMaterializer

  Scenario: Table format falls back to plain on non-TTY
    Given a non-TTY terminal environment
    When I select a materializer for format "table"
    Then the selected materializer is PlainMaterializer

  # ================================================================
  # Coverage boost: session edge cases
  # ================================================================

  Scenario: Session rejects creating panel with empty title
    Given an OutputSession with format "plain"
    Then creating a panel with empty title raises ValueError

  Scenario: Session rejects creating status with empty message
    Given an OutputSession with format "plain"
    Then creating a status with empty message raises ValueError

  Scenario: Session rejects creating progress with empty label
    Given an OutputSession with format "plain"
    Then creating a progress with empty label raises ValueError

  Scenario: Closed session rejects new handles
    Given an OutputSession with format "plain"
    When the session is closed
    Then creating a panel on closed session raises RuntimeError

  Scenario: Session close force-closes open handles
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a panel handle with title "Unclosed"
    And I set entry "A" to "B"
    And the session is closed
    Then the plain output contains "Unclosed"

  # ================================================================
  # Coverage boost: ElementHandle validation guards
  # ================================================================

  Scenario: ElementHandle rejects empty handle_id
    Given an OutputSession with format "plain"
    Then constructing a handle with empty handle_id raises ValueError

  Scenario: ElementHandle rejects empty element_type
    Given an OutputSession with format "plain"
    Then constructing a handle with empty element_type raises ValueError

  Scenario: ElementHandle rejects negative declaration_index
    Given an OutputSession with format "plain"
    Then constructing a handle with negative index raises ValueError

  Scenario: ElementHandle rejects None session
    Given an OutputSession with format "plain"
    Then constructing a handle with None session raises ValueError

  Scenario: ElementHandle rejects None element
    Given an OutputSession with format "plain"
    Then constructing a handle with None element raises ValueError

  # ================================================================
  # Coverage boost: handle close and __exit__ edge cases
  # ================================================================

  Scenario: Closing an already-closed handle is a no-op
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a panel handle with title "CloseMe"
    And I close the panel handle
    And I close the panel handle again
    Then the handle state is "closed"

  Scenario: Handle used as context manager auto-closes
    Given an OutputSession with format "plain" using PlainMaterializer
    When I use a panel handle as context manager with title "AutoClose"
    Then the handle state is "closed"
    And the plain output contains "AutoClose"

  # ================================================================
  # Coverage boost: table rows as list (not dict)
  # ================================================================

  Scenario: TableHandle accepts list rows
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a table handle with columns "A,B"
    And I add a list row with values "x,y"
    And I close the table handle
    And the session is closed
    Then the plain output contains "x"

  Scenario: TableHandle batch add accepts list rows
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a table handle with columns "A,B"
    And I batch add list rows with values "1,2" and "3,4"
    And I close the table handle
    And the session is closed
    Then the plain output contains "1"

  # ================================================================
  # Coverage boost: color materializer table with title
  # ================================================================

  Scenario: Color materializer renders table title
    Given an OutputSession with format "color" using ColorMaterializer
    When I create a table handle with columns "Col1,Col2"
    And I set table title to "Colored Title"
    And I add a dict row with Name "Col1" and Status "val"
    And I close the table handle
    And the session is closed
    Then the color output contains "Colored Title"

  # ================================================================
  # Coverage boost: box-draw table dispatch for non-table
  # ================================================================

  Scenario: Table materializer renders panel as plain text
    Given an OutputSession with format "table" using TableMaterializer
    When I create a panel handle with title "PanelInTable"
    And I set entry "info" to "details"
    And I close the panel handle
    And the session is closed
    Then the strategy output contains "PanelInTable"

  # ================================================================
  # Coverage boost: session creates table with ColumnDef objects
  # ================================================================

  Scenario: Session table accepts ColumnDef objects
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a table with ColumnDef objects "Alpha,Beta"
    And I add a dict row with Name "Alpha" and Status "val1"
    And I close the table handle
    And the session is closed
    Then the plain output contains "Alpha"

  # ================================================================
  # Coverage boost: explicit format flag
  # ================================================================

  Scenario: Explicit format flag creates strategy without fallback
    When I select a materializer for format "rich" with explicit flag
    Then the selected materializer is RichMaterializer

  # ================================================================
  # Coverage boost: serializer on_session_end with buffered content
  # ================================================================

  Scenario: Plain materializer flushes remaining buffers on session end
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a panel handle with title "First"
    And I create a second panel handle with title "Second"
    And I set entry "a" to "1" on handle 2
    And I close handle 2
    And I set entry "b" to "2"
    And I close the panel handle
    And the session is closed
    Then the plain output contains "First"
    And the plain output contains "Second"

  # ================================================================
  # Coverage boost: serializer get_output with None session
  # ================================================================

  Scenario: JSON serializer returns empty when session is None
    Given a JsonMaterializer with no session
    Then the serializer get_output returns empty string

  # ================================================================
  # TreeHandle
  # ================================================================

  Scenario: TreeHandle creates a tree with root label
    Given an OutputSession with format "plain"
    When I create a tree handle with root label "Root"
    Then the tree root label is "Root"

  Scenario: TreeHandle add_child adds children to root
    Given an OutputSession with format "plain"
    When I create a tree handle with root label "Project"
    And I add a tree child "src" under "Project"
    And I add a tree child "tests" under "Project"
    Then the tree root has 2 children

  Scenario: TreeHandle add_child adds nested children
    Given an OutputSession with format "plain"
    When I create a tree handle with root label "Root"
    And I add a tree child "child" under "Root"
    And I add a tree child "grandchild" under "Root/child"
    Then the tree node "Root/child" has 1 children

  Scenario: TreeHandle set_node_style updates style hint
    Given an OutputSession with format "plain"
    When I create a tree handle with root label "Root"
    And I add a tree child "item" under "Root"
    And I set tree node style "Root/item" to "success"
    Then the tree node "Root/item" has style "success"

  Scenario: TreeHandle rejects empty label
    Given an OutputSession with format "plain"
    When I create a tree handle with root label "Root"
    Then adding a tree child with empty label raises ValueError

  Scenario: TreeHandle rejects invalid parent path
    Given an OutputSession with format "plain"
    When I create a tree handle with root label "Root"
    Then adding a tree child under invalid path raises ValueError

  Scenario: TreeHandle rejects empty path in set_node_style
    Given an OutputSession with format "plain"
    When I create a tree handle with root label "Root"
    Then setting tree node style with empty path raises ValueError

  Scenario: Writing to closed TreeHandle raises ElementClosedError
    Given an OutputSession with format "plain"
    When I create a tree handle with root label "Root"
    And I close the tree handle
    Then adding a tree child on closed handle raises ElementClosedError

  Scenario: Session rejects tree with empty root label
    Given an OutputSession with format "plain"
    Then creating a tree with empty root label raises ValueError

  # ================================================================
  # TextHandle
  # ================================================================

  Scenario: TextHandle creates a text block
    Given an OutputSession with format "plain"
    When I create a text handle with content "Hello world"
    Then the text content is "Hello world"

  Scenario: TextHandle append adds text
    Given an OutputSession with format "plain"
    When I create a text handle with content "Hello"
    And I append text " world"
    Then the text content is "Hello world"

  Scenario: TextHandle set_content replaces text
    Given an OutputSession with format "plain"
    When I create a text handle with content "old"
    And I set text content to "new"
    Then the text content is "new"

  Scenario: TextHandle rejects None in append
    Given an OutputSession with format "plain"
    When I create a text handle with content "x"
    Then appending None text raises ValueError

  Scenario: TextHandle rejects None in set_content
    Given an OutputSession with format "plain"
    When I create a text handle with content "x"
    Then setting None text content raises ValueError

  Scenario: Writing to closed TextHandle raises ElementClosedError
    Given an OutputSession with format "plain"
    When I create a text handle with content "x"
    And I close the text handle
    Then appending to closed text handle raises ElementClosedError

  # ================================================================
  # CodeHandle
  # ================================================================

  Scenario: CodeHandle creates a code block
    Given an OutputSession with format "plain"
    When I create a code handle with content "print('hi')"
    Then the code content is "print('hi')"

  Scenario: CodeHandle set_content replaces code
    Given an OutputSession with format "plain"
    When I create a code handle with content "old"
    And I set code content to "new code"
    Then the code content is "new code"

  Scenario: CodeHandle set_language updates language
    Given an OutputSession with format "plain"
    When I create a code handle with content "x = 1" and language "python"
    And I set code language to "javascript"
    Then the code language is "javascript"

  Scenario: CodeHandle set_highlight_lines sets lines
    Given an OutputSession with format "plain"
    When I create a code handle with content "a\nb\nc"
    And I set code highlight lines to "1,3"
    Then the code highlight lines are "1,3"

  Scenario: CodeHandle rejects None content
    Given an OutputSession with format "plain"
    When I create a code handle with content "x"
    Then setting None code content raises ValueError

  Scenario: CodeHandle rejects empty language
    Given an OutputSession with format "plain"
    When I create a code handle with content "x"
    Then setting empty code language raises ValueError

  Scenario: CodeHandle rejects None highlight lines
    Given an OutputSession with format "plain"
    When I create a code handle with content "x"
    Then setting None code highlight lines raises ValueError

  Scenario: Writing to closed CodeHandle raises ElementClosedError
    Given an OutputSession with format "plain"
    When I create a code handle with content "x"
    And I close the code handle
    Then setting code content on closed handle raises ElementClosedError

  # ================================================================
  # DiffHandle
  # ================================================================

  Scenario: DiffHandle creates a diff block
    Given an OutputSession with format "plain"
    When I create a diff handle with file_a "a.py" and file_b "b.py"
    Then the diff file_a is "a.py"
    And the diff file_b is "b.py"

  Scenario: DiffHandle add_hunk adds hunks
    Given an OutputSession with format "plain"
    When I create a diff handle with file_a "a.py" and file_b "b.py"
    And I add a diff hunk with header "@@ -1,3 +1,4 @@" and context line "line1" and add line "new"
    Then the diff has 1 hunks

  Scenario: DiffHandle set_stats sets statistics
    Given an OutputSession with format "plain"
    When I create a diff handle with file_a "a.py" and file_b "b.py"
    And I set diff stats with 5 insertions and 3 deletions
    Then the diff stats insertions is 5
    And the diff stats deletions is 3

  Scenario: DiffHandle rejects empty hunk header
    Given an OutputSession with format "plain"
    When I create a diff handle with file_a "a.py" and file_b "b.py"
    Then adding a diff hunk with empty header raises ValueError

  Scenario: DiffHandle rejects None hunk lines
    Given an OutputSession with format "plain"
    When I create a diff handle with file_a "a.py" and file_b "b.py"
    Then adding a diff hunk with None lines raises ValueError

  Scenario: DiffHandle rejects negative insertions
    Given an OutputSession with format "plain"
    When I create a diff handle with file_a "a.py" and file_b "b.py"
    Then setting diff stats with negative insertions raises ValueError

  Scenario: DiffHandle rejects negative deletions
    Given an OutputSession with format "plain"
    When I create a diff handle with file_a "a.py" and file_b "b.py"
    Then setting diff stats with negative deletions raises ValueError

  Scenario: Writing to closed DiffHandle raises ElementClosedError
    Given an OutputSession with format "plain"
    When I create a diff handle with file_a "a.py" and file_b "b.py"
    And I close the diff handle
    Then adding a diff hunk on closed handle raises ElementClosedError

  # ================================================================
  # SeparatorHandle
  # ================================================================

  Scenario: SeparatorHandle is auto-closed on creation
    Given an OutputSession with format "plain"
    When I create a separator with style "line"
    Then the separator handle is closed

  Scenario: SeparatorHandle accepts blank style
    Given an OutputSession with format "plain"
    When I create a separator with style "blank"
    Then the separator handle is closed

  Scenario: SeparatorHandle accepts double style
    Given an OutputSession with format "plain"
    When I create a separator with style "double"
    Then the separator handle is closed

  Scenario: SeparatorHandle rejects invalid style
    Given an OutputSession with format "plain"
    Then creating a separator with invalid style raises ValueError

  # ================================================================
  # ActionHintHandle
  # ================================================================

  Scenario: ActionHintHandle is auto-closed on creation
    Given an OutputSession with format "plain"
    When I create an action hint with commands "agents list,agents deploy"
    Then the action hint handle is closed

  Scenario: ActionHintHandle stores commands and description
    Given an OutputSession with format "plain"
    When I create an action hint with commands "agents list" and description "Try these"
    Then the action hint has 1 commands
    And the action hint description is "Try these"

  Scenario: ActionHintHandle rejects empty commands
    Given an OutputSession with format "plain"
    Then creating an action hint with empty commands raises ValueError

  # ================================================================
  # Plain materializer rendering: new element types
  # ================================================================

  Scenario: Plain materializer renders tree with guides
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a tree handle with root label "Root"
    And I add a tree child "child1" under "Root"
    And I add a tree child "child2" under "Root"
    And I close the tree handle
    And the session is closed
    Then the plain output contains "Root"
    And the plain output contains "child1"
    And the plain output contains "child2"
    And the plain output does not contain ANSI codes

  Scenario: Plain materializer renders tree without guides
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a tree handle with root label "Flat" and show_guides false
    And I add a tree child "item" under "Flat"
    And I close the tree handle
    And the session is closed
    Then the plain output contains "Flat"
    And the plain output contains "item"

  Scenario: Plain materializer renders tree with max depth hint
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a tree handle with root label "Deep" and max_depth_hint 1
    And I add a tree child "level1" under "Deep"
    And I add a tree child "level2" under "Deep/level1"
    And I close the tree handle
    And the session is closed
    Then the plain output contains "Deep"
    And the plain output contains "level1"
    And the plain output contains "more"

  Scenario: Plain materializer renders text block
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a text handle with content "Hello plain world"
    And I close the text handle
    And the session is closed
    Then the plain output contains "Hello plain world"
    And the plain output does not contain ANSI codes

  Scenario: Plain materializer renders text with indent
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a text handle with content "indented" and indent 4
    And I close the text handle
    And the session is closed
    Then the plain output contains "    indented"

  Scenario: Plain materializer renders code block
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a code handle with content "x = 1" and language "python"
    And I close the code handle
    And the session is closed
    Then the plain output contains "x = 1"
    And the plain output does not contain ANSI codes

  Scenario: Plain materializer renders code with line numbers
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a code handle with content "a\nb\nc" and line_numbers true
    And I close the code handle
    And the session is closed
    Then the plain output contains "1 |"
    And the plain output contains "2 |"

  Scenario: Plain materializer renders diff block
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a diff handle with file_a "old.py" and file_b "new.py"
    And I add a diff hunk with header "@@ -1,2 +1,3 @@" and context line "same" and add line "added"
    And I set diff stats with 1 insertions and 0 deletions
    And I close the diff handle
    And the session is closed
    Then the plain output contains "--- old.py"
    And the plain output contains "+++ new.py"
    And the plain output contains "+added"
    And the plain output contains "1 insertion(s)"
    And the plain output does not contain ANSI codes

  Scenario: Plain materializer renders separator line
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a separator with style "line"
    And the session is closed
    Then the plain output contains "----"

  Scenario: Plain materializer renders separator double
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a separator with style "double"
    And the session is closed
    Then the plain output contains "===="

  Scenario: Plain materializer renders separator blank
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a separator with style "blank"
    And the session is closed
    Then the session completed without error

  Scenario: Plain materializer renders action hint
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create an action hint with commands "agents list,agents deploy" and description "Try these"
    And the session is closed
    Then the plain output contains "Next steps"
    And the plain output contains "$ agents list"
    And the plain output contains "Try these"
    And the plain output does not contain ANSI codes

  # ================================================================
  # Color materializer rendering: new element types
  # ================================================================

  Scenario: Color materializer renders tree with ANSI
    Given an OutputSession with format "color" using ColorMaterializer
    When I create a tree handle with root label "Root"
    And I add a tree child "child1" under "Root"
    And I close the tree handle
    And the session is closed
    Then the color output contains ANSI codes
    And the color output contains "Root"

  Scenario: Color materializer renders text block
    Given an OutputSession with format "color" using ColorMaterializer
    When I create a text handle with content "Color text"
    And I close the text handle
    And the session is closed
    Then the color output contains "Color text"

  Scenario: Color materializer renders code with language header
    Given an OutputSession with format "color" using ColorMaterializer
    When I create a code handle with content "x = 1" and language "python"
    And I close the code handle
    And the session is closed
    Then the color output contains ANSI codes
    And the color output contains "python"

  Scenario: Color materializer renders diff with coloured lines
    Given an OutputSession with format "color" using ColorMaterializer
    When I create a diff handle with file_a "a.py" and file_b "b.py"
    And I add a diff hunk with header "@@ -1,2 +1,3 @@" and context line "same" and add line "new"
    And I close the diff handle
    And the session is closed
    Then the color output contains ANSI codes

  Scenario: Color materializer renders diff with stats
    Given an OutputSession with format "color" using ColorMaterializer
    When I create a diff handle with file_a "a.py" and file_b "b.py"
    And I set diff stats with 3 insertions and 2 deletions
    And I close the diff handle
    And the session is closed
    Then the color output contains ANSI codes
    And the color output contains "insertion"

  Scenario: Color materializer renders separator with ANSI
    Given an OutputSession with format "color" using ColorMaterializer
    When I create a separator with style "line"
    And the session is closed
    Then the color output contains ANSI codes

  Scenario: Color materializer renders double separator
    Given an OutputSession with format "color" using ColorMaterializer
    When I create a separator with style "double"
    And the session is closed
    Then the color output contains ANSI codes

  Scenario: Color materializer renders action hint with ANSI
    Given an OutputSession with format "color" using ColorMaterializer
    When I create an action hint with commands "agents list" and description "Hint"
    And the session is closed
    Then the color output contains ANSI codes
    And the color output contains "agents list"

  # ================================================================
  # Table materializer rendering: new element types
  # ================================================================

  Scenario: Table materializer renders tree using colour renderer
    Given an OutputSession with format "table" using TableMaterializer
    When I create a tree handle with root label "TreeRoot"
    And I add a tree child "branch" under "TreeRoot"
    And I close the tree handle
    And the session is closed
    Then the strategy output contains "TreeRoot"

  Scenario: Table materializer renders code using colour renderer
    Given an OutputSession with format "table" using TableMaterializer
    When I create a code handle with content "code()" and language "python"
    And I close the code handle
    And the session is closed
    Then the strategy output contains "code()"

  Scenario: Table materializer renders diff using colour renderer
    Given an OutputSession with format "table" using TableMaterializer
    When I create a diff handle with file_a "a.py" and file_b "b.py"
    And I add a diff hunk with header "@@ -1 +1 @@" and context line "ctx" and add line "new"
    And I close the diff handle
    And the session is closed
    Then the strategy output contains "a.py"

  Scenario: Table materializer renders text as plain
    Given an OutputSession with format "table" using TableMaterializer
    When I create a text handle with content "plain text"
    And I close the text handle
    And the session is closed
    Then the strategy output contains "plain text"

  # ================================================================
  # Rich materializer rendering: new element types
  # ================================================================

  Scenario: Rich materializer renders tree with ANSI
    Given an OutputSession with format "rich" using RichMaterializer
    When I create a tree handle with root label "Root"
    And I add a tree child "child" under "Root"
    And I close the tree handle
    And the session is closed
    Then the rich output contains "Root"

  Scenario: Rich materializer renders code with language
    Given an OutputSession with format "rich" using RichMaterializer
    When I create a code handle with content "fn()" and language "python"
    And I close the code handle
    And the session is closed
    Then the rich output contains "fn()"

  # ================================================================
  # JSON materializer rendering: new element types
  # ================================================================

  Scenario: JSON materializer serializes tree element
    Given an OutputSession with format "json" using JsonMaterializer
    When I create a tree handle with root label "Project"
    And I add a tree child "src" under "Project"
    And I close the tree handle
    And the session is closed
    Then the json output is valid JSON
    And the json output contains element type "tree"
    And the json output contains "Project"
    And the json output contains "src"

  Scenario: JSON materializer serializes text element
    Given an OutputSession with format "json" using JsonMaterializer
    When I create a text handle with content "Some text"
    And I close the text handle
    And the session is closed
    Then the json output is valid JSON
    And the json output contains element type "text"
    And the json output contains "Some text"

  Scenario: JSON materializer serializes code element
    Given an OutputSession with format "json" using JsonMaterializer
    When I create a code handle with content "x = 1" and language "python"
    And I close the code handle
    And the session is closed
    Then the json output is valid JSON
    And the json output contains element type "code"
    And the json output contains "python"

  Scenario: JSON materializer serializes code with highlight lines
    Given an OutputSession with format "json" using JsonMaterializer
    When I create a code handle with content "a\nb\nc"
    And I set code highlight lines to "1,3"
    And I close the code handle
    And the session is closed
    Then the json output is valid JSON
    And the json output contains "highlight_lines"

  Scenario: JSON materializer serializes diff element
    Given an OutputSession with format "json" using JsonMaterializer
    When I create a diff handle with file_a "old.py" and file_b "new.py"
    And I add a diff hunk with header "@@ -1 +1 @@" and context line "ctx" and add line "added"
    And I set diff stats with 1 insertions and 0 deletions
    And I close the diff handle
    And the session is closed
    Then the json output is valid JSON
    And the json output contains element type "diff"
    And the json output contains "old.py"
    And the json output contains "stats"

  Scenario: JSON materializer serializes separator element
    Given an OutputSession with format "json" using JsonMaterializer
    When I create a separator with style "line"
    And the session is closed
    Then the json output is valid JSON
    And the json output contains element type "separator"

  Scenario: JSON materializer serializes action hint element
    Given an OutputSession with format "json" using JsonMaterializer
    When I create an action hint with commands "agents list" and description "Try this"
    And the session is closed
    Then the json output is valid JSON
    And the json output contains element type "action_hint"
    And the json output contains "agents list"

  # ================================================================
  # YAML materializer rendering: new element types
  # ================================================================

  Scenario: YAML materializer serializes tree element
    Given an OutputSession with format "yaml" using YamlMaterializer
    When I create a tree handle with root label "Project"
    And I add a tree child "src" under "Project"
    And I close the tree handle
    And the session is closed
    Then the yaml output is valid YAML
    And the yaml output contains "tree"
    And the yaml output contains "Project"

  Scenario: YAML materializer serializes text element
    Given an OutputSession with format "yaml" using YamlMaterializer
    When I create a text handle with content "yaml text"
    And I close the text handle
    And the session is closed
    Then the yaml output is valid YAML
    And the yaml output contains "text"

  Scenario: YAML materializer serializes code element
    Given an OutputSession with format "yaml" using YamlMaterializer
    When I create a code handle with content "fn()" and language "ruby"
    And I close the code handle
    And the session is closed
    Then the yaml output is valid YAML
    And the yaml output contains "ruby"

  Scenario: YAML materializer serializes diff element
    Given an OutputSession with format "yaml" using YamlMaterializer
    When I create a diff handle with file_a "a.py" and file_b "b.py"
    And I add a diff hunk with header "@@ -1 +1 @@" and context line "x" and add line "y"
    And I close the diff handle
    And the session is closed
    Then the yaml output is valid YAML
    And the yaml output contains "diff"

  Scenario: YAML materializer serializes separator element
    Given an OutputSession with format "yaml" using YamlMaterializer
    When I create a separator with style "double"
    And the session is closed
    Then the yaml output is valid YAML
    And the yaml output contains "separator"

  Scenario: YAML materializer serializes action hint element
    Given an OutputSession with format "yaml" using YamlMaterializer
    When I create an action hint with commands "agents list" and description "Do this"
    And the session is closed
    Then the yaml output is valid YAML
    And the yaml output contains "action_hint"

  # ================================================================
  # Plain diff rendering: edge cases
  # ================================================================

  Scenario: Plain materializer renders diff with no files
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a diff handle with no files
    And I add a diff hunk with header "@@ -1 +1 @@" and context line "x" and add line "y"
    And I close the diff handle
    And the session is closed
    Then the plain output contains "@@ -1 +1 @@"

  Scenario: Plain materializer renders diff with remove lines
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a diff handle with file_a "a.py" and file_b "b.py"
    And I add a diff hunk with header "@@ -1,2 +1,1 @@" and remove line "deleted"
    And I close the diff handle
    And the session is closed
    Then the plain output contains "-deleted"

  # ================================================================
  # Color diff rendering: edge cases
  # ================================================================

  Scenario: Color materializer renders diff with no files
    Given an OutputSession with format "color" using ColorMaterializer
    When I create a diff handle with no files
    And I add a diff hunk with header "@@ -1 +1 @@" and context line "x" and add line "y"
    And I close the diff handle
    And the session is closed
    Then the color output contains ANSI codes

  Scenario: Color materializer renders separator blank
    Given an OutputSession with format "color" using ColorMaterializer
    When I create a separator with style "blank"
    And the session is closed
    Then the session completed without error

  # ================================================================
  # TreeHandle: find_node with empty path returns root
  # ================================================================

  Scenario: TreeHandle find_node with empty path returns root
    Given an OutputSession with format "plain"
    When I create a tree handle with root label "Root"
    And I add a tree child "child" under the root via empty path
    Then the tree root has 1 children

  # ================================================================
  # F5: Rich materializer — missing 7 element types
  # ================================================================

  Scenario: Rich materializer renders table
    Given an OutputSession with format "rich" using RichMaterializer
    When I create a table handle with columns "Name,Status"
    And I add a dict row with Name "svc" and Status "ok"
    And I close the table handle
    And the session is closed
    Then the rich output contains "svc"

  Scenario: Rich materializer renders status
    Given an OutputSession with format "rich" using RichMaterializer
    When I create a status handle with message "All good"
    And I close the status handle
    And the session is closed
    Then the rich output contains "All good"

  Scenario: Rich materializer renders progress
    Given an OutputSession with format "rich" using RichMaterializer
    When I create a progress handle labelled "Loading" with total 100
    And I set progress to 50 of 100
    And I close the progress handle
    And the session is closed
    Then the rich output contains "Loading"

  Scenario: Rich materializer renders text
    Given an OutputSession with format "rich" using RichMaterializer
    When I create a text handle with content "Some rich text"
    And I close the text handle
    And the session is closed
    Then the rich output contains "Some rich text"

  Scenario: Rich materializer renders diff
    Given an OutputSession with format "rich" using RichMaterializer
    When I create a diff handle with file_a "old.py" and file_b "new.py"
    And I add a diff hunk with header "@@ -1 +1 @@" and context line "ctx" and add line "added"
    And I close the diff handle
    And the session is closed
    Then the rich output contains "old.py"

  Scenario: Rich materializer renders separator
    Given an OutputSession with format "rich" using RichMaterializer
    When I create a separator with style "line"
    And the session is closed
    Then the session completed without error

  Scenario: Rich materializer renders action hint
    Given an OutputSession with format "rich" using RichMaterializer
    When I create an action hint with commands "deploy"
    And the session is closed
    Then the rich output contains "deploy"

  # ================================================================
  # N4: strip_terminal_escapes — security-critical function
  # ================================================================

  Scenario: strip_terminal_escapes removes 7-bit CSI sequences
    When I strip terminal escapes from "hello\x1b[31mred\x1b[0m world"
    Then the stripped result is "hellored world"

  Scenario: strip_terminal_escapes removes 8-bit CSI sequences
    When I strip terminal escapes from text with 8-bit CSI
    Then the stripped result does not contain bytes 0x9b

  Scenario: strip_terminal_escapes removes OSC sequences
    When I strip terminal escapes from text with OSC title set
    Then the stripped result does not contain OSC payload

  Scenario: strip_terminal_escapes removes C1 control characters
    When I strip terminal escapes from text with C1 control chars
    Then the stripped result does not contain bytes 0x80 through 0x9f

  Scenario: strip_terminal_escapes preserves normal text
    When I strip terminal escapes from "hello world 123"
    Then the stripped result is "hello world 123"

  Scenario: strip_terminal_escapes preserves tabs and newlines
    When I strip terminal escapes from "line1\tindented\nline2"
    Then the stripped result is "line1\tindented\nline2"

  Scenario: strip_terminal_escapes removes BEL-terminated OSC
    When I strip terminal escapes from text with BEL-terminated OSC
    Then the stripped result is "safetext"

  Scenario: strip_terminal_escapes removes private-mode CSI
    When I strip terminal escapes from text with private-mode CSI
    Then the stripped result is "content"

  Scenario: strip_terminal_escapes removes hyperlink OSC injection
    When I strip terminal escapes from text with hyperlink OSC
    Then the stripped result is "click here"

  Scenario: strip_terminal_escapes handles empty input
    When I strip terminal escapes from ""
    Then the stripped result is ""

  Scenario: strip_terminal_escapes handles mixed escape combinations
    When I strip terminal escapes from text with mixed escapes
    Then the stripped result is "hello world"

  # ================================================================
  # N11: YAML materializer — missing Panel, Table, Status, Progress
  # ================================================================

  Scenario: YAML materializer serializes panel element
    Given an OutputSession with format "yaml" using YamlMaterializer
    When I create a panel handle with title "Info"
    And I set entry "Name" to "myapp"
    And I close the panel handle
    And the session is closed
    Then the yaml output is valid YAML
    And the yaml output contains "panel"
    And the yaml output contains "Info"

  Scenario: YAML materializer serializes table element
    Given an OutputSession with format "yaml" using YamlMaterializer
    When I create a table handle with columns "Col1,Col2"
    And I add a dict row with Name "Col1" and Status "val1"
    And I close the table handle
    And the session is closed
    Then the yaml output is valid YAML
    And the yaml output contains "table"
    And the yaml output contains "Col1"

  Scenario: YAML materializer serializes status element
    Given an OutputSession with format "yaml" using YamlMaterializer
    When I create a status handle with message "System ready"
    And I close the status handle
    And the session is closed
    Then the yaml output is valid YAML
    And the yaml output contains "status"
    And the yaml output contains "System ready"

  Scenario: YAML materializer serializes progress element
    Given an OutputSession with format "yaml" using YamlMaterializer
    When I create a progress handle labelled "Deploying" with total 50
    And I set progress to 25 of 50
    And I close the progress handle
    And the session is closed
    Then the yaml output is valid YAML

  # ================================================================
  # N12: Table materializer — missing Status, Progress, Separator, ActionHint
  # ================================================================

  Scenario: Table materializer renders status
    Given an OutputSession with format "table" using TableMaterializer
    When I create a status handle with message "System OK"
    And I close the status handle
    And the session is closed
    Then the strategy output contains "System OK"

  Scenario: Table materializer renders progress
    Given an OutputSession with format "table" using TableMaterializer
    When I create a progress handle labelled "Building" with total 10
    And I set progress to 5 of 10
    And I close the progress handle
    And the session is closed
    Then the strategy output contains "Building"

  Scenario: Table materializer renders separator
    Given an OutputSession with format "table" using TableMaterializer
    When I create a separator with style "double"
    And the session is closed
    Then the session completed without error

  Scenario: Table materializer renders action hint
    Given an OutputSession with format "table" using TableMaterializer
    When I create an action hint with commands "agents list"
    And the session is closed
    Then the strategy output contains "agents list"

  # ================================================================
  # N13: Concurrency tests
  # ================================================================

  Scenario: Concurrent writes to the same handle are thread-safe
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a text handle with content ""
    And I concurrently append 100 lines to the same text handle from 4 threads
    And I close the text handle
    And the session is closed
    Then the text handle has exactly 100 appended segments

  Scenario: Concurrent handle creation is thread-safe
    Given an OutputSession with format "plain" using PlainMaterializer
    When I concurrently create 20 panels from 4 threads
    And the session is closed
    Then the session snapshot has 20 elements

  Scenario: Race between session close and handle write does not crash
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a text handle with content "initial"
    And I race session close against handle writes
    Then no unhandled exception was raised

  # ================================================================
  # N14: Progress throttling
  # ================================================================

  Scenario: Progress handle throttles rapid updates
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a progress handle labelled "Throttle test" with total 1000
    And I rapidly tick the progress 200 times within 50ms
    And I close the progress handle
    Then fewer than 20 element-updated events were emitted for the progress handle

  # ================================================================
  # Luis review fixes — M2: TreeHandle rejects slash in labels
  # ================================================================

  Scenario: TreeHandle rejects label containing slash
    Given an OutputSession with format "plain"
    When I create a tree handle with root label "Root"
    Then adding a tree child with slash in label raises ValueError

  # ================================================================
  # Luis review fixes — M3: MAX_ELEMENTS_PER_SESSION enforcement
  # ================================================================

  Scenario: Session enforces MAX_ELEMENTS_PER_SESSION limit
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create elements up to the session limit
    Then creating one more element raises ValueError

  # ================================================================
  # R2-C2 fix #15: MAX_TABLE_ROWS enforcement
  # ================================================================

  Scenario: Table enforces MAX_TABLE_ROWS limit on add_row
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a table handle with columns "Name"
    And I fill the table to its MAX_TABLE_ROWS limit
    Then adding one more row raises ValueError

  # ================================================================
  # Luis review fixes — M4: sort_descending rendering
  # ================================================================

  Scenario: Table renders rows in descending sort order
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a table handle with columns "Name,Status"
    And I add a dict row with Name "Alpha" and Status "1"
    And I add a dict row with Name "Charlie" and Status "3"
    And I add a dict row with Name "Bravo" and Status "2"
    And I set sort key to "Name" descending
    And I close the table handle
    And the session is closed
    Then the plain output has "Charlie" before "Alpha"

  # ================================================================
  # Luis review fixes — L4: TextBlock indent validation
  # ================================================================

  Scenario: TextBlock rejects negative indent
    Given an OutputSession with format "plain"
    Then creating a text handle with negative indent raises ValueError

  # ================================================================
  # Luis review fixes — L9: TextBlock with wrap=False
  # ================================================================

  Scenario: Plain materializer renders text with wrap=False verbatim
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a text handle with content "first line" and wrap false and indent 4
    And I close the text handle
    And the session is closed
    Then the plain output contains "    first line"

  # ================================================================
  # Luis review fixes — L10: DiffBlock with no files and no hunks
  # ================================================================

  Scenario: Plain materializer renders empty diff block
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a diff handle with no files
    And I close the diff handle
    And the session is closed
    Then the session completed without error

  # ================================================================
  # Review fix — collapsed tree node rendering (L3 fix validation)
  # ================================================================

  Scenario: Plain materializer renders collapsed tree child with marker
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a tree handle with root label "Root"
    And I add a collapsed tree child "hidden" under "Root"
    And I close the tree handle
    And the session is closed
    Then the plain output contains "[collapsed]"
    And the plain output contains "hidden"

  Scenario: Plain materializer renders collapsed child in flat mode
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a tree handle with root label "Flat" and show_guides false
    And I add a collapsed tree child "hidden" under "Flat"
    And I close the tree handle
    And the session is closed
    Then the plain output contains "[collapsed]"
    And the plain output contains "hidden"

  # ================================================================
  # Review fix — detect_terminal_capabilities actual assertion
  # ================================================================

  Scenario: Detect terminal capabilities returns correct types
    When I detect terminal capabilities
    Then the capabilities is_tty is a boolean
    And the capabilities supports_ansi is a boolean
    And the capabilities supports_cursor is a boolean
    And the capabilities term is a string

  # ================================================================
  # Review fix — session double-close behavior (C1 fix validation)
  # ================================================================

  Scenario: Session double-close does not deadlock or raise
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a panel handle with title "DoubleClose"
    And I set entry "K" to "V"
    And the session is closed
    And the session is closed again
    Then no deadlock occurred and both closes returned snapshots

  # ================================================================
  # Review fix — ColumnDef non-default field serialization
  # ================================================================

  Scenario: JSON materializer serializes non-default ColumnDef fields
    Given an OutputSession with format "json" using JsonMaterializer
    When I create a table with non-default ColumnDef fields
    And I add a dict row with Name "Name" and Status "Alice"
    And I close the table handle
    And the session is closed
    Then the json output is valid JSON
    And the json output contains "alignment"
    And the json output contains "sortable"
    And the json output contains "width_hint"

  # ================================================================
  # P1-1: ProgressHandle validates total parameter
  # ================================================================

  Scenario: ProgressHandle rejects negative total in set_progress
    Given an OutputSession with format "plain"
    When I create a progress handle labelled "Test"
    Then setting progress total to negative raises ValueError

  Scenario: Session progress factory rejects negative total
    Given an OutputSession with format "plain"
    Then creating a progress with negative total raises ValueError

  # ================================================================
  # P1-2: JSON/YAML output includes timing field
  # ================================================================

  Scenario: JSON output includes timing after session close
    Given an OutputSession with format "json" using JsonMaterializer
    When I create a panel handle with title "Timed"
    And I close the panel handle
    And the session is closed
    Then the json output is valid JSON
    And the json output contains "timing"

  # ================================================================
  # P1-4: NO_COLOR environment variable forces plain format
  # ================================================================

  Scenario: NO_COLOR env var forces plain materializer for rich
    Given the NO_COLOR environment variable is set
    When I select a materializer for format "rich"
    Then the selected materializer is PlainMaterializer

  Scenario: NO_COLOR env var forces plain materializer for color
    Given the NO_COLOR environment variable is set
    When I select a materializer for format "color"
    Then the selected materializer is PlainMaterializer

  Scenario: NO_COLOR env var forces plain materializer for table
    Given the NO_COLOR environment variable is set
    When I select a materializer for format "table"
    Then the selected materializer is PlainMaterializer

  Scenario: NO_COLOR env var does not affect json format
    Given the NO_COLOR environment variable is set
    When I select a materializer for format "json"
    Then the selected materializer is JsonMaterializer

  # ================================================================
  # P2-1: DiffLine.line_type backward compatibility
  # ================================================================

  Scenario: DiffLine accepts line_type field name
    When I create a DiffLine with line_type "add" and content "new"
    Then the DiffLine line_type is "add"

  Scenario: DiffLine accepts type alias for backward compatibility
    When I create a DiffLine with type alias "remove" and content "old"
    Then the DiffLine line_type is "remove"

  # ================================================================
  # P2-2: Color materializer renders text with ANSI codes
  # ================================================================

  Scenario: Color materializer renders text block with ANSI codes
    Given an OutputSession with format "color" using ColorMaterializer
    When I create a text handle with content "Color styled text"
    And I close the text handle
    And the session is closed
    Then the color output contains ANSI codes
    And the color output contains "Color styled text"

  # ================================================================
  # P2-3: Numeric column sorting
  # ================================================================

  Scenario: Table sorts numeric column by value not lexicographically
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a table with a numeric column "Score"
    And I add rows with scores 9, 10, 100, 20
    And I set sort key to "Score" ascending
    And I close the table handle
    And the session is closed
    Then the plain output has "9" before "10"
    And the plain output has "10" before "20"
    And the plain output has "20" before "100"

  # ================================================================
  # P2-4: ColumnDef serialization includes all fields
  # ================================================================

  Scenario: JSON output includes all ColumnDef fields even when default
    Given an OutputSession with format "json" using JsonMaterializer
    When I create a table with default ColumnDef fields
    And I add a dict row with Name "Name" and Status "Alice"
    And I close the table handle
    And the session is closed
    Then the json output is valid JSON
    And the json output contains "width_hint"
    And the json output contains "alignment"

  # ================================================================
  # P2-5: Unicode box-drawing characters
  # ================================================================

  Scenario: Table materializer uses Unicode box-drawing characters
    Given an OutputSession with format "table" using TableMaterializer
    When I create a table handle with columns "Name,Status"
    And I add a dict row with Name "svc" and Status "up"
    And I close the table handle
    And the session is closed
    Then the strategy output contains Unicode box-drawing chars

  # ================================================================
  # P3-3: MAX_TABLE_ROWS enforcement in add_rows batch
  # ================================================================

  Scenario: Table enforces MAX_TABLE_ROWS limit on add_rows batch
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a table handle with columns "Name"
    And I fill the table close to its limit
    Then adding a batch that exceeds the limit raises ValueError

  # ================================================================
  # P3-5: Stress test with 10+ concurrent producers
  # ================================================================

  Scenario: 10 concurrent producers create handles without corruption
    Given an OutputSession with format "plain" using PlainMaterializer
    When I concurrently create 10 panels from 10 threads
    And the session is closed
    Then the session snapshot has 10 elements

  # ================================================================
  # P3-6: Boxdraw summary truncation
  # ================================================================

  Scenario: Boxdraw table truncates long summary to fit border
    Given an OutputSession with format "table" using TableMaterializer
    When I create a table with a 2-char column "AB"
    And I set a very long summary string
    And I close the table handle
    And the session is closed
    Then the strategy output does not have summary exceeding table width

  # ================================================================
  # P3-7: Explicit format flag for color and table
  # ================================================================

  Scenario: Explicit color format creates ColorMaterializer without fallback
    Given a non-TTY terminal environment
    When I select a materializer for format "color" with explicit flag
    Then the selected materializer is ColorMaterializer

  Scenario: Explicit table format creates TableMaterializer without fallback
    Given a non-TTY terminal environment
    When I select a materializer for format "table" with explicit flag
    Then the selected materializer is TableMaterializer

  # ================================================================
  # P3-8: ProgressHandle rejects zero delta
  # ================================================================

  Scenario: ProgressHandle rejects zero increment delta
    Given an OutputSession with format "plain"
    When I create a progress handle labelled "Test"
    Then incrementing by zero delta raises ValueError

  # ================================================================
  # Coverage: color materializer table with summary
  # ================================================================

  Scenario: Color materializer renders table with summary
    Given an OutputSession with format "color" using ColorMaterializer
    When I create a table handle with columns "Name,Count"
    And I add a dict row with Name "A" and Status "10"
    And I set summary with total "10"
    And I close the table handle
    And the session is closed
    Then the color output contains ANSI codes
    And the color output contains "total"

  # ================================================================
  # Coverage: color diff with remove lines
  # ================================================================

  Scenario: Color materializer renders diff with remove lines
    Given an OutputSession with format "color" using ColorMaterializer
    When I create a diff handle with file_a "a.py" and file_b "b.py"
    And I add a diff hunk with header "@@ -1,2 +1,1 @@" and remove line "deleted"
    And I close the diff handle
    And the session is closed
    Then the color output contains ANSI codes

  # ================================================================
  # Coverage: color text with wrap=False and multiline
  # ================================================================

  Scenario: Color materializer renders text with wrap=False
    Given an OutputSession with format "color" using ColorMaterializer
    When I create a text handle with content "first line" and wrap false and indent 2
    And I close the text handle
    And the session is closed
    Then the color output contains ANSI codes
    And the color output contains "first line"

  Scenario: Color materializer renders multiline text with empty lines
    Given an OutputSession with format "color" using ColorMaterializer
    When I create a text handle with multiline content including empty lines
    And I close the text handle
    And the session is closed
    Then the color output contains ANSI codes

  # ================================================================
  # Coverage: _reset_counters test helper
  # ================================================================

  Scenario: ID counter reset helper works correctly
    When I reset the ID counters
    Then the next session ID starts from 1
    And the next handle ID starts from 1

  # ================================================================
  # Coverage: numeric sort with non-numeric values
  # ================================================================

  Scenario: Table numeric sort handles non-numeric values gracefully
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a table with a numeric column "Score"
    And I add rows with scores 10, N/A, 5
    And I set sort key to "Score" ascending
    And I close the table handle
    And the session is closed
    Then the plain output has "5" before "10"

  # ================================================================
  # Coverage: JSON diff with line numbers
  # ================================================================

  Scenario: JSON materializer serializes diff with line numbers
    Given an OutputSession with format "json" using JsonMaterializer
    When I create a diff handle with file_a "a.py" and file_b "b.py"
    And I add a diff hunk with line numbers
    And I close the diff handle
    And the session is closed
    Then the json output is valid JSON
    And the json output contains "line_number_old"
    And the json output contains "line_number_new"

  # ================================================================
  # Coverage: JSON tree with style_hint and metadata
  # ================================================================

  Scenario: JSON materializer serializes tree with style hint and metadata
    Given an OutputSession with format "json" using JsonMaterializer
    When I create a tree handle with root label "Root"
    And I add a tree child "styled" under "Root" with style "success" and metadata
    And I close the tree handle
    And the session is closed
    Then the json output is valid JSON
    And the json output contains "style_hint"
    And the json output contains "metadata"

  # ================================================================
  # Coverage: flat tree (no guides) depth guard
  # ================================================================

  Scenario: Flat tree rendering truncates at max_depth_hint
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a tree handle with root label "Flat" and show_guides false and max_depth_hint 1
    And I add a tree child "child" under "Flat"
    And I add a tree child "grandchild" under "Flat/child"
    And I close the tree handle
    And the session is closed
    Then the plain output contains "Flat"
    And the plain output contains "child"
    And the plain output contains "more"

  # ================================================================
  # Coverage: _find_node cache miss traversal
  # ================================================================

  Scenario: TreeHandle find_node traverses path on cache miss
    Given an OutputSession with format "plain"
    When I create a tree handle with root label "Root"
    And I build a 3-level deep tree and query a deep path
    Then the deep tree node was found correctly

  # ================================================================
  # Coverage: TextHandle _emit_update with incremental strategy
  # ================================================================

  Scenario: TextHandle emits updates when strategy supports incremental
    Given an OutputSession with a mock incremental strategy
    When I create a text handle with content ""
    And I append text "hello"
    Then the incremental strategy received an update event

  # ================================================================
  # Coverage: table format falls back to color on non-TTY with ANSI
  # ================================================================

  Scenario: Table format falls back to color when non-TTY but ANSI
    Given a terminal with ANSI but non-TTY
    When I select a materializer for format "table"
    Then the selected materializer is ColorMaterializer

  # ================================================================
  # Coverage: set_node_style with non-existent path
  # ================================================================

  Scenario: TreeHandle set_node_style rejects non-existent path
    Given an OutputSession with format "plain"
    When I create a tree handle with root label "Root"
    Then setting tree node style on non-existent path raises ValueError

  # ================================================================
  # Coverage: session status factory rejects invalid level
  # ================================================================

  Scenario: Session status factory rejects invalid level
    Given an OutputSession with format "plain"
    Then creating a status with invalid level raises ValueError

  # ================================================================
  # Coverage: JSON tree with collapsed node
  # ================================================================

  Scenario: JSON materializer serializes tree with collapsed node
    Given an OutputSession with format "json" using JsonMaterializer
    When I create a tree handle with root label "Root"
    And I add a collapsed tree child "hidden" under "Root"
    And I close the tree handle
    And the session is closed
    Then the json output is valid JSON
    And the json output contains "collapsed"

  # ================================================================
  # Coverage: deep tree at MAX_TREE_DEPTH rendering
  # ================================================================

  Scenario: Deep tree at MAX_TREE_DEPTH triggers render depth guard
    Given an OutputSession with format "plain" using PlainMaterializer
    When I create a tree at maximum depth
    And I close the tree handle
    And the session is closed
    Then the deep tree output contains "more"

  # ================================================================
  # Coverage: JSON tree truncated at depth limit
  # ================================================================

  Scenario: JSON tree node serialization truncates at depth limit
    When I serialize a tree node beyond MAX_TREE_DEPTH
    Then the serialized node contains truncated flag

  # ================================================================
  # Coverage: selection.py — rich with cursor support
  # ================================================================

  Scenario: Rich format uses RichMaterializer when cursor supported
    Given a terminal with cursor support
    When I select a materializer for format "rich"
    Then the selected materializer is RichMaterializer

  Scenario: Rich format falls back to color when ANSI but no TTY no cursor
    Given a terminal with ANSI only no TTY no cursor
    When I select a materializer for format "rich"
    Then the selected materializer is ColorMaterializer

  # ================================================================
  # Coverage: TreeHandle add_child exceeds MAX_TREE_DEPTH
  # ================================================================

  Scenario: TreeHandle rejects child beyond MAX_TREE_DEPTH
    Given an OutputSession with format "plain"
    When I create a tree at exactly max depth via handle
    Then adding one more child level raises ValueError

  # ================================================================
  # Coverage: detect_terminal_capabilities with cursor-capable TERM
  # ================================================================

  Scenario: detect_terminal_capabilities detects cursor support for xterm
    When I detect capabilities with TERM "xterm-256color" on a TTY
    Then the capabilities supports_cursor is true

  # ================================================================
  # Coverage: _find_node with mismatched root label
  # ================================================================

  Scenario: TreeHandle find_node returns None for mismatched root
    Given an OutputSession with format "plain"
    When I create a tree handle with root label "Root"
    Then querying a path with wrong root returns None

  # ================================================================
  # LiveMaterializationStrategy
  # ================================================================

  Scenario: LiveMaterializationStrategy renders element on creation
    Given an OutputSession with format "rich" using RichMaterializer
    When I create a panel handle with title "Live Panel"
    And I set entry "Key" to "Val"
    And I close the panel handle
    And the session is closed
    Then the rich output contains "Live Panel"
    And the rich output contains "Key"

  Scenario: LiveMaterializationStrategy supports incremental updates
    Given a RichMaterializer instance
    Then the rich materializer supports incremental updates
    And the rich materializer strategy name is "rich"

  Scenario: LiveMaterializationStrategy coalesces dirty elements
    Given an OutputSession with format "rich" using RichMaterializer
    When I create a text handle with content "initial"
    And I append text " more"
    And I close the text handle
    And the session is closed
    Then the rich output contains "initial more"

  Scenario: LiveMaterializationStrategy renders all element types
    Given an OutputSession with format "rich" using RichMaterializer
    When I create a tree handle with root label "Root"
    And I add a tree child "child" under "Root"
    And I close the tree handle
    And I create a code handle with content "x = 1" and language "python"
    And I close the code handle
    And I create a diff handle with file_a "a.py" and file_b "b.py"
    And I add a diff hunk with header "@@ -1 +1 @@" and context line "ctx" and add line "new"
    And I close the diff handle
    And I create a separator with style "line"
    And I create an action hint with commands "deploy"
    And the session is closed
    Then the rich output contains "Root"
    And the rich output contains "x = 1"
    And the rich output contains "a.py"
    And the rich output contains "deploy"

  Scenario: LiveMaterializationStrategy frame rate is approximately 15 fps
    Given a RichMaterializer instance
    Then the live strategy frame rate is 15.0
