//! ZigZag state machine — the core streaming pivot detection engine.
//!
//! ## Repainting Contract
//!
//! - Exactly one pending pivot exists at all times (after initialization).
//! - Pending pivots update in-place as the current leg extends — this IS expected.
//! - Confirmation occurs when reversal ≥ τ: pending → Confirmed, new pending created.
//! - Once confirmed, a pivot NEVER changes (verified by proptest).
//! - Range bars are immune to MQL5's "clear last 2 pivots" issue because
//!   completed bar OHLC is final.

use crate::zigzag::base_class::{classify_base_class, classify_high_class, compute_z, compute_z_high};
use crate::zigzag::config::{ZigZagConfig, compute_epsilon, compute_tau};
use crate::zigzag::direction::{Direction, InitState, Phase};
use crate::zigzag::types::{BarInput, ConfirmationStatus, Formation, Pivot, PivotKind, Segment, ZigZagOutput};

/// Streaming ZigZag state machine.
///
/// # Example
///
/// ```
/// use qta::{ZigZagConfig, ZigZagState, BarInput};
///
/// let config = ZigZagConfig::new(3.0, 1.0, 250).unwrap();
/// let mut state = ZigZagState::new(config);
///
/// let bar = BarInput {
///     index: 0, timestamp_us: 1_000_000,
///     high: 5_012_500_000_000, low: 5_000_000_000_000,
///     close: 5_006_000_000_000, duration_us: Some(60_000_000),
/// };
/// let output = state.process_bar(&bar);
/// ```
pub struct ZigZagState {
    config: ZigZagConfig,
    phase: Phase,
    pending_price: i64,
    pending_index: usize,
    pending_timestamp_us: i64,
    reversal_threshold: i64,
    confirmed_pivots: Vec<Pivot>,
    confirmation_generation: u64,
    bars_processed: u64,
}

impl ZigZagState {
    #[must_use]
    pub fn new(config: ZigZagConfig) -> Self {
        Self {
            config,
            phase: Phase::Uninitialized(InitState {
                high: i64::MIN,
                high_index: 0,
                high_timestamp: 0,
                low: i64::MAX,
                low_index: 0,
                low_timestamp: 0,
            }),
            pending_price: 0,
            pending_index: 0,
            pending_timestamp_us: 0,
            reversal_threshold: 0,
            confirmed_pivots: Vec::new(),
            confirmation_generation: 0,
            bars_processed: 0,
        }
    }

    pub fn process_bar(&mut self, bar: &BarInput) -> ZigZagOutput {
        self.bars_processed += 1;
        match &self.phase {
            Phase::Uninitialized(_) => self.process_init(bar),
            Phase::Active(Direction::Up) => self.process_up(bar),
            Phase::Active(Direction::Down) => self.process_down(bar),
        }
    }

    #[must_use]
    pub fn confirmed_pivots(&self) -> &[Pivot] {
        &self.confirmed_pivots
    }

    #[must_use]
    pub fn pending(&self) -> Option<Pivot> {
        match &self.phase {
            Phase::Uninitialized(_) => None,
            Phase::Active(dir) => Some(Pivot {
                bar_index: self.pending_index,
                timestamp_us: self.pending_timestamp_us,
                price: self.pending_price,
                kind: match dir {
                    Direction::Up => PivotKind::High,
                    Direction::Down => PivotKind::Low,
                },
                status: ConfirmationStatus::Pending,
            }),
        }
    }

    #[must_use]
    pub fn is_initialized(&self) -> bool {
        matches!(self.phase, Phase::Active(_))
    }

    #[must_use]
    pub fn bars_processed(&self) -> u64 {
        self.bars_processed
    }

    #[must_use]
    pub fn config(&self) -> &ZigZagConfig {
        &self.config
    }

    // ── Initialization phase ─────────────────────────────────────────────

    fn process_init(&mut self, bar: &BarInput) -> ZigZagOutput {
        let init = match &mut self.phase {
            Phase::Uninitialized(s) => s,
            Phase::Active(_) => unreachable!(),
        };

        if init.high == i64::MIN {
            *init = InitState::from_bar(bar);
            return ZigZagOutput {
                pending: None,
                newly_confirmed: None,
                pending_updated: false,
                completed_segment: None,
                completed_formation: None,
            };
        }

        init.update(bar);

        // Copy init values before mutating self (satisfies borrow checker)
        let ih = init.high;
        let ih_idx = init.high_index;
        let ih_ts = init.high_timestamp;
        let il = init.low;
        let il_idx = init.low_index;
        let il_ts = init.low_timestamp;

        // Reversal from high (drop ≥ τ → first pivot is High)
        let tau_high = compute_tau(&self.config, ih);
        if ih - bar.low >= tau_high {
            let pivot = self.confirm_pivot(ih_idx, ih_ts, ih, PivotKind::High, bar.index);
            self.phase = Phase::Active(Direction::Down);
            self.pending_price = bar.low;
            self.pending_index = bar.index;
            self.pending_timestamp_us = bar.timestamp_us;
            self.reversal_threshold = compute_tau(&self.config, bar.low);

            tracing::debug!(
                kind = "High",
                price = ih,
                bar = ih_idx,
                "first pivot (init → Down)"
            );

            return ZigZagOutput {
                pending: self.pending(),
                newly_confirmed: Some(pivot),
                pending_updated: false,
                completed_segment: None,
                completed_formation: None,
            };
        }

        // Reversal from low (rise ≥ τ → first pivot is Low)
        let tau_low = compute_tau(&self.config, il);
        if bar.high - il >= tau_low {
            let pivot = self.confirm_pivot(il_idx, il_ts, il, PivotKind::Low, bar.index);
            self.phase = Phase::Active(Direction::Up);
            self.pending_price = bar.high;
            self.pending_index = bar.index;
            self.pending_timestamp_us = bar.timestamp_us;
            self.reversal_threshold = compute_tau(&self.config, bar.high);

            tracing::debug!(
                kind = "Low",
                price = il,
                bar = il_idx,
                "first pivot (init → Up)"
            );

            return ZigZagOutput {
                pending: self.pending(),
                newly_confirmed: Some(pivot),
                pending_updated: false,
                completed_segment: None,
                completed_formation: None,
            };
        }

        ZigZagOutput {
            pending: None,
            newly_confirmed: None,
            pending_updated: false,
            completed_segment: None,
            completed_formation: None,
        }
    }

    // ── Active phase: tracking toward a swing extreme ──────────────────

    fn process_up(&mut self, bar: &BarInput) -> ZigZagOutput {
        self.process_active(
            bar,
            bar.high,
            bar.low,
            PivotKind::High,
            Direction::Down,
            false,
        )
    }

    fn process_down(&mut self, bar: &BarInput) -> ZigZagOutput {
        self.process_active(bar, bar.low, bar.high, PivotKind::Low, Direction::Up, true)
    }

    /// Unified active-phase logic for both Up and Down directions.
    ///
    /// - `extend_price`: the bar extreme that can extend the pending pivot (high for Up, low for Down).
    /// - `reversal_price`: the bar extreme that can trigger a reversal (low for Up, high for Down).
    /// - `confirm_kind`: the pivot kind to confirm on reversal (High for Up, Low for Down).
    /// - `new_direction`: the direction to enter after reversal (Down for Up, Up for Down).
    /// - `form_segment`: whether to attempt segment formation on reversal.
    ///   Only Down→Up reversals form segments (Low-High-Low triplets).
    fn process_active(
        &mut self,
        bar: &BarInput,
        extend_price: i64,
        reversal_price: i64,
        confirm_kind: PivotKind,
        new_direction: Direction,
        form_segment: bool,
    ) -> ZigZagOutput {
        let mut pending_updated = false;

        let extends = match confirm_kind {
            PivotKind::High => extend_price > self.pending_price,
            PivotKind::Low => extend_price < self.pending_price,
        };

        if extends {
            self.pending_price = extend_price;
            self.pending_index = bar.index;
            self.pending_timestamp_us = bar.timestamp_us;
            self.reversal_threshold = compute_tau(&self.config, self.pending_price);
            pending_updated = true;
        }

        if (self.pending_price - reversal_price).abs() >= self.reversal_threshold {
            let pivot = self.confirm_pivot(
                self.pending_index,
                self.pending_timestamp_us,
                self.pending_price,
                confirm_kind,
                bar.index,
            );
            self.phase = Phase::Active(new_direction);
            self.pending_price = reversal_price;
            self.pending_index = bar.index;
            self.pending_timestamp_us = bar.timestamp_us;
            self.reversal_threshold = compute_tau(&self.config, reversal_price);

            let (segment, formation) = if form_segment {
                (self.try_form_segment(), self.try_form_formation())
            } else {
                (None, None)
            };

            tracing::debug!(
                kind = ?confirm_kind,
                price = pivot.price,
                gen = self.confirmation_generation,
                seg = segment.is_some(),
                fmt = formation.is_some(),
                "pivot confirmed"
            );

            return ZigZagOutput {
                pending: self.pending(),
                newly_confirmed: Some(pivot),
                pending_updated: false,
                completed_segment: segment,
                completed_formation: formation,
            };
        }

        ZigZagOutput {
            pending: self.pending(),
            newly_confirmed: None,
            pending_updated,
            completed_segment: None,
            completed_formation: None,
        }
    }

    // ── Helpers ──────────────────────────────────────────────────────────

    fn confirm_pivot(
        &mut self,
        bar_index: usize,
        timestamp_us: i64,
        price: i64,
        kind: PivotKind,
        confirmed_at_bar: usize,
    ) -> Pivot {
        self.confirmation_generation += 1;
        let pivot = Pivot {
            bar_index,
            timestamp_us,
            price,
            kind,
            status: ConfirmationStatus::Confirmed {
                confirmed_at_bar,
                generation: self.confirmation_generation,
            },
        };
        self.confirmed_pivots.push(pivot);
        pivot
    }

    fn try_form_segment(&self) -> Option<Segment> {
        let len = self.confirmed_pivots.len();
        if len < 3 {
            return None;
        }

        let l2 = &self.confirmed_pivots[len - 1];
        let h1 = &self.confirmed_pivots[len - 2];
        let l0 = &self.confirmed_pivots[len - 3];

        if l0.kind != PivotKind::Low || h1.kind != PivotKind::High || l2.kind != PivotKind::Low {
            return None;
        }

        let segment_size = h1.price - l0.price;
        if segment_size <= 0 {
            return None;
        }

        let z = compute_z(l0.price, h1.price, l2.price)?;
        let epsilon = compute_epsilon(&self.config, l0.price);
        let base_class = classify_base_class(l0.price, l2.price, epsilon);

        Some(Segment {
            l0: *l0,
            h1: *h1,
            l2: *l2,
            segment_size,
            z,
            base_class,
        })
    }

    /// Attempt to form a L₀→H₁→L₂→H₃ formation from the last 5 confirmed pivots.
    ///
    /// Called on Down→Up reversals (when a Low is confirmed). The formation uses
    /// pivots[-5]=L₀, [-4]=H₁, [-3]=L₂, [-2]=H₃ (the 4 pivots BEFORE the
    /// newly confirmed Low that triggered the segment).
    fn try_form_formation(&self) -> Option<Formation> {
        let len = self.confirmed_pivots.len();
        if len < 5 {
            return None;
        }

        let l0 = &self.confirmed_pivots[len - 5];
        let h1 = &self.confirmed_pivots[len - 4];
        let l2 = &self.confirmed_pivots[len - 3];
        let h3 = &self.confirmed_pivots[len - 2];

        if l0.kind != PivotKind::Low
            || h1.kind != PivotKind::High
            || l2.kind != PivotKind::Low
            || h3.kind != PivotKind::High
        {
            return None;
        }

        let first_leg_size = h1.price - l0.price;
        let second_leg_size = h3.price - l2.price;
        if first_leg_size <= 0 || second_leg_size <= 0 {
            return None;
        }

        let z_low = compute_z(l0.price, h1.price, l2.price)?;
        let z_high = compute_z_high(h1.price, l2.price, h3.price)?;

        let epsilon_low = compute_epsilon(&self.config, l0.price);
        let epsilon_high = compute_epsilon(&self.config, h1.price);
        let base_class = classify_base_class(l0.price, l2.price, epsilon_low);
        let high_class = classify_high_class(h1.price, h3.price, epsilon_high);

        Some(Formation {
            l0: *l0,
            h1: *h1,
            l2: *l2,
            h3: *h3,
            base_class,
            high_class,
            z_low,
            z_high,
            first_leg_size,
            second_leg_size,
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::zigzag::types::BaseClass;

    const SCALE: i64 = 100_000_000;

    fn fp(value: f64) -> i64 {
        (value * SCALE as f64).round() as i64
    }

    fn make_bar(index: usize, high: f64, low: f64, close: f64) -> BarInput {
        BarInput {
            index,
            timestamp_us: (index as i64) * 1_000_000,
            high: fp(high),
            low: fp(low),
            close: fp(close),
            duration_us: Some(60_000_000),
        }
    }

    fn test_config() -> ZigZagConfig {
        ZigZagConfig::new(3.0, 1.0, 250).unwrap()
    }

    #[test]
    fn test_initialization_starts_uninitialized() {
        let state = ZigZagState::new(test_config());
        assert!(!state.is_initialized());
        assert!(state.pending().is_none());
        assert!(state.confirmed_pivots().is_empty());
    }

    #[test]
    fn test_first_bar_seeds_init() {
        let mut state = ZigZagState::new(test_config());
        let output = state.process_bar(&make_bar(0, 50125.0, 50000.0, 50060.0));
        assert!(!state.is_initialized());
        assert!(!output.has_event());
    }

    #[test]
    fn test_upward_reversal_initializes() {
        let mut state = ZigZagState::new(test_config());
        state.process_bar(&make_bar(0, 50100.0, 50000.0, 50050.0));
        let output = state.process_bar(&make_bar(1, 50400.0, 50200.0, 50300.0));
        assert!(state.is_initialized());
        let pivot = output.newly_confirmed.unwrap();
        assert_eq!(pivot.kind, PivotKind::Low);
        assert_eq!(pivot.price, fp(50000.0));
    }

    #[test]
    fn test_downward_reversal_initializes() {
        let mut state = ZigZagState::new(test_config());
        state.process_bar(&make_bar(0, 50500.0, 50400.0, 50450.0));
        let output = state.process_bar(&make_bar(1, 50200.0, 50100.0, 50150.0));
        assert!(state.is_initialized());
        let pivot = output.newly_confirmed.unwrap();
        assert_eq!(pivot.kind, PivotKind::High);
        assert_eq!(pivot.price, fp(50500.0));
    }

    #[test]
    fn test_alternating_pivots() {
        let mut state = ZigZagState::new(test_config());
        let mut kinds: Vec<PivotKind> = Vec::new();
        let bars = vec![
            make_bar(0, 50100.0, 50000.0, 50050.0),
            make_bar(1, 50400.0, 50200.0, 50300.0),
            make_bar(2, 50600.0, 50400.0, 50500.0),
            make_bar(3, 50700.0, 50500.0, 50600.0),
            make_bar(4, 50300.0, 50100.0, 50200.0),
            make_bar(5, 50100.0, 49900.0, 50000.0),
            make_bar(6, 50500.0, 50300.0, 50400.0),
        ];
        for bar in &bars {
            if let Some(p) = state.process_bar(bar).newly_confirmed {
                kinds.push(p.kind);
            }
        }
        for w in kinds.windows(2) {
            assert_ne!(w[0], w[1]);
        }
    }

    #[test]
    fn test_pending_updates() {
        let mut state = ZigZagState::new(test_config());
        state.process_bar(&make_bar(0, 50100.0, 50000.0, 50050.0));
        state.process_bar(&make_bar(1, 50400.0, 50200.0, 50300.0));
        let p1 = state.pending().unwrap();
        assert_eq!(p1.kind, PivotKind::High);
        let output = state.process_bar(&make_bar(2, 50500.0, 50350.0, 50450.0));
        assert!(output.pending_updated);
        assert_eq!(state.pending().unwrap().price, fp(50500.0));
    }

    #[test]
    fn test_segment_formation() {
        let mut state = ZigZagState::new(test_config());
        let mut segments = Vec::new();
        let bars = vec![
            make_bar(0, 50100.0, 50000.0, 50050.0),
            make_bar(1, 50500.0, 50300.0, 50400.0),
            make_bar(2, 50800.0, 50600.0, 50700.0),
            make_bar(3, 51000.0, 50800.0, 50900.0),
            make_bar(4, 50700.0, 50400.0, 50500.0),
            make_bar(5, 50300.0, 50100.0, 50200.0),
            make_bar(6, 50600.0, 50400.0, 50500.0),
        ];
        for bar in &bars {
            if let Some(seg) = state.process_bar(bar).completed_segment {
                segments.push(seg);
            }
        }
        assert!(!segments.is_empty());
        let seg = &segments[0];
        assert_eq!(seg.l0.kind, PivotKind::Low);
        assert_eq!(seg.h1.kind, PivotKind::High);
        assert_eq!(seg.l2.kind, PivotKind::Low);
        assert!(seg.segment_size > 0);
    }

    #[test]
    fn test_generation_monotonic() {
        let mut state = ZigZagState::new(test_config());
        let mut gens: Vec<u64> = Vec::new();
        let bars = vec![
            make_bar(0, 50100.0, 50000.0, 50050.0),
            make_bar(1, 50500.0, 50200.0, 50300.0),
            make_bar(2, 51000.0, 50800.0, 50900.0),
            make_bar(3, 50500.0, 50200.0, 50300.0),
            make_bar(4, 50100.0, 49800.0, 49900.0),
            make_bar(5, 50500.0, 50300.0, 50400.0),
        ];
        for bar in &bars {
            if let Some(p) = state.process_bar(bar).newly_confirmed {
                if let ConfirmationStatus::Confirmed { generation, .. } = p.status {
                    gens.push(generation);
                }
            }
        }
        for w in gens.windows(2) {
            assert!(w[1] > w[0]);
        }
    }

    #[test]
    fn test_no_event_on_quiet_bar() {
        let mut state = ZigZagState::new(test_config());
        state.process_bar(&make_bar(0, 50100.0, 50000.0, 50050.0));
        state.process_bar(&make_bar(1, 50500.0, 50200.0, 50300.0));
        let output = state.process_bar(&make_bar(2, 50450.0, 50250.0, 50350.0));
        assert!(!output.pending_updated);
        assert!(output.newly_confirmed.is_none());
    }

    #[test]
    fn test_up_reversal_no_segment() {
        // Up→Down reversals (confirming High pivots) must NEVER produce a segment.
        // This guards the `form_segment: false` asymmetry in process_active.
        let mut state = ZigZagState::new(test_config());
        let bars = vec![
            make_bar(0, 50100.0, 50000.0, 50050.0),
            make_bar(1, 50500.0, 50200.0, 50300.0), // init → Up (Low confirmed)
            make_bar(2, 50800.0, 50600.0, 50700.0),
            make_bar(3, 51000.0, 50800.0, 50900.0), // pending High extends
            make_bar(4, 50700.0, 50400.0, 50500.0),
            make_bar(5, 50300.0, 50100.0, 50200.0), // Up→Down reversal (High confirmed)
            make_bar(6, 50600.0, 50400.0, 50500.0),
            make_bar(7, 50900.0, 50700.0, 50800.0), // Down→Up reversal (Low confirmed, segment possible)
            make_bar(8, 51200.0, 51000.0, 51100.0),
            make_bar(9, 51400.0, 51200.0, 51300.0), // pending High extends
            make_bar(10, 51000.0, 50700.0, 50800.0),
            make_bar(11, 50600.0, 50400.0, 50500.0), // Up→Down reversal with ≥3 pivots
        ];
        for bar in &bars {
            let output = state.process_bar(bar);
            if let Some(ref pivot) = output.newly_confirmed {
                if pivot.kind == PivotKind::High {
                    // Up→Down reversal confirming a High must never produce a segment.
                    assert!(
                        output.completed_segment.is_none(),
                        "Up→Down reversal at bar {} produced a segment (should never happen)",
                        bar.index
                    );
                }
            }
        }
        // Verify we actually confirmed at least one High pivot.
        let high_count = state
            .confirmed_pivots()
            .iter()
            .filter(|p| p.kind == PivotKind::High)
            .count();
        assert!(high_count >= 2, "expected ≥2 High pivots, got {high_count}");
    }

    #[test]
    fn test_down_reversal_produces_segment() {
        // Down→Up reversals (confirming Low pivots) SHOULD produce a segment when ≥3 pivots exist.
        // This guards the `form_segment: true` asymmetry in process_active.
        let mut state = ZigZagState::new(test_config());
        let mut got_segment = false;
        let bars = vec![
            make_bar(0, 50100.0, 50000.0, 50050.0),
            make_bar(1, 50500.0, 50300.0, 50400.0), // init → Up
            make_bar(2, 50800.0, 50600.0, 50700.0),
            make_bar(3, 51000.0, 50800.0, 50900.0),
            make_bar(4, 50700.0, 50400.0, 50500.0),
            make_bar(5, 50300.0, 50100.0, 50200.0), // High confirmed (pivot 2)
            make_bar(6, 50600.0, 50400.0, 50500.0),
            make_bar(7, 50900.0, 50700.0, 50800.0), // Low confirmed (pivot 3) → segment
        ];
        for bar in &bars {
            let output = state.process_bar(bar);
            if output.completed_segment.is_some() {
                got_segment = true;
                let seg = output.completed_segment.unwrap();
                assert_eq!(seg.l0.kind, PivotKind::Low);
                assert_eq!(seg.h1.kind, PivotKind::High);
                assert_eq!(seg.l2.kind, PivotKind::Low);
            }
        }
        assert!(
            got_segment,
            "expected a segment from Down→Up reversal with ≥3 pivots"
        );
    }

    #[test]
    fn test_pending_updates_down() {
        // Pending updates in the Down direction (existing test only covers Up).
        let mut state = ZigZagState::new(test_config());
        // Initialize: seed → upward reversal → Up phase
        state.process_bar(&make_bar(0, 50500.0, 50400.0, 50450.0));
        state.process_bar(&make_bar(1, 50200.0, 50100.0, 50150.0)); // init → Down (High confirmed)
        assert!(state.is_initialized());
        let p1 = state.pending().unwrap();
        assert_eq!(p1.kind, PivotKind::Low);

        // Feed a lower low — pending should update downward.
        let output = state.process_bar(&make_bar(2, 50150.0, 50050.0, 50100.0));
        assert!(output.pending_updated);
        assert_eq!(state.pending().unwrap().price, fp(50050.0));

        // Feed another lower low — pending should update again.
        let output2 = state.process_bar(&make_bar(3, 50100.0, 49950.0, 50000.0));
        assert!(output2.pending_updated);
        assert_eq!(state.pending().unwrap().price, fp(49950.0));

        // Feed a bar that doesn't make a new low — no update.
        let output3 = state.process_bar(&make_bar(4, 50050.0, 49980.0, 50000.0));
        assert!(!output3.pending_updated);
    }

    #[test]
    fn test_base_class_assignment() {
        let mut state = ZigZagState::new(test_config());
        let mut segments = Vec::new();
        let bars = vec![
            make_bar(0, 50100.0, 50000.0, 50050.0),
            make_bar(1, 50500.0, 50200.0, 50400.0),
            make_bar(2, 50800.0, 50600.0, 50700.0),
            make_bar(3, 51100.0, 50900.0, 51000.0),
            make_bar(4, 50700.0, 50400.0, 50500.0),
            make_bar(5, 50500.0, 50300.0, 50400.0),
            make_bar(6, 50900.0, 50700.0, 50800.0),
        ];
        for bar in &bars {
            if let Some(seg) = state.process_bar(bar).completed_segment {
                segments.push(seg);
            }
        }
        if !segments.is_empty() {
            let seg = &segments[0];
            if seg.l2.price > seg.l0.price {
                assert_eq!(seg.base_class, BaseClass::HL);
                assert!(seg.z > 0.0 && seg.z < 1.0);
            }
        }
    }

    #[test]
    fn test_formation_detection() {
        use crate::zigzag::types::{HighClass, Formation};

        let mut state = ZigZagState::new(test_config());
        let mut formations: Vec<Formation> = Vec::new();
        // Need 5 confirmed pivots (L0, H1, L2, H3, L4) to form a formation.
        // tau(50000)=375, eps(50000)=125. L2 must be > 50125 for HL.
        // tau(51000)=382.5. Down move from 51000 must exceed 382.5.
        // So pending Low must be < 51000-382.5=50617.5. Keep above 50125.
        let bars = vec![
            make_bar(0,  50100.0, 50000.0, 50050.0),
            make_bar(1,  50500.0, 50200.0, 50400.0),  // init → Up (L0~50000 confirmed)
            make_bar(2,  50800.0, 50600.0, 50700.0),
            make_bar(3,  51000.0, 50800.0, 50900.0),  // extending High
            make_bar(4,  50700.0, 50500.0, 50600.0),
            make_bar(5,  50500.0, 50350.0, 50400.0),  // H1~51000 confirmed → Down
            make_bar(6,  50700.0, 50500.0, 50600.0),
            make_bar(7,  50900.0, 50700.0, 50800.0),  // L2~50350 confirmed → segment (HL: 50350-50000=350>125)
            make_bar(8,  51200.0, 51000.0, 51100.0),
            make_bar(9,  51500.0, 51300.0, 51400.0),  // extending High
            make_bar(10, 51100.0, 50900.0, 51000.0),
            make_bar(11, 50800.0, 50600.0, 50700.0),  // H3~51500 confirmed → Down
            make_bar(12, 51000.0, 50800.0, 50900.0),
            make_bar(13, 51200.0, 51000.0, 51100.0),  // L4 confirmed → formation fires
        ];
        for bar in &bars {
            let output = state.process_bar(bar);
            if let Some(f) = output.completed_formation {
                formations.push(f);
            }
        }

        assert!(
            !formations.is_empty(),
            "expected at least one formation from 5+ pivots, got none. \
             confirmed pivots: {}",
            state.confirmed_pivots().len()
        );

        let f = &formations[0];
        assert_eq!(f.l0.kind, PivotKind::Low);
        assert_eq!(f.h1.kind, PivotKind::High);
        assert_eq!(f.l2.kind, PivotKind::Low);
        assert_eq!(f.h3.kind, PivotKind::High);
        assert!(f.first_leg_size > 0);
        assert!(f.second_leg_size > 0);
        // L2 > L0 → HL
        assert_eq!(f.base_class, BaseClass::HL);
        // H3 > H1 → HH
        assert_eq!(f.high_class, HighClass::HH);
    }
}
