//! Scenario DSL for unified E2E, Conformance, and Benchmark testing.
//!
//! This module defines a `Scenario` struct that can drive tests in three modes:
//! - **E2E**: Run `br` commands only, validate exit codes and JSON shapes
//! - **Conformance**: Run both `br` and `bd`, compare outputs with normalization
//! - **Benchmark**: Time commands, capture RSS, produce metrics
//!
//! Related beads:
//! - beads_rust-ir0t: Scenario DSL + normalization rules for conformance
//! - beads_rust-ag35: EPIC: Exhaustive E2E + Conformance + Benchmark Harness

#![allow(dead_code, clippy::similar_names)]

use super::binary_discovery::{check_bd_version, discover_binaries};
use super::dataset_registry::{DatasetOverride, IsolatedDataset, KnownDataset};
use super::harness::{
    CommandResult, ConformanceWorkspace as HarnessConformanceWorkspace, TestWorkspace,
};
use chrono::{DateTime, FixedOffset};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::{HashMap, HashSet};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
use tempfile::TempDir;
use walkdir::WalkDir;

/// Execution mode for a scenario.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ExecutionMode {
    /// E2E mode: run br only, validate behavior
    E2E,
    /// Conformance mode: run br and bd, compare outputs
    Conformance,
    /// Benchmark mode: time execution, capture metrics
    Benchmark,
}

/// How to compare JSON outputs in conformance mode.
#[derive(Debug, Clone, Default)]
pub enum CompareMode {
    /// JSON outputs must be byte-identical after stable sort
    ExactJson,
    /// Ignore volatile fields (timestamps, IDs), compare structure
    #[default]
    NormalizedJson,
    /// Only check that specific fields match
    ContainsFields(Vec<String>),
    /// Compare exit codes only, ignore output
    ExitCodeOnly,
    /// Arrays may be in different order (sort before compare)
    ArrayUnordered,
    /// Exclude specific fields from comparison
    FieldsExcluded(Vec<String>),
    /// Only compare JSON structure (keys present), not values
    StructureOnly,
}

/// Fields that are volatile and should be normalized.
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone, Default)]
pub struct NormalizationRules {
    /// Fields to replace with fixed placeholder (e.g., timestamps)
    pub mask_fields: HashSet<String>,
    /// Fields to completely remove before comparison
    pub remove_fields: HashSet<String>,
    /// Allow timestamp differences within this duration
    pub timestamp_tolerance: Option<Duration>,
    /// Sort arrays before comparison
    pub sort_arrays: bool,
    /// Normalize ID hash portions (keep prefix, mask hash)
    pub normalize_ids: bool,
    /// Log when normalization is applied
    pub log_normalization: bool,
    /// Normalize path separators (Windows backslash to Unix forward slash)
    pub normalize_paths: bool,
    /// Normalize line endings (CRLF to LF)
    pub normalize_line_endings: bool,
    /// Fields that contain file paths and should have separators normalized
    pub path_fields: HashSet<String>,
}

impl NormalizationRules {
    /// Default normalization for conformance (timestamps + IDs + cross-platform).
    pub fn conformance_default() -> Self {
        let mask_fields = [
            "created_at",
            "updated_at",
            "closed_at",
            "defer_until",
            "due_at",
            "deleted_at",
            "compacted_at",
        ]
        .into_iter()
        .map(String::from)
        .collect();

        let path_fields = [
            "path",
            "file_path",
            "source_path",
            "db_path",
            "jsonl_path",
            "log_path",
            "workspace_root",
            "beads_dir",
        ]
        .into_iter()
        .map(String::from)
        .collect();

        Self {
            mask_fields,
            remove_fields: HashSet::new(),
            timestamp_tolerance: Some(Duration::from_secs(5)),
            sort_arrays: true,
            normalize_ids: true,
            log_normalization: true,
            normalize_paths: true,
            normalize_line_endings: true,
            path_fields,
        }
    }

    /// Strict normalization: only sort arrays.
    pub fn strict() -> Self {
        Self {
            sort_arrays: true,
            log_normalization: true,
            ..Default::default()
        }
    }

    /// Cross-platform normalization only (paths + line endings).
    pub fn cross_platform() -> Self {
        let path_fields = [
            "path",
            "file_path",
            "source_path",
            "db_path",
            "jsonl_path",
            "log_path",
            "workspace_root",
            "beads_dir",
        ]
        .into_iter()
        .map(String::from)
        .collect();

        Self {
            normalize_paths: true,
            normalize_line_endings: true,
            path_fields,
            log_normalization: true,
            ..Default::default()
        }
    }

    /// Apply normalization to a JSON value.
    pub fn apply(&self, value: &mut Value) -> Vec<String> {
        let mut log = Vec::new();
        self.normalize_value(value, "", &mut log);
        log
    }

    fn normalize_value(&self, value: &mut Value, path: &str, log: &mut Vec<String>) {
        match value {
            Value::Object(map) => {
                // Remove fields
                for field in &self.remove_fields {
                    if map.remove(field).is_some() && self.log_normalization {
                        let removed_path = if path.is_empty() {
                            field.clone()
                        } else {
                            format!("{path}.{field}")
                        };
                        log.push(format!("Removed field: {removed_path}"));
                    }
                }

                // Mask and normalize fields
                for (key, val) in map.iter_mut() {
                    let field_path = if path.is_empty() {
                        key.clone()
                    } else {
                        format!("{path}.{key}")
                    };

                    if self.mask_fields.contains(key) {
                        if let Some(s) = val.as_str()
                            && !s.is_empty()
                            && self.log_normalization
                        {
                            log.push(format!("Masked timestamp: {field_path}"));
                        }
                        *val = Value::String("NORMALIZED_TIMESTAMP".to_string());
                    } else if self.normalize_ids && (key == "id" || key.ends_with("_id")) {
                        if let Some(s) = val.as_str()
                            && let Some(dash_pos) = s.rfind('-')
                        {
                            let prefix = &s[..dash_pos];
                            *val = Value::String(format!("{prefix}-HASH"));
                            if self.log_normalization {
                                log.push(format!("Normalized ID: {field_path}"));
                            }
                        }
                    } else if self.normalize_paths && self.path_fields.contains(key) {
                        // Normalize path separators (Windows backslash to Unix forward slash)
                        if let Some(s) = val.as_str()
                            && s.contains('\\')
                        {
                            let normalized = s.replace('\\', "/");
                            if self.log_normalization {
                                log.push(format!("Normalized path: {field_path}"));
                            }
                            *val = Value::String(normalized);
                        }
                        // Also apply line ending normalization if enabled
                        if self.normalize_line_endings
                            && let Some(s) = val.as_str()
                            && s.contains("\r\n")
                        {
                            let normalized = s.replace("\r\n", "\n");
                            *val = Value::String(normalized);
                        }
                    } else {
                        self.normalize_value(val, &field_path, log);
                    }
                }
            }
            Value::Array(arr) => {
                for (i, item) in arr.iter_mut().enumerate() {
                    self.normalize_value(item, &format!("{path}[{i}]"), log);
                }
                if self.sort_arrays && !arr.is_empty() {
                    // Sort by JSON string representation for determinism
                    arr.sort_by(|a, b| {
                        serde_json::to_string(a)
                            .unwrap_or_default()
                            .cmp(&serde_json::to_string(b).unwrap_or_default())
                    });
                }
            }
            Value::String(s) => {
                // Normalize line endings for all string values
                if self.normalize_line_endings && s.contains("\r\n") {
                    let normalized = s.replace("\r\n", "\n");
                    if self.log_normalization {
                        log.push(format!("Normalized line endings: {path}"));
                    }
                    *s = normalized;
                }
            }
            _ => {}
        }
    }
}

/// Expected invariants for a scenario.
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone, Default)]
pub struct Invariants {
    /// Command must succeed (exit 0)
    pub expect_success: bool,
    /// Command must fail (non-zero exit)
    pub expect_failure: bool,
    /// Expected exit code (if specific)
    pub expected_exit_code: Option<i32>,
    /// stdout must contain these strings
    pub stdout_contains: Vec<String>,
    /// stderr must contain these strings
    pub stderr_contains: Vec<String>,
    /// stdout must NOT contain these strings
    pub stdout_excludes: Vec<String>,
    /// No git operations allowed (checked via log patterns)
    pub no_git_ops: bool,
    /// Only .beads/ files may be modified
    pub path_confinement: bool,
    /// JSON output must match this schema (field names present)
    pub json_schema_fields: Vec<String>,
}

impl Invariants {
    pub fn success() -> Self {
        Self {
            expect_success: true,
            ..Default::default()
        }
    }

    pub fn failure() -> Self {
        Self {
            expect_failure: true,
            ..Default::default()
        }
    }

    pub const fn with_no_git_ops(mut self) -> Self {
        self.no_git_ops = true;
        self
    }

    pub const fn with_path_confinement(mut self) -> Self {
        self.path_confinement = true;
        self
    }
}

/// Setup configuration for a scenario.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum ScenarioSetup {
    /// Start with a fresh (empty) workspace, run `br init`
    #[default]
    Fresh,
    /// Copy from a known dataset
    Dataset(KnownDataset),
    /// Run these setup commands before the test
    Commands(Vec<ScenarioCommand>),
}

/// A command to run as part of a scenario.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScenarioCommand {
    /// Command arguments (e.g., `["create", "Test issue", "--priority", "1"]`)
    pub args: Vec<String>,
    /// Environment variables to set
    pub env: Vec<(String, String)>,
    /// Optional stdin input
    pub stdin: Option<String>,
    /// Label for logging
    pub label: String,
}

impl ScenarioCommand {
    pub fn new<I, S>(args: I) -> Self
    where
        I: IntoIterator<Item = S>,
        S: Into<String>,
    {
        let args: Vec<String> = args.into_iter().map(Into::into).collect();
        let label = args.first().cloned().unwrap_or_else(|| "cmd".to_string());
        Self {
            args,
            env: Vec::new(),
            stdin: None,
            label,
        }
    }

    pub fn with_env<I, K, V>(mut self, env: I) -> Self
    where
        I: IntoIterator<Item = (K, V)>,
        K: Into<String>,
        V: Into<String>,
    {
        self.env = env.into_iter().map(|(k, v)| (k.into(), v.into())).collect();
        self
    }

    pub fn with_stdin(mut self, input: impl Into<String>) -> Self {
        self.stdin = Some(input.into());
        self
    }

    pub fn with_label(mut self, label: impl Into<String>) -> Self {
        self.label = label.into();
        self
    }
}

/// Expected result for a command inside a workspace evolution plan.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ExpectedCommandOutcome {
    /// Command must succeed.
    #[default]
    Success,
    /// Command must fail.
    Failure,
    /// Either success or failure is acceptable.
    Either,
}

/// A `ScenarioCommand` with expectation and ID-capture semantics.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkspaceEvolutionCommand {
    pub command: ScenarioCommand,
    pub expected: ExpectedCommandOutcome,
    pub capture_issue_id_as: Option<String>,
}

impl WorkspaceEvolutionCommand {
    pub fn new(command: ScenarioCommand) -> Self {
        Self {
            command,
            expected: ExpectedCommandOutcome::Success,
            capture_issue_id_as: None,
        }
    }

    pub const fn expect_failure(mut self) -> Self {
        self.expected = ExpectedCommandOutcome::Failure;
        self
    }

    pub const fn allow_either(mut self) -> Self {
        self.expected = ExpectedCommandOutcome::Either;
        self
    }

    pub fn capture_issue_id_as(mut self, alias: impl Into<String>) -> Self {
        self.capture_issue_id_as = Some(alias.into());
        self
    }
}

/// A single step in a long-lived workspace evolution plan.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WorkspaceEvolutionStep {
    /// Run a `br` command, optionally capturing created IDs for later steps.
    Command(WorkspaceEvolutionCommand),
    /// Snapshot the current workspace so later steps can restore it.
    Snapshot { label: String },
    /// Append raw text to `.beads/issues.jsonl` to simulate external drift/corruption.
    AppendToIssuesJsonl { label: String, text: String },
    /// Restore `.beads/` and `.git/` from a named snapshot.
    RestoreSnapshot {
        label: String,
        snapshot_label: String,
    },
}

impl WorkspaceEvolutionStep {
    pub fn command(command: WorkspaceEvolutionCommand) -> Self {
        Self::Command(command)
    }

    pub fn snapshot(label: impl Into<String>) -> Self {
        Self::Snapshot {
            label: label.into(),
        }
    }

    pub fn append_to_issues_jsonl(label: impl Into<String>, text: impl Into<String>) -> Self {
        Self::AppendToIssuesJsonl {
            label: label.into(),
            text: text.into(),
        }
    }

    pub fn restore_snapshot(label: impl Into<String>, snapshot_label: impl Into<String>) -> Self {
        Self::RestoreSnapshot {
            label: label.into(),
            snapshot_label: snapshot_label.into(),
        }
    }

    pub fn label(&self) -> &str {
        match self {
            Self::Command(command) => &command.command.label,
            Self::Snapshot { label }
            | Self::AppendToIssuesJsonl { label, .. }
            | Self::RestoreSnapshot { label, .. } => label,
        }
    }
}

/// A deterministic plan that evolves one workspace across many commands.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkspaceEvolutionPlan {
    pub name: String,
    pub description: String,
    pub seed: u64,
    pub setup: ScenarioSetup,
    pub steps: Vec<WorkspaceEvolutionStep>,
}

impl WorkspaceEvolutionPlan {
    pub fn new(name: impl Into<String>, seed: u64) -> Self {
        Self {
            name: name.into(),
            description: String::new(),
            seed,
            setup: ScenarioSetup::Fresh,
            steps: Vec::new(),
        }
    }

    pub fn with_description(mut self, description: impl Into<String>) -> Self {
        self.description = description.into();
        self
    }

    pub fn with_setup(mut self, setup: ScenarioSetup) -> Self {
        self.setup = setup;
        self
    }

    pub fn with_steps<I>(mut self, steps: I) -> Self
    where
        I: IntoIterator<Item = WorkspaceEvolutionStep>,
    {
        self.steps = steps.into_iter().collect();
        self
    }

    pub fn push_step(mut self, step: WorkspaceEvolutionStep) -> Self {
        self.steps.push(step);
        self
    }

    pub fn execute(&self) -> std::io::Result<MaterializedWorkspaceEvolution> {
        let mut workspace = TestWorkspace::new("workspace_evolution", &self.name);
        let snapshot_dir = TempDir::new()?;
        let execution = execute_workspace_evolution_plan(self, &mut workspace, &snapshot_dir);

        match execution {
            Ok((mut events, aliases)) => {
                let materialized =
                    materialize_workspace_evolution(self, &workspace, &mut events, aliases)?;
                workspace.finish(true);
                Ok(materialized)
            }
            Err(err) => {
                workspace.finish(false);
                Err(err)
            }
        }
    }
}

/// Event emitted while materializing a long-lived workspace evolution plan.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WorkspaceEvolutionEventKind {
    Command,
    Snapshot,
    Mutation,
    Restore,
}

/// Result of one step inside a materialized workspace evolution run.
#[derive(Debug, Clone)]
pub struct WorkspaceEvolutionEvent {
    pub label: String,
    pub kind: WorkspaceEvolutionEventKind,
    pub matched_expectation: bool,
    pub command_result: Option<CommandResult>,
    pub detail: Option<String>,
}

/// A materialized long-lived workspace ready to feed back into replay-style tests.
#[derive(Debug)]
pub struct MaterializedWorkspaceEvolution {
    temp_dir: TempDir,
    pub root: PathBuf,
    pub beads_dir: PathBuf,
    pub plan: WorkspaceEvolutionPlan,
    pub events: Vec<WorkspaceEvolutionEvent>,
    pub aliases: HashMap<String, String>,
}

impl MaterializedWorkspaceEvolution {
    pub fn event(&self, label: &str) -> Option<&WorkspaceEvolutionEvent> {
        self.events.iter().find(|event| event.label == label)
    }

    pub fn dataset_override(&self, reason: impl Into<String>) -> DatasetOverride {
        DatasetOverride::new(&self.root, reason).with_name(self.plan.name.clone())
    }

    pub fn materialize_into(&self, target_root: impl AsRef<Path>) -> std::io::Result<()> {
        copy_workspace_state(&self.root, target_root.as_ref())?;
        copy_dir_contents(self.root.join("logs"), target_root.as_ref().join("logs"))?;
        Ok(())
    }
}

/// Result of running a scenario.
#[derive(Debug)]
pub struct ScenarioResult {
    pub passed: bool,
    pub mode: ExecutionMode,
    pub br_result: Option<CommandResult>,
    pub bd_result: Option<CommandResult>,
    pub comparison_result: Option<ComparisonResult>,
    pub invariant_failures: Vec<String>,
    pub normalization_log: Vec<String>,
    pub benchmark_metrics: Option<BenchmarkMetrics>,
}

/// Result of comparing br and bd outputs.
#[derive(Debug)]
pub struct ComparisonResult {
    pub matched: bool,
    pub br_json: Option<Value>,
    pub bd_json: Option<Value>,
    pub diff_description: Option<String>,
}

/// Benchmark metrics from a single run.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BenchmarkMetrics {
    pub br_duration_ms: u128,
    pub bd_duration_ms: Option<u128>,
    pub speedup_ratio: Option<f64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub br_peak_rss_bytes: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub bd_peak_rss_bytes: Option<u64>,
    /// CPU time in milliseconds (user + system)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub br_cpu_time_ms: Option<u128>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub bd_cpu_time_ms: Option<u128>,
    /// Database file size after operation (bytes)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub db_size_bytes: Option<u64>,
    /// JSONL export file size after operation (bytes)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub jsonl_size_bytes: Option<u64>,
    /// Iteration number (for multi-run benchmarks)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub iteration: Option<u32>,
    /// Whether this was a warmup run
    #[serde(default)]
    pub is_warmup: bool,
}

/// Configuration for benchmark runs.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BenchmarkConfig {
    /// Number of warmup iterations (not counted in statistics)
    pub warmup_iterations: u32,
    /// Number of measured iterations
    pub measured_iterations: u32,
    /// Whether to include bd in benchmarks (for ratio computation)
    pub include_bd: bool,
    /// Whether to measure peak RSS (Linux only, may add overhead)
    pub measure_rss: bool,
    /// Whether to measure IO sizes (db and jsonl files)
    pub measure_io: bool,
    /// Minimum duration for a valid benchmark run (filters outliers)
    pub min_duration_ms: u64,
    /// Maximum duration before considering a timeout (0 = no limit)
    pub max_duration_ms: u64,
}

impl Default for BenchmarkConfig {
    fn default() -> Self {
        Self {
            warmup_iterations: 1,
            measured_iterations: 3,
            include_bd: std::env::var("BENCH_INCLUDE_BD").is_ok_and(|v| v == "1"),
            measure_rss: std::env::var("BENCH_MEASURE_RSS").map_or(true, |v| v != "0"),
            measure_io: std::env::var("BENCH_MEASURE_IO").map_or(true, |v| v != "0"),
            min_duration_ms: 0,
            max_duration_ms: 300_000, // 5 minutes
        }
    }
}

impl BenchmarkConfig {
    /// Minimal config for quick benchmarks (1 warmup, 1 measured)
    pub fn quick() -> Self {
        Self {
            warmup_iterations: 1,
            measured_iterations: 1,
            ..Default::default()
        }
    }

    /// Thorough config for production benchmarks
    pub fn thorough() -> Self {
        Self {
            warmup_iterations: 3,
            measured_iterations: 10,
            include_bd: true,
            ..Default::default()
        }
    }

    /// Builder: set iterations
    pub const fn with_iterations(mut self, warmup: u32, measured: u32) -> Self {
        self.warmup_iterations = warmup;
        self.measured_iterations = measured;
        self
    }

    /// Builder: include bd
    pub const fn with_bd(mut self, include: bool) -> Self {
        self.include_bd = include;
        self
    }
}

/// Aggregated benchmark statistics from multiple runs.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BenchmarkSummary {
    /// Scenario name
    pub scenario: String,
    /// Configuration used
    pub config: BenchmarkConfig,
    /// Start timestamp (RFC3339)
    pub started_at: String,
    /// End timestamp (RFC3339)
    pub completed_at: String,
    /// Total number of runs (including warmup)
    pub total_runs: u32,
    /// br statistics
    pub br_stats: RunStatistics,
    /// bd statistics (if included)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub bd_stats: Option<RunStatistics>,
    /// Speedup ratio (`bd_median` / `br_median`)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub speedup_ratio: Option<f64>,
    /// Individual run metrics (measured runs only)
    pub runs: Vec<BenchmarkMetrics>,
    /// Warnings or notes
    pub notes: Vec<String>,
}

/// Statistics for a set of runs.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunStatistics {
    /// Minimum duration (ms)
    pub min_ms: u128,
    /// Maximum duration (ms)
    pub max_ms: u128,
    /// Median duration (ms)
    pub median_ms: u128,
    /// Mean duration (ms)
    pub mean_ms: f64,
    /// Standard deviation (ms)
    pub stddev_ms: f64,
    /// Coefficient of variation (stddev/mean)
    pub cv: f64,
    /// Median peak RSS (bytes), if measured
    #[serde(skip_serializing_if = "Option::is_none")]
    pub median_rss_bytes: Option<u64>,
}

// ============================================================================
// BENCHMARK HELPERS (beads_rust-owu6)
// ============================================================================

/// Measure peak RSS from /proc/<pid>/status on Linux.
/// Returns `VmHWM` (high water mark) which is the peak resident set size.
#[cfg(target_os = "linux")]
pub fn measure_peak_rss(pid: u32) -> Option<u64> {
    use std::fs;
    let status_path = format!("/proc/{pid}/status");
    let content = fs::read_to_string(&status_path).ok()?;

    for line in content.lines() {
        // VmHWM is the peak RSS (high water mark)
        if line.starts_with("VmHWM:") {
            let parts: Vec<&str> = line.split_whitespace().collect();
            if parts.len() >= 2
                && let Ok(kb) = parts[1].parse::<u64>()
            {
                return Some(kb * 1024); // Convert KB to bytes
            }
        }
    }
    None
}

/// Stub for non-Linux platforms (RSS measurement not supported).
#[cfg(not(target_os = "linux"))]
pub fn measure_peak_rss(_pid: u32) -> Option<u64> {
    None
}

/// Measure IO sizes (database and JSONL files) in a workspace.
pub fn measure_io_sizes(workspace_root: &std::path::Path) -> (Option<u64>, Option<u64>) {
    let beads_dir = workspace_root.join(".beads");

    // Database size
    let db_size = beads_dir.join("beads.db").metadata().ok().map(|m| m.len());

    // JSONL size (issues.jsonl)
    let jsonl_size = beads_dir
        .join("issues.jsonl")
        .metadata()
        .ok()
        .map(|m| m.len());

    (db_size, jsonl_size)
}

/// Compute statistics from a vector of durations.
#[allow(clippy::cast_precision_loss)]
pub fn compute_statistics(durations: &[u128], rss_values: &[Option<u64>]) -> RunStatistics {
    let n = durations.len();
    if n == 0 {
        return RunStatistics {
            min_ms: 0,
            max_ms: 0,
            median_ms: 0,
            mean_ms: 0.0,
            stddev_ms: 0.0,
            cv: 0.0,
            median_rss_bytes: None,
        };
    }

    // Sort for min/max/median
    let mut sorted = durations.to_vec();
    sorted.sort_unstable();

    let min_ms = sorted[0];
    let max_ms = sorted[n - 1];
    let median_ms = if n.is_multiple_of(2) {
        u128::midpoint(sorted[n / 2 - 1], sorted[n / 2])
    } else {
        sorted[n / 2]
    };

    // Mean
    let sum: u128 = sorted.iter().sum();
    let mean_ms = sum as f64 / n as f64;

    // Standard deviation
    let variance: f64 = sorted
        .iter()
        .map(|&x| {
            let diff = x as f64 - mean_ms;
            diff * diff
        })
        .sum::<f64>()
        / n as f64;
    let stddev_ms = variance.sqrt();

    // Coefficient of variation
    let cv = if mean_ms > 0.0 {
        stddev_ms / mean_ms
    } else {
        0.0
    };

    // Median RSS
    let median_rss_bytes = {
        let mut rss_vals: Vec<u64> = rss_values.iter().filter_map(|&v| v).collect();
        if rss_vals.is_empty() {
            None
        } else {
            rss_vals.sort_unstable();
            let rn = rss_vals.len();
            Some(if rn.is_multiple_of(2) {
                u64::midpoint(rss_vals[rn / 2 - 1], rss_vals[rn / 2])
            } else {
                rss_vals[rn / 2]
            })
        }
    };

    RunStatistics {
        min_ms,
        max_ms,
        median_ms,
        mean_ms,
        stddev_ms,
        cv,
        median_rss_bytes,
    }
}

/// Benchmark runner that executes scenarios with proper methodology.
///
/// Implements:
/// - Warmup iterations (discarded from statistics)
/// - Multiple measured iterations
/// - Peak RSS measurement (Linux only)
/// - IO size measurement (db + jsonl files)
/// - Statistics computation (median, mean, stddev, CV)
/// - Optional br/bd comparison with ratio computation
pub struct BenchmarkRunner {
    config: BenchmarkConfig,
    artifacts_dir: Option<std::path::PathBuf>,
}

impl BenchmarkRunner {
    /// Create a new benchmark runner with the given configuration.
    pub fn new(config: BenchmarkConfig) -> Self {
        let artifacts_dir = std::env::var("BENCH_ARTIFACTS_DIR")
            .ok()
            .map(std::path::PathBuf::from);

        Self {
            config,
            artifacts_dir,
        }
    }

    /// Create a runner with default configuration.
    pub fn default_runner() -> Self {
        Self::new(BenchmarkConfig::default())
    }

    /// Create a runner for quick benchmarks (minimal iterations).
    pub fn quick() -> Self {
        Self::new(BenchmarkConfig::quick())
    }

    /// Create a runner for thorough benchmarks.
    pub fn thorough() -> Self {
        Self::new(BenchmarkConfig::thorough())
    }

    /// Run a benchmark for a scenario and return the summary.
    pub fn run_benchmark(&self, scenario: &Scenario) -> BenchmarkSummary {
        use chrono::Utc;

        let started_at = Utc::now().to_rfc3339();
        let mut all_runs: Vec<BenchmarkMetrics> = Vec::new();
        let mut notes: Vec<String> = Vec::new();
        let include_bd = self.resolve_include_bd(&mut notes);

        let total_iterations = self.config.warmup_iterations + self.config.measured_iterations;

        // Run all iterations (warmup + measured)
        for i in 0..total_iterations {
            let is_warmup = i < self.config.warmup_iterations;
            let iteration = i + 1;

            // Create fresh workspace(s) for each iteration
            if include_bd {
                if let Some(metrics) =
                    self.run_benchmark_iteration_with_bd(scenario, iteration, is_warmup, &mut notes)
                {
                    all_runs.push(metrics);
                }
            } else if let Some(metrics) =
                self.run_benchmark_iteration_br_only(scenario, iteration, is_warmup, &mut notes)
            {
                all_runs.push(metrics);
            }
        }

        let completed_at = Utc::now().to_rfc3339();

        self.build_benchmark_summary(
            scenario,
            started_at,
            completed_at,
            total_iterations,
            include_bd,
            all_runs,
            notes,
        )
    }

    fn resolve_include_bd(&self, notes: &mut Vec<String>) -> bool {
        if !self.config.include_bd {
            return false;
        }

        match discover_binaries() {
            Ok(binaries) => {
                if let Some(bd) = binaries.bd {
                    if let Err(err) = check_bd_version(&bd) {
                        notes.push(format!("bd benchmarks disabled: {err}"));
                        false
                    } else {
                        true
                    }
                } else {
                    notes.push("bd benchmarks disabled: bd binary not found".to_string());
                    false
                }
            }
            Err(err) => {
                notes.push(format!("bd benchmarks disabled: {err}"));
                false
            }
        }
    }

    fn run_benchmark_iteration_with_bd(
        &self,
        scenario: &Scenario,
        iteration: u32,
        is_warmup: bool,
        notes: &mut Vec<String>,
    ) -> Option<BenchmarkMetrics> {
        let mut workspace = HarnessConformanceWorkspace::new(
            "benchmark",
            &format!("{}_{}", scenario.name, iteration),
        );

        if let ScenarioSetup::Dataset(dataset) = scenario.setup
            && let Err(err) = populate_conformance_with_dataset(&workspace, dataset)
        {
            notes.push(format!(
                "Dataset setup failed for iteration {iteration}: {err}"
            ));
            return None;
        }

        if matches!(scenario.setup, ScenarioSetup::Fresh) {
            let _ = workspace.init_both();
        }

        let setup_commands = collect_setup_commands(scenario);
        for cmd in &setup_commands {
            let label = format!("setup_{}", cmd.label);
            run_conformance_command(&mut workspace, cmd, &label, BinaryTarget::Br);
            run_conformance_command(&mut workspace, cmd, &label, BinaryTarget::Bd);
        }

        let br_result = run_conformance_command(
            &mut workspace,
            &scenario.test_command,
            &scenario.test_command.label,
            BinaryTarget::Br,
        );
        let br_duration_ms = br_result.duration.as_millis();

        let bd_result = run_conformance_command(
            &mut workspace,
            &scenario.test_command,
            &scenario.test_command.label,
            BinaryTarget::Bd,
        );
        let bd_duration_ms = bd_result.duration.as_millis();

        if !bd_result.success {
            notes.push(format!(
                "bd benchmark failed for iteration {iteration} (exit {})",
                bd_result.exit_code
            ));
        }

        let (db_size, jsonl_size) = if self.config.measure_io {
            measure_io_sizes(&workspace.br_workspace)
        } else {
            (None, None)
        };

        let speedup_ratio = if br_duration_ms > 0 {
            Some(bd_duration_ms as f64 / br_duration_ms as f64)
        } else {
            None
        };

        let metrics = BenchmarkMetrics {
            br_duration_ms,
            bd_duration_ms: Some(bd_duration_ms),
            speedup_ratio,
            br_peak_rss_bytes: None,
            bd_peak_rss_bytes: None,
            br_cpu_time_ms: None,
            bd_cpu_time_ms: None,
            db_size_bytes: db_size,
            jsonl_size_bytes: jsonl_size,
            iteration: Some(iteration),
            is_warmup,
        };

        workspace.finish(br_result.success && bd_result.success);
        Some(metrics)
    }

    fn run_benchmark_iteration_br_only(
        &self,
        scenario: &Scenario,
        iteration: u32,
        is_warmup: bool,
        notes: &mut Vec<String>,
    ) -> Option<BenchmarkMetrics> {
        let mut workspace =
            TestWorkspace::new("benchmark", &format!("{}_{}", scenario.name, iteration));

        if let ScenarioSetup::Dataset(dataset) = scenario.setup
            && let Err(err) = populate_workspace_with_dataset(&workspace, dataset)
        {
            notes.push(format!(
                "Dataset setup failed for iteration {iteration}: {err}"
            ));
            return None;
        }

        if matches!(scenario.setup, ScenarioSetup::Fresh) {
            let _ = workspace.init_br();
        }

        let setup_commands = collect_setup_commands(scenario);
        for cmd in &setup_commands {
            run_scenario_command(&mut workspace, cmd, None);
        }

        let br_result = run_scenario_command(&mut workspace, &scenario.test_command, None);
        let br_duration_ms = br_result.duration.as_millis();

        let (db_size, jsonl_size) = if self.config.measure_io {
            measure_io_sizes(&workspace.root)
        } else {
            (None, None)
        };

        let metrics = BenchmarkMetrics {
            br_duration_ms,
            bd_duration_ms: None,
            speedup_ratio: None,
            br_peak_rss_bytes: None,
            bd_peak_rss_bytes: None,
            br_cpu_time_ms: None,
            bd_cpu_time_ms: None,
            db_size_bytes: db_size,
            jsonl_size_bytes: jsonl_size,
            iteration: Some(iteration),
            is_warmup,
        };

        workspace.finish(br_result.success);
        Some(metrics)
    }

    #[allow(clippy::too_many_arguments)]
    fn build_benchmark_summary(
        &self,
        scenario: &Scenario,
        started_at: String,
        completed_at: String,
        total_iterations: u32,
        include_bd: bool,
        all_runs: Vec<BenchmarkMetrics>,
        mut notes: Vec<String>,
    ) -> BenchmarkSummary {
        // Separate warmup from measured runs
        let measured_runs: Vec<&BenchmarkMetrics> =
            all_runs.iter().filter(|m| !m.is_warmup).collect();

        // Compute statistics for measured runs only
        let br_durations: Vec<u128> = measured_runs.iter().map(|m| m.br_duration_ms).collect();
        let br_rss: Vec<Option<u64>> = measured_runs.iter().map(|m| m.br_peak_rss_bytes).collect();
        let br_stats = compute_statistics(&br_durations, &br_rss);

        let bd_durations: Vec<u128> = measured_runs
            .iter()
            .filter_map(|m| m.bd_duration_ms)
            .collect();
        let bd_stats = if include_bd && !bd_durations.is_empty() {
            let bd_rss: Vec<Option<u64>> = vec![None; bd_durations.len()];
            Some(compute_statistics(&bd_durations, &bd_rss))
        } else {
            if include_bd {
                notes.push("bd benchmarks produced no measurable runs".to_string());
            }
            None
        };

        let speedup_ratio = bd_stats.as_ref().and_then(|stats| {
            if br_stats.median_ms > 0 {
                Some(stats.median_ms as f64 / br_stats.median_ms as f64)
            } else {
                None
            }
        });

        // Check for high variance
        if br_stats.cv > 0.15 {
            notes.push(format!(
                "High variance detected (CV={:.2}%). Consider increasing iterations or reducing system load.",
                br_stats.cv * 100.0
            ));
        }

        // Build summary
        BenchmarkSummary {
            scenario: scenario.name.clone(),
            config: self.config.clone(),
            started_at,
            completed_at,
            total_runs: total_iterations,
            br_stats,
            bd_stats,
            speedup_ratio,
            runs: all_runs.into_iter().filter(|m| !m.is_warmup).collect(),
            notes,
        }
    }

    /// Run benchmark and write results to artifacts directory.
    pub fn run_and_save(&self, scenario: &Scenario) -> std::io::Result<BenchmarkSummary> {
        let summary = self.run_benchmark(scenario);

        if let Some(ref artifacts_dir) = self.artifacts_dir {
            use std::fs;
            use std::io::Write;

            // Ensure directory exists
            fs::create_dir_all(artifacts_dir)?;

            // Write summary JSON
            let summary_path = artifacts_dir.join(format!("{}_summary.json", scenario.name));
            let mut file = fs::File::create(&summary_path)?;
            let json = serde_json::to_string_pretty(&summary).map_err(std::io::Error::other)?;
            file.write_all(json.as_bytes())?;

            // Write per-run JSONL
            let runs_path = artifacts_dir.join(format!("{}_runs.jsonl", scenario.name));
            let mut runs_file = fs::File::create(&runs_path)?;
            for run in &summary.runs {
                let line = serde_json::to_string(run).map_err(std::io::Error::other)?;
                writeln!(runs_file, "{line}")?;
            }
        }

        Ok(summary)
    }
}

/// A test scenario that can be run in E2E, Conformance, or Benchmark mode.
#[derive(Debug, Clone)]
pub struct Scenario {
    /// Unique name for the scenario
    pub name: String,
    /// Human-readable description
    pub description: String,
    /// Tags for filtering (e.g., "quick", "sync", "crud")
    pub tags: Vec<String>,
    /// How to set up the workspace
    pub setup: ScenarioSetup,
    /// Commands to run before the main test command
    pub setup_commands: Vec<ScenarioCommand>,
    /// The main command to test
    pub test_command: ScenarioCommand,
    /// Expected invariants
    pub invariants: Invariants,
    /// How to compare outputs (for conformance mode)
    pub compare_mode: CompareMode,
    /// Normalization rules (for conformance mode)
    pub normalization: NormalizationRules,
    /// Supported execution modes
    pub supported_modes: Vec<ExecutionMode>,
}

impl Scenario {
    /// Create a new scenario with minimal configuration.
    pub fn new(name: impl Into<String>, test_command: ScenarioCommand) -> Self {
        Self {
            name: name.into(),
            description: String::new(),
            tags: Vec::new(),
            setup: ScenarioSetup::Fresh,
            setup_commands: Vec::new(),
            test_command,
            invariants: Invariants::success(),
            compare_mode: CompareMode::default(),
            normalization: NormalizationRules::conformance_default(),
            supported_modes: vec![ExecutionMode::E2E, ExecutionMode::Conformance],
        }
    }

    /// Builder: add description.
    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
        self.description = desc.into();
        self
    }

    /// Builder: add tags.
    pub fn with_tags<I, S>(mut self, tags: I) -> Self
    where
        I: IntoIterator<Item = S>,
        S: Into<String>,
    {
        self.tags = tags.into_iter().map(Into::into).collect();
        self
    }

    /// Builder: set setup mode.
    pub fn with_setup(mut self, setup: ScenarioSetup) -> Self {
        self.setup = setup;
        self
    }

    /// Builder: add setup commands.
    pub fn with_setup_commands(mut self, commands: Vec<ScenarioCommand>) -> Self {
        self.setup_commands = commands;
        self
    }

    /// Builder: set invariants.
    pub fn with_invariants(mut self, invariants: Invariants) -> Self {
        self.invariants = invariants;
        self
    }

    /// Builder: set compare mode.
    pub fn with_compare_mode(mut self, mode: CompareMode) -> Self {
        self.compare_mode = mode;
        self
    }

    /// Builder: set normalization rules.
    pub fn with_normalization(mut self, rules: NormalizationRules) -> Self {
        self.normalization = rules;
        self
    }

    /// Builder: set supported modes.
    pub fn with_modes(mut self, modes: Vec<ExecutionMode>) -> Self {
        self.supported_modes = modes;
        self
    }

    /// Check if scenario supports a given mode.
    pub fn supports_mode(&self, mode: ExecutionMode) -> bool {
        self.supported_modes.contains(&mode)
    }

    /// Check if scenario has a given tag.
    pub fn has_tag(&self, tag: &str) -> bool {
        self.tags.iter().any(|t| t == tag)
    }

    /// Check if scenario has any of the given tags.
    pub fn has_any_tag(&self, tags: &[String]) -> bool {
        tags.iter().any(|t| self.has_tag(t))
    }

    /// Check if scenario has all of the given tags.
    pub fn has_all_tags(&self, tags: &[String]) -> bool {
        tags.iter().all(|t| self.has_tag(t))
    }
}

// ============================================================================
// SCENARIO FILTER (beads_rust-o1az)
// ============================================================================

/// Filter for selecting scenarios by tags.
///
/// Supports include/exclude logic:
/// - If `include_tags` is non-empty, only scenarios with matching tags are selected
/// - If `exclude_tags` is non-empty, scenarios with any excluded tag are skipped
/// - Exclude takes precedence over include
///
/// Environment variables:
/// - `HARNESS_TAGS`: comma-separated list of tags to include (e.g., "quick,crud")
/// - `HARNESS_EXCLUDE_TAGS`: comma-separated list of tags to exclude (e.g., "slow,stress")
/// - `HARNESS_TAG_MATCH`: "any" (default) or "all" for include matching
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ScenarioFilter {
    /// Tags to include (scenario must have at least one, or all if `match_mode` is All)
    pub include_tags: Vec<String>,
    /// Tags to exclude (scenario must not have any)
    pub exclude_tags: Vec<String>,
    /// Match mode for include tags: "any" (default) or "all"
    pub match_mode: TagMatchMode,
}

/// How to match include tags.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TagMatchMode {
    /// Scenario matches if it has ANY of the include tags (default)
    #[default]
    Any,
    /// Scenario matches if it has ALL of the include tags
    All,
}

impl ScenarioFilter {
    /// Create a new empty filter (matches all scenarios).
    pub fn new() -> Self {
        Self::default()
    }

    /// Create a filter from environment variables.
    ///
    /// Reads:
    /// - `HARNESS_TAGS`: comma-separated include tags
    /// - `HARNESS_EXCLUDE_TAGS`: comma-separated exclude tags
    /// - `HARNESS_TAG_MATCH`: "any" or "all"
    pub fn from_env() -> Self {
        let include_tags = std::env::var("HARNESS_TAGS")
            .ok()
            .map(|s| {
                s.split(',')
                    .map(|t| t.trim().to_string())
                    .filter(|t| !t.is_empty())
                    .collect()
            })
            .unwrap_or_default();

        let exclude_tags = std::env::var("HARNESS_EXCLUDE_TAGS")
            .ok()
            .map(|s| {
                s.split(',')
                    .map(|t| t.trim().to_string())
                    .filter(|t| !t.is_empty())
                    .collect()
            })
            .unwrap_or_default();

        let match_mode = std::env::var("HARNESS_TAG_MATCH")
            .ok()
            .map(|s| match s.to_lowercase().as_str() {
                "all" => TagMatchMode::All,
                _ => TagMatchMode::Any,
            })
            .unwrap_or_default();

        Self {
            include_tags,
            exclude_tags,
            match_mode,
        }
    }

    /// Builder: add include tags.
    pub fn with_include_tags<I, S>(mut self, tags: I) -> Self
    where
        I: IntoIterator<Item = S>,
        S: Into<String>,
    {
        self.include_tags = tags.into_iter().map(Into::into).collect();
        self
    }

    /// Builder: add exclude tags.
    pub fn with_exclude_tags<I, S>(mut self, tags: I) -> Self
    where
        I: IntoIterator<Item = S>,
        S: Into<String>,
    {
        self.exclude_tags = tags.into_iter().map(Into::into).collect();
        self
    }

    /// Builder: set match mode.
    pub const fn with_match_mode(mut self, mode: TagMatchMode) -> Self {
        self.match_mode = mode;
        self
    }

    /// Check if a scenario matches this filter.
    pub fn matches(&self, scenario: &Scenario) -> bool {
        // Check exclude tags first (takes precedence)
        for tag in &self.exclude_tags {
            if scenario.has_tag(tag) {
                return false;
            }
        }

        // If no include tags specified, match all (that weren't excluded)
        if self.include_tags.is_empty() {
            return true;
        }

        // Check include tags based on match mode
        match self.match_mode {
            TagMatchMode::Any => scenario.has_any_tag(&self.include_tags),
            TagMatchMode::All => scenario.has_all_tags(&self.include_tags),
        }
    }

    /// Filter a list of scenarios, returning only those that match.
    pub fn filter<'a>(&self, scenarios: &'a [Scenario]) -> Vec<&'a Scenario> {
        scenarios.iter().filter(|s| self.matches(s)).collect()
    }

    /// Check if filter is empty (matches all scenarios).
    pub fn is_empty(&self) -> bool {
        self.include_tags.is_empty() && self.exclude_tags.is_empty()
    }

    /// Get a human-readable description of the filter.
    pub fn description(&self) -> String {
        let mut parts = Vec::new();

        if !self.include_tags.is_empty() {
            let mode = match self.match_mode {
                TagMatchMode::Any => "any of",
                TagMatchMode::All => "all of",
            };
            parts.push(format!(
                "include {} [{}]",
                mode,
                self.include_tags.join(", ")
            ));
        }

        if !self.exclude_tags.is_empty() {
            parts.push(format!("exclude [{}]", self.exclude_tags.join(", ")));
        }

        if parts.is_empty() {
            "all scenarios".to_string()
        } else {
            parts.join("; ")
        }
    }

    /// Convert to JSON for logging in summary.
    pub fn to_json(&self) -> serde_json::Value {
        serde_json::json!({
            "include_tags": self.include_tags,
            "exclude_tags": self.exclude_tags,
            "match_mode": self.match_mode,
            "description": self.description(),
        })
    }
}

/// Scenario runner that executes scenarios in different modes.
pub struct ScenarioRunner {
    mode: ExecutionMode,
    artifacts_enabled: bool,
    filter: ScenarioFilter,
}

impl ScenarioRunner {
    pub fn new(mode: ExecutionMode) -> Self {
        Self {
            mode,
            artifacts_enabled: std::env::var("HARNESS_ARTIFACTS").is_ok_and(|v| v == "1"),
            filter: ScenarioFilter::from_env(),
        }
    }

    pub const fn with_artifacts(mut self, enabled: bool) -> Self {
        self.artifacts_enabled = enabled;
        self
    }

    /// Builder: set the scenario filter.
    pub fn with_filter(mut self, filter: ScenarioFilter) -> Self {
        self.filter = filter;
        self
    }

    /// Get the current filter.
    pub const fn filter(&self) -> &ScenarioFilter {
        &self.filter
    }

    /// Check if a scenario should be run based on the filter.
    pub fn should_run(&self, scenario: &Scenario) -> bool {
        self.filter.matches(scenario)
    }

    /// Run multiple scenarios, filtering based on current filter.
    /// Returns results with filter info in logs.
    pub fn run_filtered(&self, scenarios: &[Scenario]) -> Vec<(String, ScenarioResult)> {
        let selected: Vec<_> = scenarios.iter().filter(|s| self.should_run(s)).collect();

        // Log filter selection
        if !self.filter.is_empty() {
            eprintln!(
                "Filter: {} ({}/{} scenarios selected)",
                self.filter.description(),
                selected.len(),
                scenarios.len()
            );
        }

        selected
            .into_iter()
            .map(|s| (s.name.clone(), self.run(s)))
            .collect()
    }

    /// Run a scenario and return the result.
    pub fn run(&self, scenario: &Scenario) -> ScenarioResult {
        if !scenario.supports_mode(self.mode) {
            return ScenarioResult {
                passed: false,
                mode: self.mode,
                br_result: None,
                bd_result: None,
                comparison_result: None,
                invariant_failures: vec![format!(
                    "Scenario {} does not support mode {:?}",
                    scenario.name, self.mode
                )],
                normalization_log: Vec::new(),
                benchmark_metrics: None,
            };
        }

        match self.mode {
            ExecutionMode::E2E => self.run_e2e(scenario),
            ExecutionMode::Conformance => self.run_conformance(scenario),
            ExecutionMode::Benchmark => self.run_benchmark(scenario),
        }
    }

    fn run_e2e(&self, scenario: &Scenario) -> ScenarioResult {
        let mut workspace = TestWorkspace::new("e2e", &scenario.name);

        if let ScenarioSetup::Dataset(dataset) = scenario.setup
            && let Err(err) = populate_workspace_with_dataset(&workspace, dataset)
        {
            return ScenarioResult {
                passed: false,
                mode: self.mode,
                br_result: None,
                bd_result: None,
                comparison_result: None,
                invariant_failures: vec![format!("Dataset setup failed: {err}")],
                normalization_log: Vec::new(),
                benchmark_metrics: None,
            };
        }

        let baseline_snapshot = if scenario.invariants.path_confinement {
            Some(snapshot_workspace(&workspace.root))
        } else {
            None
        };

        // Setup
        if matches!(scenario.setup, ScenarioSetup::Fresh) {
            let init = workspace.init_br();
            if !init.success {
                return ScenarioResult {
                    passed: false,
                    mode: self.mode,
                    br_result: Some(init),
                    bd_result: None,
                    comparison_result: None,
                    invariant_failures: vec!["Init failed".to_string()],
                    normalization_log: Vec::new(),
                    benchmark_metrics: None,
                };
            }
        }

        // Run setup commands
        let setup_commands = collect_setup_commands(scenario);
        for cmd in &setup_commands {
            let result = run_scenario_command(&mut workspace, cmd, None);
            if !result.success && scenario.invariants.expect_success {
                return ScenarioResult {
                    passed: false,
                    mode: self.mode,
                    br_result: Some(result),
                    bd_result: None,
                    comparison_result: None,
                    invariant_failures: vec![format!("Setup command {} failed", cmd.label)],
                    normalization_log: Vec::new(),
                    benchmark_metrics: None,
                };
            }
        }

        // Run test command
        let br_result = run_scenario_command(&mut workspace, &scenario.test_command, None);

        // Check invariants
        let mut invariant_failures = check_invariants(&scenario.invariants, &br_result);
        if let (true, Some(before)) = (scenario.invariants.path_confinement, baseline_snapshot) {
            let after = snapshot_workspace(&workspace.root);
            let violations = detect_path_confinement_violations(&before, &after);
            invariant_failures.extend(violations);
        }

        let passed = invariant_failures.is_empty();
        workspace.finish(passed);

        ScenarioResult {
            passed,
            mode: self.mode,
            br_result: Some(br_result),
            bd_result: None,
            comparison_result: None,
            invariant_failures,
            normalization_log: Vec::new(),
            benchmark_metrics: None,
        }
    }

    #[allow(clippy::too_many_lines)]
    fn run_conformance(&self, scenario: &Scenario) -> ScenarioResult {
        let mut workspace = HarnessConformanceWorkspace::new("conformance", &scenario.name);

        // Initialize both (unless using a dataset)
        if matches!(scenario.setup, ScenarioSetup::Fresh) {
            let (br_init, bd_init) = workspace.init_both();
            if !br_init.success || !bd_init.success {
                return ScenarioResult {
                    passed: false,
                    mode: self.mode,
                    br_result: Some(br_init),
                    bd_result: Some(bd_init),
                    comparison_result: None,
                    invariant_failures: vec!["Init failed".to_string()],
                    normalization_log: Vec::new(),
                    benchmark_metrics: None,
                };
            }
        } else if let ScenarioSetup::Dataset(dataset) = scenario.setup
            && let Err(err) = populate_conformance_with_dataset(&workspace, dataset)
        {
            return ScenarioResult {
                passed: false,
                mode: self.mode,
                br_result: None,
                bd_result: None,
                comparison_result: None,
                invariant_failures: vec![format!("Dataset setup failed: {err}")],
                normalization_log: Vec::new(),
                benchmark_metrics: None,
            };
        }

        let baseline_snapshot = if scenario.invariants.path_confinement {
            Some(snapshot_workspace(&workspace.br_workspace))
        } else {
            None
        };

        // Run setup commands on both
        let setup_commands = collect_setup_commands(scenario);
        for cmd in &setup_commands {
            let br_setup = run_conformance_command(
                &mut workspace,
                cmd,
                &format!("{}_setup", cmd.label),
                BinaryTarget::Br,
            );
            let bd_setup = run_conformance_command(
                &mut workspace,
                cmd,
                &format!("{}_setup", cmd.label),
                BinaryTarget::Bd,
            );
            if !br_setup.success || !bd_setup.success {
                return ScenarioResult {
                    passed: false,
                    mode: self.mode,
                    br_result: Some(br_setup),
                    bd_result: Some(bd_setup),
                    comparison_result: None,
                    invariant_failures: vec![format!("Setup command {} failed", cmd.label)],
                    normalization_log: Vec::new(),
                    benchmark_metrics: None,
                };
            }
        }

        // Run test command on both
        let br_result = run_conformance_command(
            &mut workspace,
            &scenario.test_command,
            &scenario.test_command.label,
            BinaryTarget::Br,
        );
        let bd_result = run_conformance_command(
            &mut workspace,
            &scenario.test_command,
            &scenario.test_command.label,
            BinaryTarget::Bd,
        );

        // Compare outputs
        let (comparison_result, normalization_log) = compare_outputs(
            &br_result,
            &bd_result,
            &scenario.compare_mode,
            &scenario.normalization,
        );

        // Check invariants (on br only)
        let mut invariant_failures = check_invariants(&scenario.invariants, &br_result);
        if let (true, Some(before)) = (scenario.invariants.path_confinement, baseline_snapshot) {
            let after = snapshot_workspace(&workspace.br_workspace);
            let violations = detect_path_confinement_violations(&before, &after);
            invariant_failures.extend(violations);
        }

        // Add comparison failure if any
        if !comparison_result.matched {
            invariant_failures.push(
                comparison_result
                    .diff_description
                    .clone()
                    .unwrap_or_else(|| "Output mismatch".to_string()),
            );
        }

        let passed = invariant_failures.is_empty();
        workspace.finish(passed);

        ScenarioResult {
            passed,
            mode: self.mode,
            br_result: Some(br_result),
            bd_result: Some(bd_result),
            comparison_result: Some(comparison_result),
            invariant_failures,
            normalization_log,
            benchmark_metrics: None,
        }
    }

    fn run_benchmark(&self, scenario: &Scenario) -> ScenarioResult {
        // For now, benchmark mode is like E2E but captures timing metrics
        let mut workspace = TestWorkspace::new("benchmark", &scenario.name);

        if let ScenarioSetup::Dataset(dataset) = scenario.setup
            && let Err(err) = populate_workspace_with_dataset(&workspace, dataset)
        {
            return ScenarioResult {
                passed: false,
                mode: self.mode,
                br_result: None,
                bd_result: None,
                comparison_result: None,
                invariant_failures: vec![format!("Dataset setup failed: {err}")],
                normalization_log: Vec::new(),
                benchmark_metrics: None,
            };
        }

        if matches!(scenario.setup, ScenarioSetup::Fresh) {
            let _ = workspace.init_br();
        }

        let setup_commands = collect_setup_commands(scenario);
        for cmd in &setup_commands {
            let _ = run_scenario_command(&mut workspace, cmd, None);
        }

        let br_result = run_scenario_command(&mut workspace, &scenario.test_command, None);

        let benchmark_metrics = Some(BenchmarkMetrics {
            br_duration_ms: br_result.duration.as_millis(),
            bd_duration_ms: None,
            speedup_ratio: None,
            br_peak_rss_bytes: None,
            bd_peak_rss_bytes: None,
            br_cpu_time_ms: None,
            bd_cpu_time_ms: None,
            db_size_bytes: None,
            jsonl_size_bytes: None,
            iteration: None,
            is_warmup: false,
        });

        workspace.finish(br_result.success);

        ScenarioResult {
            passed: br_result.success,
            mode: self.mode,
            br_result: Some(br_result),
            bd_result: None,
            comparison_result: None,
            invariant_failures: Vec::new(),
            normalization_log: Vec::new(),
            benchmark_metrics,
        }
    }
}

/// Check invariants against a command result.
fn check_invariants(invariants: &Invariants, result: &CommandResult) -> Vec<String> {
    let mut failures = Vec::new();

    if invariants.expect_success && !result.success {
        failures.push(format!(
            "Expected success, got exit code {}",
            result.exit_code
        ));
    }

    if invariants.expect_failure && result.success {
        failures.push("Expected failure, but command succeeded".to_string());
    }

    if let Some(expected_code) = invariants.expected_exit_code
        && result.exit_code != expected_code
    {
        failures.push(format!(
            "Expected exit code {}, got {}",
            expected_code, result.exit_code
        ));
    }

    for needle in &invariants.stdout_contains {
        if !result.stdout.contains(needle) {
            failures.push(format!("stdout missing: {needle}"));
        }
    }

    for needle in &invariants.stderr_contains {
        if !result.stderr.contains(needle) {
            failures.push(format!("stderr missing: {needle}"));
        }
    }

    for needle in &invariants.stdout_excludes {
        if result.stdout.contains(needle) {
            failures.push(format!("stdout should not contain: {needle}"));
        }
    }

    if !invariants.json_schema_fields.is_empty() {
        let payload = extract_json_payload(&result.stdout);
        match serde_json::from_str::<Value>(&payload) {
            Ok(value) => {
                for field in &invariants.json_schema_fields {
                    if value.get(field).is_none() {
                        failures.push(format!("json missing field: {field}"));
                    }
                }
            }
            Err(err) => failures.push(format!("json parse error: {err}")),
        }
    }

    if invariants.no_git_ops {
        // Check for git command patterns in logs
        let git_patterns = ["git commit", "git push", "git add", "git checkout"];
        for pattern in git_patterns {
            if result.stderr.contains(pattern) || result.stdout.contains(pattern) {
                failures.push(format!("Unexpected git operation: {pattern}"));
            }
        }
    }

    failures
}

/// Compare br and bd outputs using the specified mode and normalization.
#[allow(clippy::too_many_lines)]
fn compare_outputs(
    br_result: &CommandResult,
    bd_result: &CommandResult,
    compare_mode: &CompareMode,
    normalization: &NormalizationRules,
) -> (ComparisonResult, Vec<String>) {
    let mut normalization_log = Vec::new();

    // Extract JSON payloads
    let br_json_str = extract_json_payload(&br_result.stdout);
    let bd_json_str = extract_json_payload(&bd_result.stdout);

    let br_json: Result<Value, _> = serde_json::from_str(&br_json_str);
    let bd_json: Result<Value, _> = serde_json::from_str(&bd_json_str);

    match compare_mode {
        CompareMode::ExitCodeOnly => {
            let matched = br_result.exit_code == bd_result.exit_code;
            (
                ComparisonResult {
                    matched,
                    br_json: None,
                    bd_json: None,
                    diff_description: if matched {
                        None
                    } else {
                        Some(format!(
                            "Exit code mismatch: br={}, bd={}",
                            br_result.exit_code, bd_result.exit_code
                        ))
                    },
                },
                normalization_log,
            )
        }

        CompareMode::ExactJson => {
            let (Ok(br), Ok(bd)) = (br_json, bd_json) else {
                return (
                    ComparisonResult {
                        matched: false,
                        br_json: None,
                        bd_json: None,
                        diff_description: Some("JSON parse error".to_string()),
                    },
                    normalization_log,
                );
            };
            let matched = br == bd;
            (
                ComparisonResult {
                    matched,
                    br_json: Some(br),
                    bd_json: Some(bd),
                    diff_description: if matched {
                        None
                    } else {
                        Some("JSON mismatch".to_string())
                    },
                },
                normalization_log,
            )
        }

        CompareMode::NormalizedJson => {
            let (Ok(mut br), Ok(mut bd)) = (br_json, bd_json) else {
                return (
                    ComparisonResult {
                        matched: false,
                        br_json: None,
                        bd_json: None,
                        diff_description: Some("JSON parse error".to_string()),
                    },
                    normalization_log,
                );
            };

            let mut tolerance_issues = Vec::new();
            if normalization.timestamp_tolerance.is_some() {
                tolerance_issues = check_timestamp_tolerance(&br, &bd, normalization);
                normalization_log.extend(tolerance_issues.iter().cloned());
            }

            // Apply normalization
            let br_log = normalization.apply(&mut br);
            let bd_log = normalization.apply(&mut bd);
            normalization_log.extend(br_log.into_iter().map(|s| format!("br: {s}")));
            normalization_log.extend(bd_log.into_iter().map(|s| format!("bd: {s}")));

            // For NormalizedJson mode, timestamps are masked so tolerance issues
            // are logged for visibility but don't affect the match result.
            // The normalized values are what matter.
            let matched = br == bd;
            (
                ComparisonResult {
                    matched,
                    br_json: Some(br),
                    bd_json: Some(bd),
                    diff_description: if matched {
                        // Log tolerance info even on match for debugging visibility
                        if tolerance_issues.is_empty() {
                            None
                        } else {
                            Some(format!(
                                "Note: timestamp drift detected (masked): {}",
                                tolerance_issues.join("; ")
                            ))
                        }
                    } else {
                        Some("Normalized JSON mismatch".to_string())
                    },
                },
                normalization_log,
            )
        }

        CompareMode::ContainsFields(fields) => {
            let (Ok(br), Ok(bd)) = (br_json, bd_json) else {
                return (
                    ComparisonResult {
                        matched: false,
                        br_json: None,
                        bd_json: None,
                        diff_description: Some("JSON parse error".to_string()),
                    },
                    normalization_log,
                );
            };

            let mut mismatches = Vec::new();
            for field in fields {
                let br_val = br.get(field);
                let bd_val = bd.get(field);
                if br_val != bd_val {
                    mismatches.push(format!("Field '{field}': br={br_val:?}, bd={bd_val:?}"));
                }
            }

            let matched = mismatches.is_empty();
            (
                ComparisonResult {
                    matched,
                    br_json: Some(br),
                    bd_json: Some(bd),
                    diff_description: if matched {
                        None
                    } else {
                        Some(mismatches.join("; "))
                    },
                },
                normalization_log,
            )
        }

        CompareMode::ArrayUnordered => {
            let (Ok(mut br), Ok(mut bd)) = (br_json, bd_json) else {
                return (
                    ComparisonResult {
                        matched: false,
                        br_json: None,
                        bd_json: None,
                        diff_description: Some("JSON parse error".to_string()),
                    },
                    normalization_log,
                );
            };
            let _ = normalization.apply(&mut br);
            let _ = normalization.apply(&mut bd);
            sort_arrays_recursively(&mut br);
            sort_arrays_recursively(&mut bd);
            let matched = br == bd;
            (
                ComparisonResult {
                    matched,
                    br_json: Some(br),
                    bd_json: Some(bd),
                    diff_description: if matched {
                        None
                    } else {
                        Some("Array-unordered JSON mismatch".to_string())
                    },
                },
                normalization_log,
            )
        }

        CompareMode::FieldsExcluded(fields) => {
            let (Ok(mut br), Ok(mut bd)) = (br_json, bd_json) else {
                return (
                    ComparisonResult {
                        matched: false,
                        br_json: None,
                        bd_json: None,
                        diff_description: Some("JSON parse error".to_string()),
                    },
                    normalization_log,
                );
            };
            for field in fields {
                remove_field_path(&mut br, field);
                remove_field_path(&mut bd, field);
            }
            let matched = br == bd;
            (
                ComparisonResult {
                    matched,
                    br_json: Some(br),
                    bd_json: Some(bd),
                    diff_description: if matched {
                        None
                    } else {
                        Some("Fields-excluded JSON mismatch".to_string())
                    },
                },
                normalization_log,
            )
        }

        CompareMode::StructureOnly => {
            let (Ok(br), Ok(bd)) = (br_json, bd_json) else {
                return (
                    ComparisonResult {
                        matched: false,
                        br_json: None,
                        bd_json: None,
                        diff_description: Some("JSON parse error".to_string()),
                    },
                    normalization_log,
                );
            };
            let br_shape = structure_only(&br);
            let bd_shape = structure_only(&bd);
            let matched = br_shape == bd_shape;
            (
                ComparisonResult {
                    matched,
                    br_json: Some(br_shape),
                    bd_json: Some(bd_shape),
                    diff_description: if matched {
                        None
                    } else {
                        Some("Structure mismatch".to_string())
                    },
                },
                normalization_log,
            )
        }
    }
}

/// Extract JSON payload from stdout (skips non-JSON preamble).
fn extract_json_payload(stdout: &str) -> String {
    for (idx, line) in stdout.lines().enumerate() {
        let trimmed = line.trim_start();
        if trimmed.starts_with('[') || trimmed.starts_with('{') {
            return stdout
                .lines()
                .skip(idx)
                .collect::<Vec<_>>()
                .join("\n")
                .trim()
                .to_string();
        }
    }
    stdout.trim().to_string()
}

#[derive(Debug, Clone)]
struct FileFingerprint {
    size: u64,
    modified: Option<SystemTime>,
    is_dir: bool,
}

fn snapshot_workspace(root: &Path) -> HashMap<String, FileFingerprint> {
    let mut entries = HashMap::new();
    for entry in WalkDir::new(root)
        .max_depth(6)
        .into_iter()
        .filter_map(Result::ok)
    {
        let path = entry.path();
        if let Ok(rel) = path.strip_prefix(root) {
            let rel_str = rel.display().to_string();
            if rel_str.is_empty() {
                continue;
            }
            let metadata = entry.metadata().ok();
            let is_dir = entry.file_type().is_dir();
            let size = metadata.as_ref().map_or(0, std::fs::Metadata::len);
            let modified = metadata.and_then(|m| m.modified().ok());
            entries.insert(
                rel_str,
                FileFingerprint {
                    size,
                    modified,
                    is_dir,
                },
            );
        }
    }
    entries
}

fn detect_path_confinement_violations(
    before: &HashMap<String, FileFingerprint>,
    after: &HashMap<String, FileFingerprint>,
) -> Vec<String> {
    let allowed_prefixes = [".beads/", "logs/"];
    let mut violations = Vec::new();

    let mut all_paths: HashSet<String> = HashSet::new();
    all_paths.extend(before.keys().cloned());
    all_paths.extend(after.keys().cloned());

    for path in all_paths {
        if allowed_prefixes.iter().any(|p| path.starts_with(p)) {
            continue;
        }
        match (before.get(&path), after.get(&path)) {
            (None, Some(_)) => violations.push(format!("Unexpected path created: {path}")),
            (Some(_), None) => violations.push(format!("Unexpected path removed: {path}")),
            (Some(before_meta), Some(after_meta)) => {
                if before_meta.is_dir || after_meta.is_dir {
                    continue;
                }
                if before_meta.size != after_meta.size
                    || before_meta.modified != after_meta.modified
                {
                    violations.push(format!("Unexpected path modified: {path}"));
                }
            }
            (None, None) => {}
        }
    }

    violations
}

fn collect_setup_commands(scenario: &Scenario) -> Vec<ScenarioCommand> {
    let mut commands = Vec::new();
    if let ScenarioSetup::Commands(cmds) = &scenario.setup {
        commands.extend(cmds.clone());
    }
    commands.extend(scenario.setup_commands.clone());
    commands
}

fn run_scenario_command(
    workspace: &mut TestWorkspace,
    command: &ScenarioCommand,
    label_suffix: Option<&str>,
) -> CommandResult {
    let label = label_suffix.map_or_else(
        || command.label.clone(),
        |suffix| format!("{}_{}", command.label, suffix),
    );

    match (command.env.is_empty(), command.stdin.as_ref()) {
        (true, None) => workspace.run_br(&command.args, &label),
        (false, None) => workspace.run_br_env(&command.args, command.env.clone(), &label),
        (true, Some(input)) => workspace.run_br_stdin(&command.args, input, &label),
        (false, Some(input)) => {
            workspace.run_br_env_stdin(&command.args, command.env.clone(), input, &label)
        }
    }
}

fn execute_workspace_evolution_plan(
    plan: &WorkspaceEvolutionPlan,
    workspace: &mut TestWorkspace,
    snapshot_dir: &TempDir,
) -> std::io::Result<(Vec<WorkspaceEvolutionEvent>, HashMap<String, String>)> {
    prepare_workspace_evolution(plan, workspace)?;

    let mut aliases = HashMap::new();
    let mut events = Vec::with_capacity(plan.steps.len());
    let mut snapshots = HashMap::new();

    run_workspace_evolution_setup_commands(plan, workspace, &mut aliases, &mut events)?;

    for step in &plan.steps {
        let event = execute_workspace_evolution_step(
            workspace,
            step,
            snapshot_dir,
            &mut aliases,
            &mut snapshots,
        )?;
        events.push(event);
    }

    Ok((events, aliases))
}

fn prepare_workspace_evolution(
    plan: &WorkspaceEvolutionPlan,
    workspace: &mut TestWorkspace,
) -> std::io::Result<()> {
    if let ScenarioSetup::Dataset(dataset) = &plan.setup {
        populate_workspace_with_dataset(workspace, *dataset)?;
    }

    if matches!(&plan.setup, ScenarioSetup::Fresh) {
        let init = workspace.init_br();
        if !init.success {
            return Err(std::io::Error::other(format!(
                "workspace evolution init failed: {}",
                init.stderr
            )));
        }
    }

    Ok(())
}

fn run_workspace_evolution_setup_commands(
    plan: &WorkspaceEvolutionPlan,
    workspace: &mut TestWorkspace,
    aliases: &mut HashMap<String, String>,
    events: &mut Vec<WorkspaceEvolutionEvent>,
) -> std::io::Result<()> {
    if let ScenarioSetup::Commands(commands) = &plan.setup {
        for command in commands {
            let evolution_command = WorkspaceEvolutionCommand::new(command.clone());
            let event =
                execute_workspace_evolution_command(workspace, &evolution_command, aliases)?;
            events.push(event);
        }
    }

    Ok(())
}

fn execute_workspace_evolution_step(
    workspace: &mut TestWorkspace,
    step: &WorkspaceEvolutionStep,
    snapshot_dir: &TempDir,
    aliases: &mut HashMap<String, String>,
    snapshots: &mut HashMap<String, PathBuf>,
) -> std::io::Result<WorkspaceEvolutionEvent> {
    match step {
        WorkspaceEvolutionStep::Command(command) => {
            execute_workspace_evolution_command(workspace, command, aliases)
        }
        WorkspaceEvolutionStep::Snapshot { label } => {
            let snapshot_root = snapshot_dir.path().join(label);
            copy_workspace_state(&workspace.root, &snapshot_root)?;
            snapshots.insert(label.clone(), snapshot_root);
            Ok(workspace_evolution_event(
                label,
                WorkspaceEvolutionEventKind::Snapshot,
                "captured workspace snapshot",
            ))
        }
        WorkspaceEvolutionStep::AppendToIssuesJsonl { label, text } => {
            append_text_to_issues_jsonl(&workspace.beads_dir, text)?;
            Ok(workspace_evolution_event(
                label,
                WorkspaceEvolutionEventKind::Mutation,
                "appended raw text to .beads/issues.jsonl",
            ))
        }
        WorkspaceEvolutionStep::RestoreSnapshot {
            label,
            snapshot_label,
        } => {
            let snapshot_root = snapshots.get(snapshot_label).ok_or_else(|| {
                std::io::Error::new(
                    std::io::ErrorKind::NotFound,
                    format!("workspace snapshot `{snapshot_label}` not found for step `{label}`"),
                )
            })?;
            copy_workspace_state(snapshot_root, &workspace.root)?;
            Ok(workspace_evolution_event(
                label,
                WorkspaceEvolutionEventKind::Restore,
                format!("restored workspace snapshot `{snapshot_label}`"),
            ))
        }
    }
}

fn workspace_evolution_event(
    label: &str,
    kind: WorkspaceEvolutionEventKind,
    detail: impl Into<String>,
) -> WorkspaceEvolutionEvent {
    WorkspaceEvolutionEvent {
        label: label.to_string(),
        kind,
        matched_expectation: true,
        command_result: None,
        detail: Some(detail.into()),
    }
}

fn materialize_workspace_evolution(
    plan: &WorkspaceEvolutionPlan,
    workspace: &TestWorkspace,
    events: &mut [WorkspaceEvolutionEvent],
    aliases: HashMap<String, String>,
) -> std::io::Result<MaterializedWorkspaceEvolution> {
    let materialized_temp_dir = TempDir::new()?;
    let materialized_root = materialized_temp_dir.path().to_path_buf();
    copy_workspace_state(&workspace.root, &materialized_root)?;
    copy_dir_contents(workspace.root.join("logs"), materialized_root.join("logs"))?;
    remap_workspace_evolution_logs(events, &workspace.root, &materialized_root);

    Ok(MaterializedWorkspaceEvolution {
        temp_dir: materialized_temp_dir,
        root: materialized_root.clone(),
        beads_dir: materialized_root.join(".beads"),
        plan: plan.clone(),
        events: events.to_vec(),
        aliases,
    })
}

fn execute_workspace_evolution_command(
    workspace: &mut TestWorkspace,
    command: &WorkspaceEvolutionCommand,
    aliases: &mut HashMap<String, String>,
) -> std::io::Result<WorkspaceEvolutionEvent> {
    let resolved = resolve_workspace_evolution_command(command, aliases);
    let result = run_scenario_command(workspace, &resolved, None);
    let matched_expectation = command_matches_expectation(command.expected, &result);

    if !matched_expectation {
        return Err(std::io::Error::other(format!(
            "workspace evolution step `{}` expected {:?}, got exit {}",
            command.command.label, command.expected, result.exit_code
        )));
    }

    let mut detail = None;
    if let Some(alias) = &command.capture_issue_id_as {
        let id = extract_issue_id_from_output(&result.stdout).ok_or_else(|| {
            std::io::Error::other(format!(
                "workspace evolution step `{}` did not emit an issue id for alias `{alias}`",
                command.command.label
            ))
        })?;
        aliases.insert(alias.clone(), id.clone());
        detail = Some(format!("captured `{alias}` as `{id}`"));
    }

    Ok(WorkspaceEvolutionEvent {
        label: command.command.label.clone(),
        kind: WorkspaceEvolutionEventKind::Command,
        matched_expectation: true,
        command_result: Some(result),
        detail,
    })
}

fn resolve_workspace_evolution_command(
    command: &WorkspaceEvolutionCommand,
    aliases: &HashMap<String, String>,
) -> ScenarioCommand {
    ScenarioCommand {
        args: command
            .command
            .args
            .iter()
            .map(|arg| apply_aliases(arg, aliases))
            .collect(),
        env: command
            .command
            .env
            .iter()
            .map(|(key, value)| (apply_aliases(key, aliases), apply_aliases(value, aliases)))
            .collect(),
        stdin: command
            .command
            .stdin
            .as_ref()
            .map(|input| apply_aliases(input, aliases)),
        label: command.command.label.clone(),
    }
}

fn apply_aliases(value: &str, aliases: &HashMap<String, String>) -> String {
    let mut resolved = value.to_string();
    let mut keys: Vec<_> = aliases.keys().collect();
    keys.sort_unstable();

    for key in keys {
        if let Some(replacement) = aliases.get(key) {
            resolved = resolved.replace(&format!("{{{key}}}"), replacement);
        }
    }

    resolved
}

fn command_matches_expectation(expected: ExpectedCommandOutcome, result: &CommandResult) -> bool {
    match expected {
        ExpectedCommandOutcome::Success => result.success,
        ExpectedCommandOutcome::Failure => !result.success,
        ExpectedCommandOutcome::Either => true,
    }
}

fn extract_issue_id_from_output(output: &str) -> Option<String> {
    let payload = extract_json_payload(output);
    if let Ok(value) = serde_json::from_str::<Value>(&payload) {
        if let Some(id) = value.get("id").and_then(Value::as_str) {
            return Some(id.to_string());
        }

        if let Some(array) = value.as_array()
            && let Some(first) = array.first()
            && let Some(id) = first.get("id").and_then(Value::as_str)
        {
            return Some(id.to_string());
        }
    }

    None
}

fn append_text_to_issues_jsonl(beads_dir: &Path, text: &str) -> std::io::Result<()> {
    let jsonl_path = beads_dir.join("issues.jsonl");
    let needs_newline = std::fs::read_to_string(&jsonl_path)
        .ok()
        .is_some_and(|content| !content.is_empty() && !content.ends_with('\n'));

    let mut file = std::fs::OpenOptions::new()
        .create(true)
        .append(true)
        .open(&jsonl_path)?;

    if needs_newline {
        writeln!(file)?;
    }

    write!(file, "{text}")?;
    if !text.ends_with('\n') {
        writeln!(file)?;
    }

    Ok(())
}

fn copy_workspace_state(src_root: &Path, dst_root: &Path) -> std::io::Result<()> {
    copy_dir_contents(src_root.join(".beads"), dst_root.join(".beads"))?;
    copy_dir_contents(src_root.join(".git"), dst_root.join(".git"))?;
    Ok(())
}

fn remap_workspace_evolution_logs(
    events: &mut [WorkspaceEvolutionEvent],
    src_root: &Path,
    dst_root: &Path,
) {
    for event in events {
        if let Some(result) = event.command_result.as_mut()
            && let Ok(relative) = result.log_path.strip_prefix(src_root)
        {
            result.log_path = dst_root.join(relative);
        }
    }
}

#[derive(Debug, Clone, Copy)]
enum BinaryTarget {
    Br,
    Bd,
}

fn run_conformance_command(
    workspace: &mut HarnessConformanceWorkspace,
    command: &ScenarioCommand,
    label: &str,
    target: BinaryTarget,
) -> CommandResult {
    match (target, command.env.is_empty(), command.stdin.as_ref()) {
        (BinaryTarget::Br, true, None) => workspace.run_br(&command.args, label),
        (BinaryTarget::Br, false, None) => {
            workspace.run_br_env(&command.args, command.env.clone(), label)
        }
        (BinaryTarget::Br, true, Some(input)) => {
            workspace.run_br_stdin(&command.args, input, label)
        }
        (BinaryTarget::Br, false, Some(input)) => {
            workspace.run_br_env_stdin(&command.args, command.env.clone(), input, label)
        }
        (BinaryTarget::Bd, true, None) => workspace.run_bd(&command.args, label),
        (BinaryTarget::Bd, false, None) => {
            workspace.run_bd_env(&command.args, command.env.clone(), label)
        }
        (BinaryTarget::Bd, true, Some(input)) => {
            workspace.run_bd_stdin(&command.args, input, label)
        }
        (BinaryTarget::Bd, false, Some(input)) => {
            workspace.run_bd_env_stdin(&command.args, command.env.clone(), input, label)
        }
    }
}

fn populate_workspace_with_dataset(
    workspace: &TestWorkspace,
    dataset: KnownDataset,
) -> std::io::Result<()> {
    let isolated = IsolatedDataset::from_dataset(dataset)?;
    copy_dir_contents(isolated.root.join(".beads"), workspace.root.join(".beads"))?;
    copy_dir_contents(isolated.root.join(".git"), workspace.root.join(".git"))?;
    Ok(())
}

fn populate_conformance_with_dataset(
    workspace: &HarnessConformanceWorkspace,
    dataset: KnownDataset,
) -> std::io::Result<()> {
    let isolated = IsolatedDataset::from_dataset(dataset)?;
    copy_dir_contents(
        isolated.root.join(".beads"),
        workspace.br_workspace.join(".beads"),
    )?;
    copy_dir_contents(
        isolated.root.join(".git"),
        workspace.br_workspace.join(".git"),
    )?;
    copy_dir_contents(
        isolated.root.join(".beads"),
        workspace.bd_workspace.join(".beads"),
    )?;
    copy_dir_contents(
        isolated.root.join(".git"),
        workspace.bd_workspace.join(".git"),
    )?;
    Ok(())
}

fn copy_dir_contents(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
    let src = src.as_ref();
    let dst = dst.as_ref();
    if !src.exists() {
        return Ok(());
    }
    for entry in WalkDir::new(src).into_iter().filter_map(Result::ok) {
        let path = entry.path();
        let rel = path.strip_prefix(src).unwrap_or(path);
        if rel.as_os_str().is_empty() {
            continue;
        }
        let target = dst.join(rel);
        if entry.file_type().is_dir() {
            std::fs::create_dir_all(&target)?;
        } else if entry.file_type().is_file() {
            if let Some(parent) = target.parent() {
                std::fs::create_dir_all(parent)?;
            }
            std::fs::copy(path, &target)?;
        }
    }
    Ok(())
}

fn sort_arrays_recursively(value: &mut Value) {
    match value {
        Value::Array(arr) => {
            for item in arr.iter_mut() {
                sort_arrays_recursively(item);
            }
            arr.sort_by(|a, b| {
                serde_json::to_string(a)
                    .unwrap_or_default()
                    .cmp(&serde_json::to_string(b).unwrap_or_default())
            });
        }
        Value::Object(map) => {
            for val in map.values_mut() {
                sort_arrays_recursively(val);
            }
        }
        _ => {}
    }
}

fn remove_field_path(value: &mut Value, path: &str) {
    let segments: Vec<&str> = path.split('.').collect();
    remove_field_segments(value, &segments);
}

fn remove_field_segments(value: &mut Value, segments: &[&str]) {
    if segments.is_empty() {
        return;
    }
    match value {
        Value::Object(map) => {
            if segments.len() == 1 {
                map.remove(segments[0]);
            } else if let Some(next) = map.get_mut(segments[0]) {
                remove_field_segments(next, &segments[1..]);
            }
        }
        Value::Array(arr) => {
            for item in arr.iter_mut() {
                remove_field_segments(item, segments);
            }
        }
        _ => {}
    }
}

fn structure_only(value: &Value) -> Value {
    match value {
        Value::Object(map) => {
            let mut out = serde_json::Map::new();
            for (k, v) in map {
                out.insert(k.clone(), structure_only(v));
            }
            Value::Object(out)
        }
        Value::Array(arr) => Value::Array(arr.iter().map(structure_only).collect()),
        _ => Value::Null,
    }
}

fn check_timestamp_tolerance(
    br: &Value,
    bd: &Value,
    normalization: &NormalizationRules,
) -> Vec<String> {
    let Some(tolerance) = normalization.timestamp_tolerance else {
        return Vec::new();
    };

    let mut issues = Vec::new();
    check_timestamp_tolerance_inner(br, bd, normalization, "", tolerance, &mut issues);
    issues
}

fn check_timestamp_tolerance_inner(
    br: &Value,
    bd: &Value,
    normalization: &NormalizationRules,
    path: &str,
    tolerance: Duration,
    issues: &mut Vec<String>,
) {
    match (br, bd) {
        (Value::Object(br_map), Value::Object(bd_map)) => {
            for (key, br_val) in br_map {
                let bd_val = bd_map.get(key);
                let field_path = if path.is_empty() {
                    key.clone()
                } else {
                    format!("{path}.{key}")
                };
                if normalization.mask_fields.contains(key)
                    && let (Some(br_str), Some(bd_str)) =
                        (br_val.as_str(), bd_val.and_then(Value::as_str))
                    && let (Ok(br_dt), Ok(bd_dt)) =
                        (parse_timestamp(br_str), parse_timestamp(bd_str))
                {
                    let diff = (br_dt - bd_dt).num_seconds().unsigned_abs();
                    if diff > tolerance.as_secs() {
                        issues.push(format!(
                            "timestamp drift at {field_path}: br={br_str} bd={bd_str} diff={diff}s"
                        ));
                    }
                }
                if let Some(bd_val) = bd_val {
                    check_timestamp_tolerance_inner(
                        br_val,
                        bd_val,
                        normalization,
                        &field_path,
                        tolerance,
                        issues,
                    );
                }
            }
        }
        (Value::Array(br_arr), Value::Array(bd_arr)) => {
            for (idx, (br_val, bd_val)) in br_arr.iter().zip(bd_arr.iter()).enumerate() {
                let field_path = format!("{path}[{idx}]");
                check_timestamp_tolerance_inner(
                    br_val,
                    bd_val,
                    normalization,
                    &field_path,
                    tolerance,
                    issues,
                );
            }
        }
        _ => {}
    }
}

fn parse_timestamp(value: &str) -> Result<DateTime<FixedOffset>, chrono::ParseError> {
    DateTime::parse_from_rfc3339(value)
}

// ============================================================================
// PREDEFINED SCENARIOS
// ============================================================================

/// Module containing predefined scenarios for common operations.
pub mod catalog {
    use super::*;

    /// Create a CRUD scenario: create, list, show, update, close.
    pub fn crud_lifecycle() -> Scenario {
        Scenario::new("crud_lifecycle", ScenarioCommand::new(["list", "--json"]))
            .with_description("Full CRUD lifecycle: create, update, close, verify list")
            .with_tags(["crud", "quick"])
            .with_setup_commands(vec![
                ScenarioCommand::new(["create", "Test issue"]).with_label("create"),
                ScenarioCommand::new(["update", "--status", "in_progress"]).with_label("update"),
            ])
            .with_compare_mode(CompareMode::ContainsFields(vec![
                "title".into(),
                "status".into(),
            ]))
    }

    /// Empty list scenario.
    pub fn empty_list() -> Scenario {
        Scenario::new("empty_list", ScenarioCommand::new(["list", "--json"]))
            .with_description("Verify empty list output matches")
            .with_tags(["quick", "list"])
            .with_compare_mode(CompareMode::ExactJson)
    }

    /// Stats command scenario.
    pub fn stats_basic() -> Scenario {
        Scenario::new("stats_basic", ScenarioCommand::new(["stats", "--json"]))
            .with_description("Verify stats output after creating issues")
            .with_tags(["stats", "quick"])
            .with_setup_commands(vec![
                ScenarioCommand::new(["create", "Issue 1"]),
                ScenarioCommand::new(["create", "Issue 2"]),
            ])
            .with_compare_mode(CompareMode::ContainsFields(vec!["total".into()]))
    }

    /// Sync safety scenario.
    pub fn sync_safety() -> Scenario {
        Scenario::new(
            "sync_safety",
            ScenarioCommand::new(["sync", "--flush-only", "--json"]),
        )
        .with_description("Verify sync does not execute git commands")
        .with_tags(["sync", "safety"])
        .with_setup_commands(vec![ScenarioCommand::new(["create", "Test issue"])])
        .with_invariants(
            Invariants::success()
                .with_no_git_ops()
                .with_path_confinement(),
        )
        .with_compare_mode(CompareMode::ExitCodeOnly)
    }

    fn seeded_title(seed: u64, role: &str) -> String {
        format!("seed-{seed}-{role}")
    }

    fn seeded_reason(seed: u64, action: &str) -> String {
        format!("seed-{seed}-{action}")
    }

    fn command_step(args: impl IntoIterator<Item = String>, label: &str) -> WorkspaceEvolutionStep {
        WorkspaceEvolutionStep::command(WorkspaceEvolutionCommand::new(
            ScenarioCommand::new(args).with_label(label),
        ))
    }

    fn captured_create_step(
        seed: u64,
        role: &str,
        alias: &str,
        label: &str,
    ) -> WorkspaceEvolutionStep {
        WorkspaceEvolutionStep::command(
            WorkspaceEvolutionCommand::new(
                ScenarioCommand::new(vec![
                    "create".to_string(),
                    seeded_title(seed, role),
                    "--json".to_string(),
                    "--no-auto-flush".to_string(),
                ])
                .with_label(label),
            )
            .capture_issue_id_as(alias),
        )
    }

    fn stale_read_step(command: &str, label: &str) -> WorkspaceEvolutionStep {
        command_step(
            vec![
                "--no-auto-import".to_string(),
                "--allow-stale".to_string(),
                command.to_string(),
                "--json".to_string(),
            ],
            label,
        )
    }

    fn sync_flush_step(label: &str) -> WorkspaceEvolutionStep {
        command_step(
            vec![
                "sync".to_string(),
                "--flush-only".to_string(),
                "--json".to_string(),
            ],
            label,
        )
    }

    fn close_step(
        seed: u64,
        issue_alias: &str,
        action: &str,
        label: &str,
        no_auto_flush: bool,
    ) -> WorkspaceEvolutionStep {
        let mut args = vec![
            "close".to_string(),
            format!("{{{issue_alias}}}"),
            "--reason".to_string(),
            seeded_reason(seed, action),
            "--json".to_string(),
        ];
        if no_auto_flush {
            args.push("--no-auto-flush".to_string());
        }
        command_step(args, label)
    }

    fn reopen_step(
        seed: u64,
        issue_alias: &str,
        action: &str,
        label: &str,
    ) -> WorkspaceEvolutionStep {
        command_step(
            vec![
                "reopen".to_string(),
                format!("{{{issue_alias}}}"),
                "--reason".to_string(),
                seeded_reason(seed, action),
                "--json".to_string(),
            ],
            label,
        )
    }

    /// Deterministic long-lived workspace that mixes stale reads, explicit sync, and dependency churn.
    pub fn long_lived_dependency_story(seed: u64) -> WorkspaceEvolutionPlan {
        WorkspaceEvolutionPlan::new("long_lived_dependency_story", seed)
            .with_description(
                "Seeded multi-command workspace evolution covering stale reads, explicit flush/import, dependency graph updates, and unblock transitions.",
            )
            .with_steps(vec![
                captured_create_step(seed, "root", "root", "create_root"),
                captured_create_step(seed, "child", "child", "create_child"),
                stale_read_step("list", "list_stale_before_flush"),
                sync_flush_step("flush_initial"),
                command_step(
                    vec![
                        "dep".to_string(),
                        "add".to_string(),
                        "{child}".to_string(),
                        "{root}".to_string(),
                        "--json".to_string(),
                    ],
                    "link_dependency",
                ),
                command_step(vec!["ready".to_string(), "--json".to_string()], "ready_blocked"),
                close_step(seed, "root", "close-root", "close_root", true),
                stale_read_step("ready", "ready_after_close"),
                sync_flush_step("flush_after_close"),
                reopen_step(seed, "root", "reopen-root", "reopen_root"),
                command_step(
                    vec![
                        "sync".to_string(),
                        "--import-only".to_string(),
                        "--force".to_string(),
                        "--json".to_string(),
                    ],
                    "import_after_reopen",
                ),
                close_step(seed, "root", "final-close-root", "close_root_final", false),
                command_step(vec!["ready".to_string(), "--json".to_string()], "ready_final"),
                sync_flush_step("flush_final"),
            ])
    }

    /// Deterministic long-lived workspace that intentionally corrupts JSONL, observes failure, and restores a known-good snapshot.
    pub fn long_lived_recovery_story(seed: u64) -> WorkspaceEvolutionPlan {
        WorkspaceEvolutionPlan::new("long_lived_recovery_story", seed)
            .with_description(
                "Seeded workspace evolution covering explicit flush, external JSONL corruption, expected import failure, snapshot restore, and clean replay handoff.",
            )
            .with_steps(vec![
                WorkspaceEvolutionStep::command(
                    WorkspaceEvolutionCommand::new(
                        ScenarioCommand::new(vec![
                            "create".to_string(),
                            seeded_title(seed, "recovery"),
                            "--json".to_string(),
                        ])
                        .with_label("create_recovery_issue"),
                    )
                    .capture_issue_id_as("recovery"),
                ),
                WorkspaceEvolutionStep::command(WorkspaceEvolutionCommand::new(
                    ScenarioCommand::new(["sync", "--flush-only", "--json"])
                        .with_label("flush_recovery_baseline"),
                )),
                WorkspaceEvolutionStep::snapshot("stable_recovery_snapshot"),
                WorkspaceEvolutionStep::append_to_issues_jsonl(
                    "append_conflict_marker",
                    format!("<<<<<<< seed-{seed}\nnot valid json\n=======\nstill bad\n>>>>>>> replay\n"),
                ),
                WorkspaceEvolutionStep::command(
                    WorkspaceEvolutionCommand::new(
                        ScenarioCommand::new(["sync", "--import-only", "--json"])
                            .with_label("import_corrupt"),
                    )
                    .expect_failure(),
                ),
                WorkspaceEvolutionStep::command(WorkspaceEvolutionCommand::new(
                    ScenarioCommand::new(vec![
                        "--no-auto-import".to_string(),
                        "--allow-stale".to_string(),
                        "list".to_string(),
                        "--json".to_string(),
                    ])
                    .with_label("list_stale_after_failure"),
                )),
                WorkspaceEvolutionStep::restore_snapshot(
                    "restore_recovery_snapshot",
                    "stable_recovery_snapshot",
                ),
                WorkspaceEvolutionStep::command(WorkspaceEvolutionCommand::new(
                    ScenarioCommand::new(["sync", "--import-only", "--force", "--json"])
                        .with_label("import_restored"),
                )),
                WorkspaceEvolutionStep::command(WorkspaceEvolutionCommand::new(
                    ScenarioCommand::new(["list", "--json"]).with_label("list_recovered"),
                )),
                WorkspaceEvolutionStep::command(WorkspaceEvolutionCommand::new(
                    ScenarioCommand::new(["sync", "--flush-only", "--json"])
                        .with_label("flush_recovered"),
                )),
            ])
    }

    /// Deterministic single-workspace stress story that repeats mixed human/agent command ordering.
    #[allow(clippy::too_many_lines)]
    pub fn long_lived_mixed_order_stress(seed: u64, iterations: usize) -> WorkspaceEvolutionPlan {
        let iterations = iterations.max(1);
        let mut steps = Vec::with_capacity(iterations.saturating_mul(12).saturating_add(8));

        steps.push(captured_create_step(
            seed,
            "stress-anchor",
            "anchor",
            "create_anchor",
        ));
        steps.push(sync_flush_step("flush_anchor"));

        for iteration in 0..iterations {
            let iteration_seed = seed
                .checked_add(u64::try_from(iteration).expect("iteration should fit into u64"))
                .expect("stress seed should not overflow");
            let alias = format!("stress_{iteration}");
            let create_label = format!("create_stress_{iteration}");
            steps.push(captured_create_step(
                iteration_seed,
                &format!("stress-{iteration}"),
                &alias,
                &create_label,
            ));

            steps.push(command_step(
                vec![
                    "sync".to_string(),
                    "--status".to_string(),
                    "--json".to_string(),
                ],
                &format!("status_after_create_{iteration}"),
            ));
            steps.push(stale_read_step("list", &format!("stale_list_{iteration}")));
            steps.push(command_step(
                vec![
                    "update".to_string(),
                    format!("{{{alias}}}"),
                    "--status".to_string(),
                    "in_progress".to_string(),
                    "--json".to_string(),
                ],
                &format!("agent_claim_{iteration}"),
            ));
            steps.push(command_step(
                vec![
                    "label".to_string(),
                    "add".to_string(),
                    format!("{{{alias}}}"),
                    format!("stress-{iteration}"),
                    "--json".to_string(),
                ],
                &format!("human_label_{iteration}"),
            ));
            steps.push(command_step(
                vec![
                    "comments".to_string(),
                    "add".to_string(),
                    format!("{{{alias}}}"),
                    seeded_reason(iteration_seed, "stress-note"),
                    "--json".to_string(),
                ],
                &format!("agent_comment_{iteration}"),
            ));

            if iteration % 3 == 1 {
                steps.push(close_step(
                    iteration_seed,
                    &alias,
                    "stress-close",
                    &format!("close_stress_{iteration}"),
                    true,
                ));
                steps.push(reopen_step(
                    iteration_seed,
                    &alias,
                    "stress-reopen",
                    &format!("reopen_stress_{iteration}"),
                ));
            }

            if iteration % 4 == 2 {
                steps.push(WorkspaceEvolutionStep::command(
                    WorkspaceEvolutionCommand::new(
                        ScenarioCommand::new(["show", "missing-stress-id", "--json"])
                            .with_label(format!("expected_missing_show_{iteration}")),
                    )
                    .expect_failure(),
                ));
            }

            if iteration % 2 == 0 {
                steps.push(sync_flush_step(&format!("flush_even_{iteration}")));
            } else {
                steps.push(command_step(
                    vec![
                        "sync".to_string(),
                        "--import-only".to_string(),
                        "--force".to_string(),
                        "--json".to_string(),
                    ],
                    &format!("import_odd_{iteration}"),
                ));
            }

            if iteration % 5 == 4 {
                steps.push(WorkspaceEvolutionStep::snapshot(format!(
                    "checkpoint_{iteration}"
                )));
            }
        }

        steps.push(command_step(
            vec!["ready".to_string(), "--json".to_string()],
            "ready_after_stress",
        ));
        steps.push(command_step(
            vec!["info".to_string(), "--json".to_string()],
            "info_after_stress",
        ));
        steps.push(command_step(
            vec!["where".to_string(), "--json".to_string()],
            "where_after_stress",
        ));
        steps.push(sync_flush_step("flush_after_stress"));
        // Per the post-#292 doctor contract (commits 96c3fad2, 1c3c4fe1):
        // any non-OK check — WARN or ERROR — now flips `ok` to false and
        // exits 1. The stress harness legitimately produces WARN-level
        // findings (RUST_LOG=beads_rust=debug set by the test runner,
        // WAL-without-SHM "expected for frankensqlite", missing
        // beads.base.jsonl anchor after the final flush, and a
        // .beads/.gitignore that doesn't list `.write.lock` because
        // `br init` writes a minimal one). None of those degrade the
        // workspace's semantic health, so the e2e test asserts
        // `workspace_health == "healthy"` on the JSON payload rather
        // than exit-code success; the step itself is `allow_either`
        // so the workspace-evolution executor doesn't trip on exit 1.
        steps.push(WorkspaceEvolutionStep::command(
            WorkspaceEvolutionCommand::new(
                ScenarioCommand::new(["doctor", "--json"]).with_label("doctor_after_stress"),
            )
            .allow_either(),
        ));

        WorkspaceEvolutionPlan::new("long_lived_mixed_order_stress", seed)
            .with_description(
                "Seeded long-running single-workspace stress story covering repeated issue lifecycle operations, sync status/import/export checks, expected failures, process restarts, and mixed human/agent command ordering.",
            )
            .with_steps(steps)
    }

    /// Deterministic catalog of long-lived workspace evolutions for a seed.
    pub fn long_lived_seed_suite(seed: u64) -> Vec<WorkspaceEvolutionPlan> {
        vec![
            long_lived_dependency_story(seed),
            long_lived_recovery_story(seed),
            long_lived_mixed_order_stress(seed, 6),
        ]
    }
}

// ============================================================================
// TESTS
// ============================================================================

#[cfg(test)]
mod tests {
    use super::super::dataset_registry::isolated_from_override;
    use super::*;
    use assert_cmd::Command;
    use std::ffi::OsStr;
    use std::path::Path;
    use std::time::Duration;
    use tempfile::TempDir;

    fn should_clear_materialized_env(key: &OsStr) -> bool {
        let key = key.to_string_lossy();
        key.starts_with("BD_")
            || key.starts_with("BEADS_")
            || matches!(
                key.as_ref(),
                "BR_OUTPUT_FORMAT" | "TOON_DEFAULT_FORMAT" | "TOON_STATS"
            )
    }

    fn run_materialized_br(root: &Path, args: &[&str], label: &str) -> CommandResult {
        let bin_path = assert_cmd::cargo::cargo_bin!("br");
        let mut command = Command::new(bin_path);
        command.current_dir(root);
        command.args(args);

        for (key, _) in std::env::vars_os() {
            if should_clear_materialized_env(&key) {
                command.env_remove(key);
            }
        }

        command.env("NO_COLOR", "1");
        command.env("RUST_BACKTRACE", "1");
        command.env("HOME", root);

        let start = std::time::Instant::now();
        let output = command.output().expect("run materialized br");

        CommandResult {
            stdout: String::from_utf8_lossy(&output.stdout).to_string(),
            stderr: String::from_utf8_lossy(&output.stderr).to_string(),
            exit_code: output.status.code().unwrap_or(-1),
            success: output.status.success(),
            duration: start.elapsed(),
            log_path: root.join("logs").join(format!("{label}.log")),
            stdout_truncated: false,
            stderr_truncated: false,
            timed_out: false,
        }
    }

    #[test]
    fn test_normalization_rules_apply() {
        let rules = NormalizationRules::conformance_default();
        let mut value = serde_json::json!({
            "id": "bd-abc123",
            "title": "Test",
            "created_at": "2026-01-17T10:00:00Z",
            "updated_at": "2026-01-17T11:00:00Z"
        });

        let log = rules.apply(&mut value);

        assert_eq!(value["created_at"], "NORMALIZED_TIMESTAMP");
        assert_eq!(value["updated_at"], "NORMALIZED_TIMESTAMP");
        assert_eq!(value["id"], "bd-HASH"); // invariant: NormalizationRules placeholder, not ID-pinning
        assert!(!log.is_empty());
    }

    #[test]
    fn test_scenario_builder() {
        let scenario = Scenario::new("test", ScenarioCommand::new(["list"]))
            .with_description("Test scenario")
            .with_tags(["quick", "test"])
            .with_invariants(Invariants::success());

        assert_eq!(scenario.name, "test");
        assert!(scenario.has_tag("quick"));
        assert!(scenario.supports_mode(ExecutionMode::E2E));
    }

    #[test]
    fn test_compare_mode_default() {
        let mode = CompareMode::default();
        assert!(matches!(mode, CompareMode::NormalizedJson));
    }

    #[test]
    fn test_extract_json_payload() {
        let stdout = "Created bd-abc: Test\n[{\"id\": \"bd-abc\"}]";
        let json = extract_json_payload(stdout);
        assert!(json.starts_with('['));
    }

    #[test]
    fn compare_mode_array_unordered_matches() {
        let br_result = CommandResult {
            stdout: "[{\"id\":1},{\"id\":2}]".to_string(),
            stderr: String::new(),
            exit_code: 0,
            success: true,
            duration: Duration::from_secs(0),
            log_path: Path::new(".").join("noop"),
            stdout_truncated: false,
            stderr_truncated: false,
            timed_out: false,
        };
        let bd_result = CommandResult {
            stdout: "[{\"id\":2},{\"id\":1}]".to_string(),
            ..br_result.clone()
        };
        let (comparison, _) = compare_outputs(
            &br_result,
            &bd_result,
            &CompareMode::ArrayUnordered,
            &NormalizationRules::strict(),
        );
        assert!(comparison.matched);
    }

    #[test]
    fn compare_mode_fields_excluded_matches() {
        let br_result = CommandResult {
            stdout: "{\"id\":1,\"ts\":123}".to_string(),
            stderr: String::new(),
            exit_code: 0,
            success: true,
            duration: Duration::from_secs(0),
            log_path: Path::new(".").join("noop"),
            stdout_truncated: false,
            stderr_truncated: false,
            timed_out: false,
        };
        let bd_result = CommandResult {
            stdout: "{\"id\":1,\"ts\":999}".to_string(),
            ..br_result.clone()
        };
        let (comparison, _) = compare_outputs(
            &br_result,
            &bd_result,
            &CompareMode::FieldsExcluded(vec!["ts".to_string()]),
            &NormalizationRules::strict(),
        );
        assert!(comparison.matched);
    }

    #[test]
    fn compare_mode_structure_only_matches() {
        let br_result = CommandResult {
            stdout: "{\"id\":1,\"nested\":{\"a\":true}}".to_string(),
            stderr: String::new(),
            exit_code: 0,
            success: true,
            duration: Duration::from_secs(0),
            log_path: Path::new(".").join("noop"),
            stdout_truncated: false,
            stderr_truncated: false,
            timed_out: false,
        };
        let bd_result = CommandResult {
            stdout: "{\"id\":2,\"nested\":{\"a\":false}}".to_string(),
            ..br_result.clone()
        };
        let (comparison, _) = compare_outputs(
            &br_result,
            &bd_result,
            &CompareMode::StructureOnly,
            &NormalizationRules::strict(),
        );
        assert!(comparison.matched);
    }

    // ========================================================================
    // ScenarioFilter tests (beads_rust-o1az)
    // ========================================================================

    #[test]
    fn test_scenario_filter_empty_matches_all() {
        let filter = ScenarioFilter::new();
        let scenario =
            Scenario::new("test", ScenarioCommand::new(["list"])).with_tags(["quick", "crud"]);

        assert!(filter.matches(&scenario));
        assert!(filter.is_empty());
    }

    #[test]
    fn test_scenario_filter_include_any() {
        let filter = ScenarioFilter::new()
            .with_include_tags(["quick", "sync"])
            .with_match_mode(TagMatchMode::Any);

        let quick_scenario =
            Scenario::new("quick_test", ScenarioCommand::new(["list"])).with_tags(["quick"]);
        let sync_scenario =
            Scenario::new("sync_test", ScenarioCommand::new(["sync"])).with_tags(["sync"]);
        let slow_scenario =
            Scenario::new("slow_test", ScenarioCommand::new(["bench"])).with_tags(["slow"]);

        assert!(filter.matches(&quick_scenario));
        assert!(filter.matches(&sync_scenario));
        assert!(!filter.matches(&slow_scenario));
    }

    #[test]
    fn test_scenario_filter_include_all() {
        let filter = ScenarioFilter::new()
            .with_include_tags(["quick", "crud"])
            .with_match_mode(TagMatchMode::All);

        let both_tags = Scenario::new("both", ScenarioCommand::new(["list"]))
            .with_tags(["quick", "crud", "extra"]);
        let one_tag = Scenario::new("one", ScenarioCommand::new(["list"])).with_tags(["quick"]);

        assert!(filter.matches(&both_tags));
        assert!(!filter.matches(&one_tag));
    }

    #[test]
    fn test_scenario_filter_exclude() {
        let filter = ScenarioFilter::new().with_exclude_tags(["slow", "stress"]);

        let quick = Scenario::new("quick", ScenarioCommand::new(["list"])).with_tags(["quick"]);
        let slow = Scenario::new("slow", ScenarioCommand::new(["bench"])).with_tags(["slow"]);
        let stress = Scenario::new("stress", ScenarioCommand::new(["stress"]))
            .with_tags(["quick", "stress"]);

        assert!(filter.matches(&quick));
        assert!(!filter.matches(&slow));
        assert!(!filter.matches(&stress)); // excluded even though has "quick"
    }

    #[test]
    fn test_scenario_filter_exclude_precedence() {
        let filter = ScenarioFilter::new()
            .with_include_tags(["test"])
            .with_exclude_tags(["slow"]);

        let quick_test =
            Scenario::new("quick", ScenarioCommand::new(["list"])).with_tags(["test", "quick"]);
        let slow_test =
            Scenario::new("slow", ScenarioCommand::new(["list"])).with_tags(["test", "slow"]);

        assert!(filter.matches(&quick_test));
        assert!(!filter.matches(&slow_test)); // excluded takes precedence
    }

    #[test]
    fn test_scenario_filter_description() {
        let empty = ScenarioFilter::new();
        assert_eq!(empty.description(), "all scenarios");

        let include = ScenarioFilter::new().with_include_tags(["quick", "crud"]);
        assert!(include.description().contains("quick"));
        assert!(include.description().contains("crud"));

        let exclude = ScenarioFilter::new().with_exclude_tags(["slow"]);
        assert!(exclude.description().contains("exclude"));
        assert!(exclude.description().contains("slow"));
    }

    #[test]
    fn test_scenario_filter_to_json() {
        let filter = ScenarioFilter::new()
            .with_include_tags(["quick"])
            .with_exclude_tags(["slow"])
            .with_match_mode(TagMatchMode::Any);

        let json = filter.to_json();
        assert_eq!(json["include_tags"], serde_json::json!(["quick"]));
        assert_eq!(json["exclude_tags"], serde_json::json!(["slow"]));
        assert_eq!(json["match_mode"], serde_json::json!("any"));
        assert!(json["description"].as_str().is_some());
    }

    #[test]
    fn test_scenario_filter_filter_list() {
        let filter = ScenarioFilter::new().with_include_tags(["quick"]);

        let scenarios = vec![
            Scenario::new("quick1", ScenarioCommand::new(["list"])).with_tags(["quick"]),
            Scenario::new("slow1", ScenarioCommand::new(["bench"])).with_tags(["slow"]),
            Scenario::new("quick2", ScenarioCommand::new(["show"])).with_tags(["quick", "read"]),
        ];

        let filtered = filter.filter(&scenarios);
        assert_eq!(filtered.len(), 2);
        assert_eq!(filtered[0].name, "quick1");
        assert_eq!(filtered[1].name, "quick2");
    }

    #[test]
    fn test_tag_match_mode_default() {
        let mode = TagMatchMode::default();
        assert_eq!(mode, TagMatchMode::Any);
    }

    #[test]
    fn test_scenario_has_any_tag() {
        let scenario =
            Scenario::new("test", ScenarioCommand::new(["list"])).with_tags(["quick", "crud"]);

        assert!(scenario.has_any_tag(&["quick".to_string(), "slow".to_string()]));
        assert!(!scenario.has_any_tag(&["slow".to_string(), "stress".to_string()]));
    }

    #[test]
    fn test_scenario_has_all_tags() {
        let scenario = Scenario::new("test", ScenarioCommand::new(["list"]))
            .with_tags(["quick", "crud", "read"]);

        assert!(scenario.has_all_tags(&["quick".to_string(), "crud".to_string()]));
        assert!(!scenario.has_all_tags(&["quick".to_string(), "slow".to_string()]));
    }

    // ========================================================================
    // Cross-platform normalization tests (beads_rust-lsht)
    // ========================================================================

    #[test]
    fn test_path_separator_normalization() {
        let rules = NormalizationRules::cross_platform();
        let mut value = serde_json::json!({
            "path": "C:\\Users\\test\\project\\.beads",
            "file_path": "src\\main.rs",
            "db_path": "data\\beads.db",
            "title": "Not a path with \\ backslash"
        });

        let log = rules.apply(&mut value);

        // Path fields should have backslashes converted to forward slashes
        assert_eq!(value["path"], "C:/Users/test/project/.beads");
        assert_eq!(value["file_path"], "src/main.rs");
        assert_eq!(value["db_path"], "data/beads.db");
        // Non-path fields should not be modified
        assert_eq!(value["title"], "Not a path with \\ backslash");
        // Should log the normalizations
        assert!(log.iter().any(|l| l.contains("Normalized path")));
    }

    #[test]
    fn test_path_normalization_no_backslashes() {
        let rules = NormalizationRules::cross_platform();
        let mut value = serde_json::json!({
            "path": "/home/user/project/.beads",
            "file_path": "src/main.rs"
        });

        let log = rules.apply(&mut value);

        // Already Unix-style paths should remain unchanged
        assert_eq!(value["path"], "/home/user/project/.beads");
        assert_eq!(value["file_path"], "src/main.rs");
        // No path normalization should be logged
        assert!(!log.iter().any(|l| l.contains("Normalized path")));
    }

    #[test]
    fn test_line_ending_normalization() {
        let rules = NormalizationRules::cross_platform();
        let mut value = serde_json::json!({
            "description": "Line 1\r\nLine 2\r\nLine 3",
            "notes": "Single line no CRLF"
        });

        let log = rules.apply(&mut value);

        // CRLF should be converted to LF
        assert_eq!(value["description"], "Line 1\nLine 2\nLine 3");
        // Single line should remain unchanged
        assert_eq!(value["notes"], "Single line no CRLF");
        // Should log line ending normalization
        assert!(log.iter().any(|l| l.contains("Normalized line endings")));
    }

    #[test]
    fn test_line_ending_normalization_nested() {
        let rules = NormalizationRules::cross_platform();
        let mut value = serde_json::json!({
            "issues": [
                {"description": "First\r\nitem"},
                {"description": "Second\r\nitem"}
            ]
        });

        let log = rules.apply(&mut value);

        // Nested strings should also have CRLF normalized
        assert_eq!(value["issues"][0]["description"], "First\nitem");
        assert_eq!(value["issues"][1]["description"], "Second\nitem");
        assert!(!log.is_empty());
    }

    #[test]
    fn test_cross_platform_constructor() {
        let rules = NormalizationRules::cross_platform();

        assert!(rules.normalize_paths);
        assert!(rules.normalize_line_endings);
        assert!(rules.log_normalization);
        assert!(rules.path_fields.contains("path"));
        assert!(rules.path_fields.contains("file_path"));
        assert!(rules.path_fields.contains("db_path"));
        assert!(rules.path_fields.contains("workspace_root"));
        // Should not have timestamp masking
        assert!(rules.mask_fields.is_empty());
        assert!(!rules.normalize_ids);
    }

    #[test]
    fn test_conformance_default_includes_cross_platform() {
        let rules = NormalizationRules::conformance_default();

        // conformance_default should include cross-platform normalization
        assert!(rules.normalize_paths);
        assert!(rules.normalize_line_endings);
        assert!(rules.path_fields.contains("path"));
        // Plus the regular conformance features
        assert!(rules.mask_fields.contains("created_at"));
        assert!(rules.normalize_ids);
    }

    #[test]
    fn test_path_field_with_line_endings() {
        // Path fields should also have line endings normalized
        let rules = NormalizationRules::cross_platform();
        let mut value = serde_json::json!({
            "path": "C:\\Users\\test\r\n\\project"
        });

        rules.apply(&mut value);

        // Both backslashes and CRLF should be normalized
        assert_eq!(value["path"], "C:/Users/test\n/project");
    }

    #[test]
    fn test_strict_no_cross_platform() {
        let rules = NormalizationRules::strict();

        // strict() should not have cross-platform normalization
        assert!(!rules.normalize_paths);
        assert!(!rules.normalize_line_endings);
        assert!(rules.path_fields.is_empty());
    }

    // ========================================================================
    // Scenario DSL + Normalization + Comparator tests (beads_rust-nh50)
    // ========================================================================

    // --- Array Sorting Tests ---

    #[test]
    fn test_array_sorting_normalization() {
        let rules = NormalizationRules {
            sort_arrays: true,
            ..Default::default()
        };
        let mut value = serde_json::json!({
            "items": [{"name": "zebra"}, {"name": "apple"}, {"name": "mango"}]
        });

        rules.apply(&mut value);

        // Arrays should be sorted by JSON string representation
        let items = value["items"].as_array().unwrap();
        assert_eq!(items[0]["name"], "apple");
        assert_eq!(items[1]["name"], "mango");
        assert_eq!(items[2]["name"], "zebra");
    }

    #[test]
    fn test_array_sorting_nested() {
        let rules = NormalizationRules {
            sort_arrays: true,
            ..Default::default()
        };
        let mut value = serde_json::json!({
            "outer": [
                {"inner": [3, 1, 2]},
                {"inner": [6, 4, 5]}
            ]
        });

        rules.apply(&mut value);

        // Nested arrays should also be sorted
        let outer = value["outer"].as_array().unwrap();
        assert_eq!(outer[0]["inner"], serde_json::json!([1, 2, 3]));
        assert_eq!(outer[1]["inner"], serde_json::json!([4, 5, 6]));
    }

    #[test]
    fn test_array_sorting_disabled() {
        let rules = NormalizationRules {
            sort_arrays: false,
            ..Default::default()
        };
        let mut value = serde_json::json!({
            "items": [3, 1, 2]
        });

        rules.apply(&mut value);

        // Arrays should remain in original order
        assert_eq!(value["items"], serde_json::json!([3, 1, 2]));
    }

    // --- Field Removal Tests ---

    #[test]
    fn test_field_removal() {
        let mut rules = NormalizationRules::default();
        rules.remove_fields.insert("secret".to_string());
        rules.remove_fields.insert("internal".to_string());
        rules.log_normalization = true;

        let mut value = serde_json::json!({
            "id": "test-123",
            "secret": "password123",
            "internal": "debug_info",
            "public": "visible"
        });

        let log = rules.apply(&mut value);

        assert!(value.get("secret").is_none());
        assert!(value.get("internal").is_none());
        assert_eq!(value["id"], "test-123");
        assert_eq!(value["public"], "visible");
        assert!(log.iter().any(|l| l.contains("Removed field: secret")));
    }

    #[test]
    fn test_field_removal_nested() {
        let mut rules = NormalizationRules::default();
        rules.remove_fields.insert("token".to_string());

        let mut value = serde_json::json!({
            "user": {
                "name": "Alice",
                "token": "abc123"
            }
        });

        rules.apply(&mut value);

        // Nested field should be removed
        assert!(value["user"].get("token").is_none());
        assert_eq!(value["user"]["name"], "Alice");
    }

    // --- Timestamp Masking Tests ---

    #[test]
    fn test_timestamp_masking() {
        let rules = NormalizationRules::conformance_default();
        let mut value = serde_json::json!({
            "id": "bd-test123",
            "title": "Test",
            "created_at": "2026-01-17T10:30:00Z",
            "updated_at": "2026-01-17T11:45:00Z",
            "defer_until": "2026-02-01T00:00:00Z"
        });

        let log = rules.apply(&mut value);

        // All timestamp fields should be masked
        assert_eq!(value["created_at"], "NORMALIZED_TIMESTAMP");
        assert_eq!(value["updated_at"], "NORMALIZED_TIMESTAMP");
        assert_eq!(value["defer_until"], "NORMALIZED_TIMESTAMP");
        assert!(log.iter().any(|l| l.contains("Masked timestamp")));
    }

    #[test]
    fn test_timestamp_masking_empty_value() {
        let mut rules = NormalizationRules::default();
        rules.mask_fields.insert("created_at".to_string());
        rules.log_normalization = true;

        let mut value = serde_json::json!({
            "created_at": ""
        });

        let log = rules.apply(&mut value);

        // Empty string should be masked without logging
        assert_eq!(value["created_at"], "NORMALIZED_TIMESTAMP");
        assert!(!log.iter().any(|l| l.contains("Masked timestamp")));
    }

    // --- ID Normalization Tests ---

    #[test]
    fn test_id_normalization() {
        let rules = NormalizationRules::conformance_default();
        let mut value = serde_json::json!({
            "id": "bd-abc123xyz",
            "parent_id": "bd-parent456",
            "blocked_by_id": "bd-blocker789"
        });

        let log = rules.apply(&mut value);

        // IDs should have hash portion masked
        assert_eq!(value["id"], "bd-HASH"); // invariant: NormalizationRules placeholder
        assert_eq!(value["parent_id"], "bd-HASH"); // invariant: NormalizationRules placeholder
        assert_eq!(value["blocked_by_id"], "bd-HASH"); // invariant: NormalizationRules placeholder
        assert!(log.iter().any(|l| l.contains("Normalized ID")));
    }

    #[test]
    fn test_id_normalization_preserves_prefix() {
        let rules = NormalizationRules::conformance_default();
        let mut value = serde_json::json!({
            "id": "beads_rust-task-abcd1234"
        });

        rules.apply(&mut value);

        // Prefix before last dash should be preserved
        assert_eq!(value["id"], "beads_rust-task-HASH");
    }

    #[test]
    fn test_id_normalization_no_dash() {
        let rules = NormalizationRules::conformance_default();
        let mut value = serde_json::json!({
            "id": "nodash"
        });

        rules.apply(&mut value);

        // ID without dash should remain unchanged
        assert_eq!(value["id"], "nodash");
    }

    #[test]
    fn test_id_normalization_disabled() {
        let rules = NormalizationRules {
            normalize_ids: false,
            ..Default::default()
        };
        let mut value = serde_json::json!({
            "id": "bd-abc123"
        });

        rules.apply(&mut value);

        // ID should remain unchanged when normalize_ids is false
        assert_eq!(value["id"], "bd-abc123"); // invariant: fixture round-trip when normalize_ids=false
    }

    // --- Clock Tolerance / Timestamp Tolerance Tests ---

    #[test]
    fn test_timestamp_tolerance_within_range() {
        let rules = NormalizationRules {
            mask_fields: std::iter::once("created_at".to_string()).collect(),
            timestamp_tolerance: Some(Duration::from_secs(10)),
            ..Default::default()
        };

        let br = serde_json::json!({
            "created_at": "2026-01-17T10:00:00Z"
        });
        let bd = serde_json::json!({
            "created_at": "2026-01-17T10:00:05Z"  // 5 seconds later
        });

        let issues = check_timestamp_tolerance(&br, &bd, &rules);

        // 5 seconds is within 10 second tolerance
        assert!(issues.is_empty());
    }

    #[test]
    fn test_timestamp_tolerance_exceeded() {
        let rules = NormalizationRules {
            mask_fields: std::iter::once("created_at".to_string()).collect(),
            timestamp_tolerance: Some(Duration::from_secs(5)),
            ..Default::default()
        };

        let br = serde_json::json!({
            "created_at": "2026-01-17T10:00:00Z"
        });
        let bd = serde_json::json!({
            "created_at": "2026-01-17T10:00:10Z"  // 10 seconds later
        });

        let issues = check_timestamp_tolerance(&br, &bd, &rules);

        // 10 seconds exceeds 5 second tolerance
        assert!(!issues.is_empty());
        assert!(issues[0].contains("timestamp drift"));
    }

    #[test]
    fn test_timestamp_tolerance_nested() {
        let rules = NormalizationRules {
            mask_fields: std::iter::once("updated_at".to_string()).collect(),
            timestamp_tolerance: Some(Duration::from_secs(60)),
            ..Default::default()
        };

        let br = serde_json::json!({
            "issues": [
                {"updated_at": "2026-01-17T10:00:00Z"},
                {"updated_at": "2026-01-17T11:00:00Z"}
            ]
        });
        let bd = serde_json::json!({
            "issues": [
                {"updated_at": "2026-01-17T10:00:30Z"},
                {"updated_at": "2026-01-17T11:02:00Z"}  // 2 minutes drift
            ]
        });

        let issues = check_timestamp_tolerance(&br, &bd, &rules);

        // Second timestamp exceeds tolerance
        assert_eq!(issues.len(), 1);
        assert!(issues[0].contains("issues[1]"));
    }

    // --- Comparator Behavior Tests ---

    #[test]
    fn compare_mode_exact_json_matches() {
        let br_result = CommandResult {
            stdout: "{\"id\":1,\"name\":\"test\"}".to_string(),
            stderr: String::new(),
            exit_code: 0,
            success: true,
            duration: Duration::from_secs(0),
            log_path: Path::new(".").join("noop"),
            stdout_truncated: false,
            stderr_truncated: false,
            timed_out: false,
        };
        let bd_result = CommandResult {
            stdout: "{\"id\":1,\"name\":\"test\"}".to_string(),
            ..br_result.clone()
        };

        let (comparison, _) = compare_outputs(
            &br_result,
            &bd_result,
            &CompareMode::ExactJson,
            &NormalizationRules::strict(),
        );

        assert!(comparison.matched);
    }

    #[test]
    fn compare_mode_exact_json_fails_on_difference() {
        let br_result = CommandResult {
            stdout: "{\"id\":1,\"name\":\"test\"}".to_string(),
            stderr: String::new(),
            exit_code: 0,
            success: true,
            duration: Duration::from_secs(0),
            log_path: Path::new(".").join("noop"),
            stdout_truncated: false,
            stderr_truncated: false,
            timed_out: false,
        };
        let bd_result = CommandResult {
            stdout: "{\"id\":2,\"name\":\"test\"}".to_string(),
            ..br_result.clone()
        };

        let (comparison, _) = compare_outputs(
            &br_result,
            &bd_result,
            &CompareMode::ExactJson,
            &NormalizationRules::strict(),
        );

        assert!(!comparison.matched);
        assert!(comparison.diff_description.is_some());
    }

    #[test]
    fn compare_mode_normalized_json_ignores_timestamps() {
        let br_result = CommandResult {
            stdout: "{\"id\":\"bd-abc\",\"created_at\":\"2026-01-17T10:00:00Z\"}".to_string(),
            stderr: String::new(),
            exit_code: 0,
            success: true,
            duration: Duration::from_secs(0),
            log_path: Path::new(".").join("noop"),
            stdout_truncated: false,
            stderr_truncated: false,
            timed_out: false,
        };
        let bd_result = CommandResult {
            stdout: "{\"id\":\"bd-xyz\",\"created_at\":\"2026-01-17T11:00:00Z\"}".to_string(),
            ..br_result.clone()
        };

        let (comparison, log) = compare_outputs(
            &br_result,
            &bd_result,
            &CompareMode::NormalizedJson,
            &NormalizationRules::conformance_default(),
        );

        // After normalization (masking timestamps and IDs), should match
        assert!(comparison.matched);
        assert!(!log.is_empty());
    }

    #[test]
    fn compare_mode_contains_fields_matches_specified() {
        let br_result = CommandResult {
            stdout: "{\"id\":1,\"title\":\"Test\",\"extra\":\"ignored\"}".to_string(),
            stderr: String::new(),
            exit_code: 0,
            success: true,
            duration: Duration::from_secs(0),
            log_path: Path::new(".").join("noop"),
            stdout_truncated: false,
            stderr_truncated: false,
            timed_out: false,
        };
        let bd_result = CommandResult {
            stdout: "{\"id\":1,\"title\":\"Test\",\"extra\":\"different\"}".to_string(),
            ..br_result.clone()
        };

        let (comparison, _) = compare_outputs(
            &br_result,
            &bd_result,
            &CompareMode::ContainsFields(vec!["id".to_string(), "title".to_string()]),
            &NormalizationRules::strict(),
        );

        // Only id and title are compared, extra is ignored
        assert!(comparison.matched);
    }

    #[test]
    fn compare_mode_contains_fields_fails_on_mismatch() {
        let br_result = CommandResult {
            stdout: "{\"id\":1,\"title\":\"Test A\"}".to_string(),
            stderr: String::new(),
            exit_code: 0,
            success: true,
            duration: Duration::from_secs(0),
            log_path: Path::new(".").join("noop"),
            stdout_truncated: false,
            stderr_truncated: false,
            timed_out: false,
        };
        let bd_result = CommandResult {
            stdout: "{\"id\":1,\"title\":\"Test B\"}".to_string(),
            ..br_result.clone()
        };

        let (comparison, _) = compare_outputs(
            &br_result,
            &bd_result,
            &CompareMode::ContainsFields(vec!["title".to_string()]),
            &NormalizationRules::strict(),
        );

        assert!(!comparison.matched);
        assert!(comparison.diff_description.unwrap().contains("title"));
    }

    #[test]
    fn compare_mode_exit_code_only_matches() {
        let br_result = CommandResult {
            stdout: "different output".to_string(),
            stderr: String::new(),
            exit_code: 0,
            success: true,
            duration: Duration::from_secs(0),
            log_path: Path::new(".").join("noop"),
            stdout_truncated: false,
            stderr_truncated: false,
            timed_out: false,
        };
        let bd_result = CommandResult {
            stdout: "completely different".to_string(),
            exit_code: 0,
            ..br_result.clone()
        };

        let (comparison, _) = compare_outputs(
            &br_result,
            &bd_result,
            &CompareMode::ExitCodeOnly,
            &NormalizationRules::strict(),
        );

        assert!(comparison.matched);
    }

    #[test]
    fn compare_mode_exit_code_only_fails_on_different_exit() {
        let br_result = CommandResult {
            stdout: "same output".to_string(),
            stderr: String::new(),
            exit_code: 0,
            success: true,
            duration: Duration::from_secs(0),
            log_path: Path::new(".").join("noop"),
            stdout_truncated: false,
            stderr_truncated: false,
            timed_out: false,
        };
        let bd_result = CommandResult {
            stdout: "same output".to_string(),
            exit_code: 1,
            success: false,
            ..br_result.clone()
        };

        let (comparison, _) = compare_outputs(
            &br_result,
            &bd_result,
            &CompareMode::ExitCodeOnly,
            &NormalizationRules::strict(),
        );

        assert!(!comparison.matched);
        assert!(comparison.diff_description.unwrap().contains("Exit code"));
    }

    // --- Scenario Validation Tests ---

    #[test]
    fn test_scenario_supports_mode() {
        let scenario = Scenario::new("test", ScenarioCommand::new(["list"]))
            .with_modes(vec![ExecutionMode::E2E]);

        assert!(scenario.supports_mode(ExecutionMode::E2E));
        assert!(!scenario.supports_mode(ExecutionMode::Conformance));
        assert!(!scenario.supports_mode(ExecutionMode::Benchmark));
    }

    #[test]
    fn test_scenario_default_modes() {
        let scenario = Scenario::new("test", ScenarioCommand::new(["list"]));

        // Default includes E2E and Conformance but not Benchmark
        assert!(scenario.supports_mode(ExecutionMode::E2E));
        assert!(scenario.supports_mode(ExecutionMode::Conformance));
        assert!(!scenario.supports_mode(ExecutionMode::Benchmark));
    }

    #[test]
    fn test_scenario_command_builder() {
        let cmd = ScenarioCommand::new(["create", "Test issue", "--priority", "1"])
            .with_env([("DEBUG", "1"), ("LOG_LEVEL", "trace")])
            .with_stdin("additional input")
            .with_label("create_issue");

        assert_eq!(cmd.args, vec!["create", "Test issue", "--priority", "1"]);
        assert_eq!(cmd.env.len(), 2);
        assert_eq!(cmd.stdin, Some("additional input".to_string()));
        assert_eq!(cmd.label, "create_issue");
    }

    #[test]
    fn test_scenario_command_default_label() {
        let cmd = ScenarioCommand::new(["list", "--json"]);

        // Default label is first argument
        assert_eq!(cmd.label, "list");
    }

    #[test]
    fn test_invariants_success() {
        let inv = Invariants::success();

        assert!(inv.expect_success);
        assert!(!inv.expect_failure);
        assert!(inv.expected_exit_code.is_none());
    }

    #[test]
    fn test_invariants_failure() {
        let inv = Invariants::failure();

        assert!(!inv.expect_success);
        assert!(inv.expect_failure);
    }

    #[test]
    fn test_invariants_with_constraints() {
        let inv = Invariants::success()
            .with_no_git_ops()
            .with_path_confinement();

        assert!(inv.no_git_ops);
        assert!(inv.path_confinement);
    }

    #[test]
    fn test_extract_json_payload_with_preamble() {
        let stdout = "Info: Created issue\nWarning: Low priority\n{\"id\": 1, \"title\": \"Test\"}";
        let json = extract_json_payload(stdout);

        assert!(json.starts_with('{'));
        assert!(json.contains("\"id\""));
    }

    #[test]
    fn test_extract_json_payload_array() {
        let stdout = "Listing issues:\n[{\"id\": 1}, {\"id\": 2}]";
        let json = extract_json_payload(stdout);

        assert!(json.starts_with('['));
    }

    #[test]
    fn test_extract_json_payload_no_json() {
        let stdout = "Plain text with no JSON";
        let json = extract_json_payload(stdout);

        // Returns trimmed input when no JSON found
        assert_eq!(json, "Plain text with no JSON");
    }

    // --- JSON Comparison Edge Cases ---

    #[test]
    fn compare_mode_handles_parse_errors() {
        let br_result = CommandResult {
            stdout: "not valid json".to_string(),
            stderr: String::new(),
            exit_code: 0,
            success: true,
            duration: Duration::from_secs(0),
            log_path: Path::new(".").join("noop"),
            stdout_truncated: false,
            stderr_truncated: false,
            timed_out: false,
        };
        let bd_result = CommandResult {
            stdout: "{\"valid\": true}".to_string(),
            ..br_result.clone()
        };

        let (comparison, _) = compare_outputs(
            &br_result,
            &bd_result,
            &CompareMode::NormalizedJson,
            &NormalizationRules::strict(),
        );

        assert!(!comparison.matched);
        assert!(comparison.diff_description.unwrap().contains("parse error"));
    }

    #[test]
    fn test_structure_only_ignores_values() {
        let a = serde_json::json!({"name": "Alice", "count": 100});
        let b = serde_json::json!({"name": "Bob", "count": 200});

        let a_shape = structure_only(&a);
        let b_shape = structure_only(&b);

        // Structure should match even with different values
        assert_eq!(a_shape, b_shape);
        assert_eq!(a_shape["name"], serde_json::Value::Null);
        assert_eq!(a_shape["count"], serde_json::Value::Null);
    }

    #[test]
    fn test_remove_field_path_nested() {
        let mut value = serde_json::json!({
            "user": {
                "profile": {
                    "secret": "hidden"
                }
            }
        });

        remove_field_path(&mut value, "user.profile.secret");

        assert!(value["user"]["profile"].get("secret").is_none());
    }

    #[test]
    fn workspace_evolution_seed_suite_is_deterministic() {
        let baseline = catalog::long_lived_seed_suite(17);
        let same_seed = catalog::long_lived_seed_suite(17);
        let different_seed = catalog::long_lived_seed_suite(18);

        assert_eq!(baseline, same_seed);
        assert_ne!(baseline, different_seed);
    }

    #[test]
    fn workspace_evolution_dependency_story_materializes_replayable_workspace() {
        let _guard = super::super::workspace_replay_test_guard();
        let materialized = catalog::long_lived_dependency_story(23)
            .execute()
            .expect("dependency story should execute");

        let ready_event = materialized
            .event("ready_after_close")
            .expect("ready_after_close event");
        assert!(ready_event.matched_expectation);

        let child_id = materialized
            .aliases
            .get("child")
            .expect("captured child id")
            .clone();

        let ready = run_materialized_br(&materialized.root, &["ready", "--json"], "ready_replay");
        assert!(ready.success, "materialized ready failed: {}", ready.stderr);

        let ready_json: Value =
            serde_json::from_str(&extract_json_payload(&ready.stdout)).expect("ready json");
        let ready_items = ready_json.as_array().expect("ready array");
        assert!(
            ready_items
                .iter()
                .any(|item| { item.get("id").and_then(Value::as_str) == Some(child_id.as_str()) }),
            "child issue should become ready after blocker closure"
        );

        let isolated = isolated_from_override(
            &materialized.dataset_override("verify long-lived dependency story replay handoff"),
        )
        .expect("isolated replay copy");
        assert!(
            isolated.metadata.issue_count >= 2,
            "replayed workspace should retain seeded issues"
        );
    }

    #[test]
    fn workspace_evolution_recovery_story_restores_clean_replay_input() {
        let _guard = super::super::workspace_replay_test_guard();
        let materialized = catalog::long_lived_recovery_story(29)
            .execute()
            .expect("recovery story should execute");

        let import_corrupt = materialized
            .event("import_corrupt")
            .expect("import_corrupt event");
        assert!(import_corrupt.matched_expectation);
        assert!(
            !import_corrupt
                .command_result
                .as_ref()
                .expect("import result")
                .success,
            "corrupt import should fail but remain an expected step"
        );

        let list = run_materialized_br(&materialized.root, &["list", "--json"], "list_replay");
        assert!(list.success, "materialized list failed: {}", list.stderr);

        let target = TempDir::new().expect("materialize target");
        materialized
            .materialize_into(target.path())
            .expect("copy materialized workspace");
        assert!(
            target.path().join(".beads").join("issues.jsonl").exists(),
            "materialized workspace should include JSONL export"
        );

        let isolated = isolated_from_override(&DatasetOverride::new(
            target.path(),
            "verify restored workspace can re-enter override flow",
        ))
        .expect("isolated restored workspace");
        assert!(
            isolated.metadata.issue_count >= 1,
            "restored workspace should remain replayable after recovery"
        );
    }
}
