//! Buffer-trim helpers for streaming transcription.
//!
//! Brief item #25: replace the current `OVERLAP_SAMPLES`-based drain
//! in `src-tauri/src/commands/live.rs` with a trim tied to the last
//! commit point emitted by the `CommitPolicy`. This keeps the capture
//! buffer bounded regardless of wall-clock session length (ufal #120 /
//! #102) by guaranteeing that any sample already committed to the
//! transcript is never kept in the working buffer.
//!
//! The helpers here are pure — they don't know about the live session
//! loop. Integration into `live.rs` ships as a follow-up after the
//! LocalAgreement wiring (#24) is dogfooded.

/// Absolute sample index at the end of the given session-relative
/// seconds mark, rounded to the nearest sample. `end_secs` typically
/// comes from `LocalAgreement::last_committed_end_secs()`.
///
/// Guards against non-finite inputs: NaN and ±infinity both return 0
/// ("nothing committed yet"). Without this, Rust's saturating
/// float-to-int cast turns `f64::INFINITY` into `u64::MAX`, which
/// would park the capture buffer origin at an index beyond any
/// reachable sample and trim the entire buffer forever.
pub fn sample_index_for_seconds(end_secs: f64, sample_rate: u32) -> u64 {
    if !end_secs.is_finite() || end_secs <= 0.0 {
        return 0;
    }
    (end_secs * sample_rate as f64).round() as u64
}

/// Drain the prefix of `buffer` whose absolute sample indices fall
/// below `commit_sample_index`. `buffer_start_sample` is the absolute
/// index of `buffer[0]` before the trim.
///
/// Returns the new `buffer_start_sample`. If the commit point is
/// before or equal to `buffer_start_sample`, nothing is drained.
/// If the commit point is beyond the current end of the buffer, the
/// whole buffer is drained and the new start is set to the commit
/// index — the buffer is still empty, but its absolute-index origin
/// moves forward so subsequent samples are positioned correctly.
pub fn trim_buffer_to_commit_point(
    buffer: &mut Vec<f32>,
    buffer_start_sample: u64,
    commit_sample_index: u64,
) -> u64 {
    if commit_sample_index <= buffer_start_sample {
        return buffer_start_sample;
    }
    let drain_count = (commit_sample_index - buffer_start_sample) as usize;
    if drain_count >= buffer.len() {
        buffer.clear();
        return commit_sample_index;
    }
    buffer.drain(..drain_count);
    buffer_start_sample + drain_count as u64
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn sample_index_for_seconds_zero_is_zero() {
        assert_eq!(sample_index_for_seconds(0.0, 16_000), 0);
    }

    #[test]
    fn sample_index_for_seconds_negative_is_zero() {
        // Defensive: end_secs should never be negative, but if it is
        // (clock skew in a future f64 source) treat as "nothing
        // committed yet" rather than wrapping to a huge u64.
        assert_eq!(sample_index_for_seconds(-1.0, 16_000), 0);
    }

    #[test]
    fn sample_index_for_seconds_rejects_nan_and_infinity() {
        // Defensive against non-finite inputs: without the is_finite()
        // check, Rust's saturating float-to-int cast makes +infinity
        // become u64::MAX, which would park the buffer origin beyond
        // reach and trim the whole buffer forever.
        assert_eq!(sample_index_for_seconds(f64::NAN, 16_000), 0);
        assert_eq!(sample_index_for_seconds(f64::INFINITY, 16_000), 0);
        assert_eq!(sample_index_for_seconds(f64::NEG_INFINITY, 16_000), 0);
    }

    #[test]
    fn sample_index_for_seconds_rounds_nearest() {
        // 0.5 s at 16 kHz = 8000 samples exactly.
        assert_eq!(sample_index_for_seconds(0.5, 16_000), 8_000);
        // Round-nearest: 0.50003 s × 16 kHz = 8000.48 → 8000.
        assert_eq!(sample_index_for_seconds(0.50003, 16_000), 8_000);
        // 0.5001 s × 16 kHz = 8001.6 → 8002.
        assert_eq!(sample_index_for_seconds(0.5001, 16_000), 8_002);
    }

    #[test]
    fn trim_does_nothing_when_commit_is_before_buffer_start() {
        let mut buf = vec![1.0, 2.0, 3.0];
        let new_start = trim_buffer_to_commit_point(&mut buf, 1000, 500);
        assert_eq!(new_start, 1000);
        assert_eq!(buf, vec![1.0, 2.0, 3.0]);
    }

    #[test]
    fn trim_does_nothing_when_commit_equals_buffer_start() {
        let mut buf = vec![1.0, 2.0, 3.0];
        let new_start = trim_buffer_to_commit_point(&mut buf, 1000, 1000);
        assert_eq!(new_start, 1000);
        assert_eq!(buf, vec![1.0, 2.0, 3.0]);
    }

    #[test]
    fn trim_drains_prefix_when_commit_is_inside_buffer() {
        let mut buf = vec![1.0, 2.0, 3.0, 4.0, 5.0];
        // buffer starts at absolute index 100, commit is at 102.
        // Drain 2 samples; remaining buffer starts at 102.
        let new_start = trim_buffer_to_commit_point(&mut buf, 100, 102);
        assert_eq!(new_start, 102);
        assert_eq!(buf, vec![3.0, 4.0, 5.0]);
    }

    #[test]
    fn trim_clears_buffer_when_commit_is_at_buffer_end() {
        let mut buf = vec![1.0, 2.0, 3.0];
        // buffer is [100, 103). commit at 103 means every sample is
        // committed — drain all, start moves forward.
        let new_start = trim_buffer_to_commit_point(&mut buf, 100, 103);
        assert_eq!(new_start, 103);
        assert!(buf.is_empty());
    }

    #[test]
    fn trim_clears_buffer_when_commit_is_past_buffer_end() {
        let mut buf = vec![1.0, 2.0, 3.0];
        // Commit well beyond the buffer — this happens in rare edge
        // cases where the committer's notion of time outstrips the
        // current buffer (e.g. after a reset). Defensive: drain and
        // park the origin at the commit point.
        let new_start = trim_buffer_to_commit_point(&mut buf, 100, 200);
        assert_eq!(new_start, 200);
        assert!(buf.is_empty());
    }

    #[test]
    fn trim_bounds_buffer_over_long_session() {
        // Simulate a committer that keeps up with capture: each cycle
        // feeds 16_000 samples and commits all but a 200-sample
        // tentative tail. Over 100 cycles the buffer must stay near
        // that tentative envelope — not accumulate 100 × 16_000 samples
        // as it would without the commit-point trim.
        //
        // The tentative tail stacks by 200 per cycle because each new
        // push extends the buffer BEFORE the trim runs against the
        // previous cycle's commit point, so the expected bound is
        // (tentative_per_cycle + new_push_minus_commit), not just
        // tentative_per_cycle.
        let mut buf: Vec<f32> = Vec::new();
        let mut start: u64 = 0;
        let mut total_pushed: u64 = 0;
        let tentative_per_cycle: u64 = 200;
        for _ in 0..100 {
            buf.extend(std::iter::repeat_n(0.25_f32, 16_000));
            total_pushed += 16_000;
            let commit_point = total_pushed - tentative_per_cycle;
            start = trim_buffer_to_commit_point(&mut buf, start, commit_point);
        }
        assert!(
            buf.len() as u64 <= 2 * tentative_per_cycle,
            "buffer outgrew the commit-bounded envelope: len = {} (bound {})",
            buf.len(),
            2 * tentative_per_cycle
        );
    }

    #[test]
    fn integrates_with_local_agreement_last_committed_end_secs() {
        use super::super::commit_policy::{LocalAgreement, Token};

        let mut la = LocalAgreement::new(2);
        let _ = la.push(vec![Token {
            text: "hello".into(),
            start_secs: 0.0,
            end_secs: 0.5,
        }]);
        let _ = la.push(vec![
            Token {
                text: "hello".into(),
                start_secs: 0.0,
                end_secs: 0.5,
            },
            Token {
                text: "world".into(),
                start_secs: 0.5,
                end_secs: 1.0,
            },
        ]);
        // "hello" is committed, ending at 0.5 s.
        let commit_idx = sample_index_for_seconds(la.last_committed_end_secs(), 16_000);
        assert_eq!(commit_idx, 8_000);

        // Simulate a capture buffer that has received 1.2 s of audio
        // starting at t=0.
        let mut buf: Vec<f32> = std::iter::repeat_n(0.1_f32, 19_200).collect();
        let new_start = trim_buffer_to_commit_point(&mut buf, 0, commit_idx);
        assert_eq!(new_start, 8_000);
        assert_eq!(buf.len(), 19_200 - 8_000);
    }
}
