use super::harness::*;
use std::collections::HashMap;

fn parse_params(output: &str) -> HashMap<String, f64> {
    let mut params = HashMap::new();
    let parts: Vec<&str> = output.trim_start_matches('/').split('/').collect();
    let mut i = 0;
    while i + 1 < parts.len() {
        if let Ok(v) = parts[i + 1].parse::<f64>() {
            params.insert(parts[i].to_string(), v);
        }
        i += 2;
    }
    params
}

fn get_deltas(outputs: &[String]) -> Vec<f64> {
    outputs
        .iter()
        .map(|o| parse_params(o).get("delta").copied().unwrap_or(0.0))
        .collect()
}

fn get_durs(outputs: &[String]) -> Vec<f64> {
    outputs
        .iter()
        .map(|o| parse_params(o).get("dur").copied().unwrap_or(0.0))
        .collect()
}

fn get_sounds(outputs: &[String]) -> Vec<String> {
    outputs
        .iter()
        .map(|o| {
            let parts: Vec<&str> = o.trim_start_matches('/').split('/').collect();
            if parts.len() >= 2 && parts[0] == "sound" {
                parts[1].to_string()
            } else {
                String::new()
            }
        })
        .collect()
}

const EPSILON: f64 = 1e-9;

fn approx_eq(a: f64, b: f64) -> bool {
    (a - b).abs() < EPSILON
}

#[test]
fn stepdur_baseline() {
    let f = run("stepdur");
    assert!(approx_eq(stack_float(&f), 0.125));
}

#[test]
fn single_emit() {
    let outputs = expect_outputs(r#""kick" s ."#, 1);
    let deltas = get_deltas(&outputs);
    assert!(approx_eq(deltas[0], 0.0), "single emit at start should have delta 0");
}

#[test]
fn multiple_emits_all_at_zero() {
    let outputs = expect_outputs(r#""kick" s . . . ."#, 4);
    let deltas = get_deltas(&outputs);
    for (i, delta) in deltas.iter().enumerate() {
        assert!(approx_eq(*delta, 0.0), "emit {}: expected delta 0, got {}", i, delta);
    }
}

#[test]
fn sound_persists() {
    let outputs = expect_outputs(r#""kick" s . . "hat" s . ."#, 4);
    let sounds = get_sounds(&outputs);
    assert_eq!(sounds[0], "kick");
    assert_eq!(sounds[1], "kick");
    assert_eq!(sounds[2], "hat");
    assert_eq!(sounds[3], "hat");
}

#[test]
fn alternating_sounds() {
    let outputs = expect_outputs(r#""kick" s . "snare" s . "kick" s . "snare" s ."#, 4);
    let sounds = get_sounds(&outputs);
    assert_eq!(sounds, vec!["kick", "snare", "kick", "snare"]);
}

#[test]
fn dur_is_step_duration() {
    let outputs = expect_outputs(r#""kick" s ."#, 1);
    let durs = get_durs(&outputs);
    assert!(approx_eq(durs[0], 0.5), "dur should be 4 * step_duration (0.5), got {}", durs[0]);
}

#[test]
fn cycle_picks_by_runs() {
    for runs in 0..4 {
        let ctx = ctx_with(|c| c.runs = runs);
        let f = forth();
        let outputs = f.evaluate(r#""kick" s ( . ) ( ) 2 cycle"#, &ctx).unwrap();
        if runs % 2 == 0 {
            assert_eq!(outputs.len(), 1, "runs={}: emit should be picked", runs);
        } else {
            assert_eq!(outputs.len(), 0, "runs={}: no-op should be picked", runs);
        }
    }
}

#[test]
fn pcycle_picks_by_iter() {
    for iter in 0..4 {
        let ctx = ctx_with(|c| c.iter = iter);
        let f = forth();
        let outputs = f.evaluate(r#""kick" s ( . ) ( ) 2 pcycle"#, &ctx).unwrap();
        if iter % 2 == 0 {
            assert_eq!(outputs.len(), 1, "iter={}: emit should be picked", iter);
        } else {
            assert_eq!(outputs.len(), 0, "iter={}: no-op should be picked", iter);
        }
    }
}

#[test]
fn cycle_with_sounds() {
    for runs in 0..3 {
        let ctx = ctx_with(|c| c.runs = runs);
        let f = forth();
        let outputs = f.evaluate(
            r#"( "kick" s . ) ( "hat" s . ) ( "snare" s . ) 3 cycle"#,
            &ctx
        ).unwrap();
        assert_eq!(outputs.len(), 1, "runs={}: expected 1 output", runs);
        let sounds = get_sounds(&outputs);
        let expected = ["kick", "hat", "snare"][runs % 3];
        assert_eq!(sounds[0], expected, "runs={}: expected {}", runs, expected);
    }
}


#[test]
fn at_single_delta() {
    let outputs = expect_outputs(r#"0.5 at "kick" s ."#, 1);
    let deltas = get_deltas(&outputs);
    let step_dur = 0.125;
    assert!(approx_eq(deltas[0], 0.5 * step_dur), "expected delta at 0.5 of step, got {}", deltas[0]);
}

#[test]
fn at_list_deltas() {
    let outputs = expect_outputs(r#"0 0.5 at "kick" s ."#, 2);
    let deltas = get_deltas(&outputs);
    let step_dur = 0.125;
    assert!(approx_eq(deltas[0], 0.0), "expected delta 0, got {}", deltas[0]);
    assert!(approx_eq(deltas[1], 0.5 * step_dur), "expected delta at 0.5 of step, got {}", deltas[1]);
}

#[test]
fn at_three_deltas() {
    let outputs = expect_outputs(r#"0 0.33 0.67 at "kick" s ."#, 3);
    let deltas = get_deltas(&outputs);
    let step_dur = 0.125;
    assert!(approx_eq(deltas[0], 0.0), "expected delta 0");
    assert!((deltas[1] - 0.33 * step_dur).abs() < 0.001, "expected delta at 0.33 of step");
    assert!((deltas[2] - 0.67 * step_dur).abs() < 0.001, "expected delta at 0.67 of step");
}

#[test]
fn at_persists_across_emits() {
    let outputs = expect_outputs(r#"0 0.5 at "kick" s . "hat" s ."#, 4);
    let sounds = get_sounds(&outputs);
    assert_eq!(sounds, vec!["kick", "kick", "hat", "hat"]);
}


#[test]
fn at_reset_with_zero() {
    let outputs = expect_outputs(r#"0 0.5 at "kick" s . 0.0 at "hat" s ."#, 3);
    let sounds = get_sounds(&outputs);
    assert_eq!(sounds, vec!["kick", "kick", "hat"]);
}

#[test]
fn clear_resets_at_deltas() {
    let outputs = expect_outputs(r#"0 0.5 at "kick" s . clear "hat" s ."#, 3);
    let sounds = get_sounds(&outputs);
    assert_eq!(sounds, vec!["kick", "kick", "hat"]);
    let deltas = get_deltas(&outputs);
    assert!(approx_eq(deltas[2], 0.0), "after clear, hat should emit at delta 0, got {}", deltas[2]);
}

#[test]
fn at_records_selected_spans() {
    use cagire::forth::ExecutionTrace;

    let f = forth();
    let mut trace = ExecutionTrace::default();
    let script = r#"0 0.5 0.75 at "kick" s ."#;
    f.evaluate_with_trace(script, &default_ctx(), &mut trace).unwrap();

    // Should have 6 selected spans: 3 for at deltas + 3 for sound (one per emit)
    assert_eq!(trace.selected_spans.len(), 6, "expected 6 selected spans (3 at + 3 sound)");

    // Verify at delta spans (even indices: 0, 2, 4)
    assert_eq!(&script[trace.selected_spans[0].start as usize..trace.selected_spans[0].end as usize], "0");
    assert_eq!(&script[trace.selected_spans[2].start as usize..trace.selected_spans[2].end as usize], "0.5");
    assert_eq!(&script[trace.selected_spans[4].start as usize..trace.selected_spans[4].end as usize], "0.75");
}

// --- arp tests ---

fn get_notes(outputs: &[String]) -> Vec<f64> {
    outputs
        .iter()
        .map(|o| parse_params(o).get("note").copied().unwrap_or(0.0))
        .collect()
}

fn get_gains(outputs: &[String]) -> Vec<f64> {
    outputs
        .iter()
        .map(|o| parse_params(o).get("gain").copied().unwrap_or(f64::NAN))
        .collect()
}

#[test]
fn arp_auto_subdivide() {
    let outputs = expect_outputs(r#"sine s c4 e4 g4 b4 arp note ."#, 4);
    let notes = get_notes(&outputs);
    assert!(approx_eq(notes[0], 60.0));
    assert!(approx_eq(notes[1], 64.0));
    assert!(approx_eq(notes[2], 67.0));
    assert!(approx_eq(notes[3], 71.0));
    let deltas = get_deltas(&outputs);
    let step_dur = 0.125;
    assert!(approx_eq(deltas[0], 0.0));
    assert!(approx_eq(deltas[1], 0.25 * step_dur));
    assert!(approx_eq(deltas[2], 0.5 * step_dur));
    assert!(approx_eq(deltas[3], 0.75 * step_dur));
}

#[test]
fn arp_with_explicit_at() {
    let outputs = expect_outputs(r#"0 0.25 0.5 0.75 at sine s c4 e4 g4 b4 arp note ."#, 4);
    let notes = get_notes(&outputs);
    assert!(approx_eq(notes[0], 60.0));
    assert!(approx_eq(notes[1], 64.0));
    assert!(approx_eq(notes[2], 67.0));
    assert!(approx_eq(notes[3], 71.0));
    let deltas = get_deltas(&outputs);
    let step_dur = 0.125;
    assert!(approx_eq(deltas[0], 0.0));
    assert!(approx_eq(deltas[1], 0.25 * step_dur));
    assert!(approx_eq(deltas[2], 0.5 * step_dur));
    assert!(approx_eq(deltas[3], 0.75 * step_dur));
}

#[test]
fn arp_single_note() {
    let outputs = expect_outputs(r#"sine s c4 arp note ."#, 1);
    let notes = get_notes(&outputs);
    assert!(approx_eq(notes[0], 60.0));
}

#[test]
fn arp_fewer_deltas_than_notes() {
    let outputs = expect_outputs(r#"0 0.5 at sine s c4 e4 g4 b4 arp note ."#, 4);
    let notes = get_notes(&outputs);
    assert!(approx_eq(notes[0], 60.0));
    assert!(approx_eq(notes[1], 64.0));
    assert!(approx_eq(notes[2], 67.0));
    assert!(approx_eq(notes[3], 71.0));
    let deltas = get_deltas(&outputs);
    let step_dur = 0.125;
    assert!(approx_eq(deltas[0], 0.0));
    assert!(approx_eq(deltas[1], 0.5 * step_dur));
    assert!(approx_eq(deltas[2], 0.0));           // wraps: 2 % 2 = 0
    assert!(approx_eq(deltas[3], 0.5 * step_dur)); // wraps: 3 % 2 = 1
}

#[test]
fn arp_fewer_notes_than_deltas() {
    let outputs = expect_outputs(r#"0 0.25 0.5 0.75 at sine s c4 e4 arp note ."#, 4);
    let notes = get_notes(&outputs);
    assert!(approx_eq(notes[0], 60.0));
    assert!(approx_eq(notes[1], 64.0));
    assert!(approx_eq(notes[2], 60.0)); // wraps
    assert!(approx_eq(notes[3], 64.0)); // wraps
}

#[test]
fn arp_multiple_params() {
    let outputs = expect_outputs(r#"sine s c4 e4 g4 arp note 0.5 0.7 0.9 arp gain ."#, 3);
    let notes = get_notes(&outputs);
    assert!(approx_eq(notes[0], 60.0));
    assert!(approx_eq(notes[1], 64.0));
    assert!(approx_eq(notes[2], 67.0));
    let gains = get_gains(&outputs);
    assert!(approx_eq(gains[0], 0.5));
    assert!(approx_eq(gains[1], 0.7));
    assert!(approx_eq(gains[2], 0.9));
}

#[test]
fn arp_no_arp_unchanged() {
    // Standard CycleList without arp → cross-product (backward compat)
    let outputs = expect_outputs(r#"0 0.5 at sine s c4 e4 note ."#, 4);
    let notes = get_notes(&outputs);
    // Cross-product: each note at each delta
    assert!(approx_eq(notes[0], 60.0));
    assert!(approx_eq(notes[1], 60.0));
    assert!(approx_eq(notes[2], 64.0));
    assert!(approx_eq(notes[3], 64.0));
}

#[test]
fn arp_mixed_cycle_and_arp() {
    // CycleList sound (2) + ArpList note (3) → 3 arp × 2 poly = 6 voices
    // Each arp step plays both sine and saw simultaneously (poly stacking)
    let outputs = expect_outputs(r#"sine saw s c4 e4 g4 arp note ."#, 6);
    let sounds = get_sounds(&outputs);
    // Arp step 0: poly 0=sine, poly 1=saw
    assert_eq!(sounds[0], "sine");
    assert_eq!(sounds[1], "saw");
    // Arp step 1: poly 0=sine, poly 1=saw
    assert_eq!(sounds[2], "sine");
    assert_eq!(sounds[3], "saw");
    // Arp step 2: poly 0=sine, poly 1=saw
    assert_eq!(sounds[4], "sine");
    assert_eq!(sounds[5], "saw");
    let notes = get_notes(&outputs);
    // Both poly voices in each arp step share the same note
    assert!(approx_eq(notes[0], 60.0));
    assert!(approx_eq(notes[1], 60.0));
    assert!(approx_eq(notes[2], 64.0));
    assert!(approx_eq(notes[3], 64.0));
    assert!(approx_eq(notes[4], 67.0));
    assert!(approx_eq(notes[5], 67.0));
}

// --- every+ / except+ tests ---

#[test]
fn every_offset_fires_at_offset() {
    for iter in 0..8 {
        let ctx = ctx_with(|c| c.iter = iter);
        let f = forth();
        let outputs = f.evaluate(r#""kick" s ( . ) 4 2 every+"#, &ctx).unwrap();
        if iter % 4 == 2 {
            assert_eq!(outputs.len(), 1, "iter={}: should fire", iter);
        } else {
            assert_eq!(outputs.len(), 0, "iter={}: should not fire", iter);
        }
    }
}

#[test]
fn every_offset_wraps_large_offset() {
    // offset 6 with n=4 → 6 % 4 = 2, same as offset 2
    for iter in 0..8 {
        let ctx = ctx_with(|c| c.iter = iter);
        let f = forth();
        let outputs = f.evaluate(r#""kick" s ( . ) 4 6 every+"#, &ctx).unwrap();
        if iter % 4 == 2 {
            assert_eq!(outputs.len(), 1, "iter={}: should fire (wrapped offset)", iter);
        } else {
            assert_eq!(outputs.len(), 0, "iter={}: should not fire", iter);
        }
    }
}

#[test]
fn except_offset_inverse() {
    for iter in 0..8 {
        let ctx = ctx_with(|c| c.iter = iter);
        let f = forth();
        let outputs = f.evaluate(r#""kick" s ( . ) 4 2 except+"#, &ctx).unwrap();
        if iter % 4 != 2 {
            assert_eq!(outputs.len(), 1, "iter={}: should fire", iter);
        } else {
            assert_eq!(outputs.len(), 0, "iter={}: should not fire", iter);
        }
    }
}

#[test]
fn every_offset_zero_is_same_as_every() {
    for iter in 0..8 {
        let ctx = ctx_with(|c| c.iter = iter);
        let f = forth();
        let a = f.evaluate(r#""kick" s ( . ) 3 every"#, &ctx).unwrap();
        let b = f.evaluate(r#""kick" s ( . ) 3 0 every+"#, &ctx).unwrap();
        assert_eq!(a.len(), b.len(), "iter={}: every and every+ 0 should match", iter);
    }
}
