use super::*;
use crate::{partial_polyline, partial_shape, partial_vector_path};

fn approx(a: f32, b: f32, tol: f32) -> bool {
    (a - b).abs() <= tol
}

#[test]
fn partial_polyline_half_interpolates_midpoint() {
    let pts = vec![(0.0, 0.0), (1.0, 0.0), (2.0, 0.0), (3.0, 0.0)];
    let out = partial_polyline(&pts, 0.5);
    let last = *out.last().unwrap();
    assert!(approx(last.0, 1.5, 1e-5));
    assert!(approx(last.1, 0.0, 1e-5));
}

#[test]
fn partial_polyline_zero_returns_first() {
    let pts = vec![(0.0, 0.0), (1.0, 0.0)];
    let out = partial_polyline(&pts, 0.0);
    assert_eq!(out.len(), 1);
    assert_eq!(out[0], (0.0, 0.0));
}

#[test]
fn partial_polyline_one_returns_full() {
    let pts = vec![(0.0, 0.0), (1.0, 0.0), (2.0, 0.0)];
    let out = partial_polyline(&pts, 1.0);
    assert_eq!(out, pts);
}

#[test]
fn partial_vector_path_quad_half_has_half_arc_length() {
    let path = VectorPath {
        commands: vec![
            PathCommand::MoveTo([0.0, 0.0]),
            PathCommand::QuadTo {
                ctrl: [1.0, 1.0],
                to: [2.0, 0.0],
            },
        ],
        color: Color::WHITE,
    };
    let full = partial_vector_path(&path, 1.0);
    let half = partial_vector_path(&path, 0.5);
    let full_len = path_length(&full);
    let half_len = path_length(&half);
    assert!(
        approx(half_len, full_len * 0.5, 1e-3),
        "expected half_len ~= full_len/2 (full={full_len}, half={half_len})"
    );
}

#[test]
fn partial_vector_path_zero_returns_single_moveto() {
    let path = VectorPath {
        commands: vec![
            PathCommand::MoveTo([1.0, 2.0]),
            PathCommand::LineTo([3.0, 4.0]),
        ],
        color: Color::WHITE,
    };
    let out = partial_vector_path(&path, 0.0);
    assert_eq!(out.commands.len(), 1);
    match out.commands[0] {
        PathCommand::MoveTo([x, y]) => {
            assert!(approx(x, 1.0, 1e-5));
            assert!(approx(y, 2.0, 1e-5));
        }
        _ => panic!("expected MoveTo at ratio 0"),
    }
}

#[test]
fn partial_vector_path_one_matches_input_arc_length() {
    let path = VectorPath {
        commands: vec![
            PathCommand::MoveTo([0.0, 0.0]),
            PathCommand::CubicTo {
                ctrl1: [0.5, 1.0],
                ctrl2: [1.5, -1.0],
                to: [2.0, 0.0],
            },
        ],
        color: Color::WHITE,
    };
    let out = partial_vector_path(&path, 1.0);
    assert_eq!(out, path);
}

#[test]
fn show_creation_distributes_alpha_across_two_targets() {
    let shape_a = SceneShape::new(
        1,
        Shape::Line(Line {
            x1: 0.0,
            y1: 0.0,
            x2: 1.0,
            y2: 0.0,
            thickness: 0.01,
            color: Color::WHITE,
        }),
    );
    let shape_b = SceneShape::new(
        2,
        Shape::Line(Line {
            x1: 0.0,
            y1: 0.0,
            x2: 1.0,
            y2: 0.0,
            thickness: 0.01,
            color: Color::WHITE,
        }),
    );
    let anim = Animation::ShowCreation {
        targets: vec![shape_a, shape_b],
    };
    let result = anim.apply(&[], 0.5);
    assert_eq!(result.len(), 2);
    // With lag_ratio = 0.7 and n = 2, piece 0 covers [0, 1/1.7] ≈ [0, 0.588]
    // and piece 1 covers [0.412, 1.0]. At global alpha = 0.5:
    //   piece 0 local ≈ 0.5 / 0.588 ≈ 0.85
    //   piece 1 local ≈ (0.5 − 0.412) / 0.588 ≈ 0.15
    match result[0].shape {
        Shape::Line(l) => {
            assert!(approx(l.x2, 0.85, 2e-2), "A.x2 = {} expected ≈0.85", l.x2);
        }
        _ => panic!("expected Line A"),
    }
    match result[1].shape {
        Shape::Line(l) => {
            assert!(approx(l.x2, 0.15, 2e-2), "B.x2 = {} expected ≈0.15", l.x2);
        }
        _ => panic!("expected Line B"),
    }
}

#[test]
fn show_creation_lag_ratio_holds_trailing_pieces_at_low_alpha() {
    let pieces: Vec<SceneShape> = (0..3)
        .map(|i| {
            SceneShape::new(
                (i + 1) as u64,
                Shape::Line(Line {
                    x1: 0.0,
                    y1: 0.0,
                    x2: 1.0,
                    y2: 0.0,
                    thickness: 0.01,
                    color: Color::WHITE,
                }),
            )
        })
        .collect();
    let anim = Animation::ShowCreation { targets: pieces };
    let result = anim.apply(&[], 0.1);
    // First piece has barely started, second/third should be exactly zero.
    let xs: Vec<f32> = result
        .iter()
        .map(|s| match s.shape {
            Shape::Line(l) => l.x2,
            _ => f32::NAN,
        })
        .collect();
    assert!(xs[0] > 0.0, "piece 0 should have started: {xs:?}");
    assert!(xs[1] < 1e-5, "piece 1 should be at zero: {xs:?}");
    assert!(xs[2] < 1e-5, "piece 2 should be at zero: {xs:?}");
}

#[test]
fn show_creation_full_alpha_reveals_all() {
    let shape_a = SceneShape::new(
        1,
        Shape::Line(Line {
            x1: 0.0,
            y1: 0.0,
            x2: 1.0,
            y2: 0.0,
            thickness: 0.01,
            color: Color::WHITE,
        }),
    );
    let anim = Animation::ShowCreation {
        targets: vec![shape_a.clone()],
    };
    let final_shapes = anim.final_state(&[], RateFunc::Linear);
    assert_eq!(final_shapes.len(), 1);
    assert_eq!(final_shapes[0], shape_a);
}

#[test]
fn show_creation_there_and_back_terminal_state_keeps_base_only() {
    let base = SceneShape::new(
        99,
        Shape::Circle(Circle {
            x: -0.5,
            y: 0.0,
            radius: 0.1,
            color: Color::RED,
        }),
    );
    let target = SceneShape::new(
        1,
        Shape::Line(Line {
            x1: 0.0,
            y1: 0.0,
            x2: 1.0,
            y2: 0.0,
            thickness: 0.01,
            color: Color::WHITE,
        }),
    );
    let anim = Animation::ShowCreation {
        targets: vec![target],
    };

    assert_eq!(
        anim.final_state(std::slice::from_ref(&base), RateFunc::ThereAndBack),
        vec![base]
    );
}

#[test]
fn partial_shape_strokedpath_trims_arc_length() {
    let path = VectorPath {
        commands: vec![
            PathCommand::MoveTo([0.0, 0.0]),
            PathCommand::LineTo([4.0, 0.0]),
        ],
        color: Color::WHITE,
    };
    let scene_shape = SceneShape::new(
        7,
        Shape::StrokedPath {
            path,
            thickness: 0.02,
        },
    );
    let half = partial_shape(&scene_shape, 0.5);
    match half.shape {
        Shape::StrokedPath { path, thickness } => {
            assert!(approx(thickness, 0.02, 1e-6));
            let len = path_length(&path);
            assert!(approx(len, 2.0, 1e-3), "len = {len} expected 2.0");
        }
        _ => panic!("expected StrokedPath"),
    }
}

fn path_length(p: &VectorPath) -> f32 {
    let mut total = 0.0;
    let mut cur = (0.0f32, 0.0f32);
    for cmd in &p.commands {
        match cmd {
            PathCommand::MoveTo([x, y]) => cur = (*x, *y),
            PathCommand::LineTo([x, y]) => {
                let dx = x - cur.0;
                let dy = y - cur.1;
                total += (dx * dx + dy * dy).sqrt();
                cur = (*x, *y);
            }
            PathCommand::QuadTo { ctrl, to } => {
                // approximate with sampling
                let samples = 64;
                let start = cur;
                let mut prev = start;
                for k in 1..=samples {
                    let t = k as f32 / samples as f32;
                    let one = 1.0 - t;
                    let x = one * one * start.0 + 2.0 * one * t * ctrl[0] + t * t * to[0];
                    let y = one * one * start.1 + 2.0 * one * t * ctrl[1] + t * t * to[1];
                    let dx = x - prev.0;
                    let dy = y - prev.1;
                    total += (dx * dx + dy * dy).sqrt();
                    prev = (x, y);
                }
                cur = (to[0], to[1]);
            }
            PathCommand::CubicTo { ctrl1, ctrl2, to } => {
                let samples = 64;
                let start = cur;
                let mut prev = start;
                for k in 1..=samples {
                    let t = k as f32 / samples as f32;
                    let one = 1.0 - t;
                    let x = one * one * one * start.0
                        + 3.0 * one * one * t * ctrl1[0]
                        + 3.0 * one * t * t * ctrl2[0]
                        + t * t * t * to[0];
                    let y = one * one * one * start.1
                        + 3.0 * one * one * t * ctrl1[1]
                        + 3.0 * one * t * t * ctrl2[1]
                        + t * t * t * to[1];
                    let dx = x - prev.0;
                    let dy = y - prev.1;
                    total += (dx * dx + dy * dy).sqrt();
                    prev = (x, y);
                }
                cur = (to[0], to[1]);
            }
            PathCommand::Close => {}
        }
    }
    total
}
