use std::borrow::Cow;

use parley::{FontFeature, FontVariation};
use std::cmp::Ordering;
use typed_builder::TypedBuilder;

use serde::Deserialize;

use crate::{
  Viewport,
  style::{
    StyleDeclarationBlock,
    selector::{MediaQueryList, StyleSheet},
    *,
  },
};

#[derive(Debug, Clone, Deserialize, PartialEq, TypedBuilder)]
#[serde(rename_all = "camelCase")]
/// A single structured keyframe rule.
pub struct KeyframeRule {
  /// Keyframe offsets as values between 0.0 and 1.0.
  #[builder(setter(into))]
  pub offsets: Vec<f32>,
  /// Declarations applied at this step.
  pub declarations: StyleDeclarationBlock,
}

#[derive(Debug, Clone, Deserialize, PartialEq, TypedBuilder)]
#[serde(rename_all = "camelCase")]
/// Structured keyframes that can be passed directly in render options.
pub struct KeyframesRule {
  /// Animation name matched by `animation-name`.
  #[builder(setter(into))]
  pub name: String,
  /// Individual keyframe rules for this animation.
  #[builder(setter(into))]
  pub keyframes: Vec<KeyframeRule>,
  #[serde(skip, default)]
  #[builder(default, setter(skip))]
  pub media_queries: Vec<MediaQueryList>,
}

pub fn apply_stylesheet_animations(
  mut base_style: ComputedStyle,
  stylesheet: &StyleSheet,
  time: u64,
  sizing: &SizingContext,
  current_color: Color,
) -> ComputedStyle {
  if base_style.animation_name.is_empty() {
    return base_style;
  }

  let base_snapshot = base_style.clone();

  for (animation_index, animation_name) in base_snapshot.animation_name.iter().enumerate() {
    let Some(animation_name) = animation_name else {
      continue;
    };

    let Some(keyframes) = find_keyframes(stylesheet, animation_name, sizing.viewport) else {
      continue;
    };

    let duration = time_at(
      &base_snapshot.animation_duration,
      animation_index,
      AnimationTime::from_milliseconds(0.0),
    );
    let delay = time_at(
      &base_snapshot.animation_delay,
      animation_index,
      AnimationTime::from_milliseconds(0.0),
    );
    let iteration_count =
      iteration_count_at(&base_snapshot.animation_iteration_count, animation_index);
    let direction = direction_at(&base_snapshot.animation_direction, animation_index);
    let fill_mode = fill_mode_at(&base_snapshot.animation_fill_mode, animation_index);
    let timing_function =
      timing_function_at(&base_snapshot.animation_timing_function, animation_index);

    let Some(progress) = sample_animation_progress(
      time as f32,
      duration.milliseconds,
      delay.milliseconds,
      iteration_count,
      direction,
      fill_mode,
    ) else {
      continue;
    };

    let resolved_frames = resolve_keyframes(&keyframes, &base_snapshot);
    let Some(segment) = sample_keyframe_segment(&resolved_frames, &base_snapshot, progress) else {
      continue;
    };

    let eased_progress = apply_timing_function(&timing_function, segment.progress);
    base_style.apply_interpolated_properties(
      segment.from_style,
      segment.to_style,
      &segment.animated_properties,
      eased_progress,
      sizing,
      current_color,
    );
  }

  base_style
}

fn find_keyframes<'a>(
  stylesheet: &'a StyleSheet,
  name: &str,
  viewport: Viewport,
) -> Option<Cow<'a, KeyframesRule>> {
  stylesheet
    .keyframes
    .iter()
    .rev()
    .find(|rule| {
      rule.name.eq_ignore_ascii_case(name)
        && rule
          .media_queries
          .iter()
          .all(|media_query| media_query.matches(viewport))
    })
    .map(Cow::Borrowed)
    .or_else(|| tailwind_animation_keyframes(name).map(Cow::Owned))
}

fn tailwind_animation_keyframes(name: &str) -> Option<KeyframesRule> {
  match name.to_ascii_lowercase().as_str() {
    "spin" => Some(KeyframesRule {
      name: "spin".to_string(),
      keyframes: vec![
        keyframe(0.25, [StyleDeclaration::rotate(Some(Angle::new(90.0)))]),
        keyframe(0.5, [StyleDeclaration::rotate(Some(Angle::new(180.0)))]),
        keyframe(0.75, [StyleDeclaration::rotate(Some(Angle::new(270.0)))]),
        keyframe(1.0, [StyleDeclaration::rotate(Some(Angle::new(359.999)))]),
      ],
      media_queries: Vec::new(),
    }),
    "ping" => Some(KeyframesRule {
      name: "ping".to_string(),
      keyframes: vec![
        keyframe(
          0.75,
          [
            StyleDeclaration::scale(SpacePair::from_single(PercentageNumber(2.0))),
            StyleDeclaration::opacity(PercentageNumber(0.0)),
          ],
        ),
        keyframe(
          1.0,
          [
            StyleDeclaration::scale(SpacePair::from_single(PercentageNumber(2.0))),
            StyleDeclaration::opacity(PercentageNumber(0.0)),
          ],
        ),
      ],
      media_queries: Vec::new(),
    }),
    "pulse" => Some(KeyframesRule {
      name: "pulse".to_string(),
      keyframes: vec![keyframe(
        0.5,
        [StyleDeclaration::opacity(PercentageNumber(0.5))],
      )],
      media_queries: Vec::new(),
    }),
    "bounce" => Some(KeyframesRule {
      name: "bounce".to_string(),
      keyframes: vec![
        keyframe(
          0.0,
          [StyleDeclaration::translate(SpacePair::from_pair(
            Length::Px(0.0),
            Length::Percentage(-25.0),
          ))],
        ),
        keyframe(
          0.5,
          [StyleDeclaration::translate(SpacePair::from_pair(
            Length::Px(0.0),
            Length::Percentage(0.0),
          ))],
        ),
        keyframe(
          1.0,
          [StyleDeclaration::translate(SpacePair::from_pair(
            Length::Px(0.0),
            Length::Percentage(-25.0),
          ))],
        ),
      ],
      media_queries: Vec::new(),
    }),
    _ => None,
  }
}

fn keyframe<const N: usize>(offset: f32, declarations: [StyleDeclaration; N]) -> KeyframeRule {
  let mut block = StyleDeclarationBlock::default();
  for declaration in declarations {
    block.push(declaration, false);
  }

  KeyframeRule {
    offsets: vec![offset],
    declarations: block,
  }
}

fn sample_animation_progress(
  time_ms: f32,
  duration_ms: f32,
  delay_ms: f32,
  iteration_count: AnimationIterationCount,
  direction: AnimationDirection,
  fill_mode: AnimationFillMode,
) -> Option<f32> {
  let active_time = time_ms - delay_ms;

  if duration_ms <= 0.0 {
    if active_time < 0.0 {
      return match fill_mode {
        AnimationFillMode::Backwards | AnimationFillMode::Both => Some(start_progress(direction)),
        _ => None,
      };
    }

    return Some(end_progress(direction, 0));
  }

  let total_active_duration = match iteration_count {
    AnimationIterationCount::Infinite => f32::INFINITY,
    AnimationIterationCount::Number(count) => duration_ms * count.max(0.0),
  };

  if active_time < 0.0 {
    return match fill_mode {
      AnimationFillMode::Backwards | AnimationFillMode::Both => Some(start_progress(direction)),
      _ => None,
    };
  }

  if active_time >= total_active_duration {
    return match fill_mode {
      AnimationFillMode::Forwards | AnimationFillMode::Both => {
        let end_progress = match iteration_count {
          AnimationIterationCount::Infinite => end_progress(direction, 0),
          AnimationIterationCount::Number(count) => {
            let count = count.max(0.0);
            let completed_iterations = count.floor() as usize;
            let fraction = count.fract();
            if fraction > f32::EPSILON {
              apply_direction(fraction, direction, completed_iterations)
            } else {
              end_progress(direction, count.max(1.0) as usize - 1)
            }
          }
        };
        Some(end_progress)
      }
      _ => None,
    };
  }

  let progress_within_iteration = active_time / duration_ms;
  let mut iteration_index = progress_within_iteration.floor() as usize;
  let mut progress = progress_within_iteration.fract();
  if active_time > 0.0 && progress_within_iteration.fract().abs() <= f32::EPSILON {
    progress = 1.0;
    iteration_index = iteration_index.saturating_sub(1);
  }

  Some(apply_direction(progress, direction, iteration_index))
}

fn start_progress(direction: AnimationDirection) -> f32 {
  apply_direction(0.0, direction, 0)
}

fn end_progress(direction: AnimationDirection, iteration_index: usize) -> f32 {
  apply_direction(1.0, direction, iteration_index)
}

fn apply_direction(progress: f32, direction: AnimationDirection, iteration_index: usize) -> f32 {
  match direction {
    AnimationDirection::Normal => progress,
    AnimationDirection::Reverse => 1.0 - progress,
    AnimationDirection::Alternate => {
      if iteration_index.is_multiple_of(2) {
        progress
      } else {
        1.0 - progress
      }
    }
    AnimationDirection::AlternateReverse => {
      if iteration_index.is_multiple_of(2) {
        1.0 - progress
      } else {
        progress
      }
    }
  }
}

fn sample_keyframe_segment<'a>(
  resolved_frames: &'a ResolvedKeyframes,
  base_style: &'a ComputedStyle,
  progress: f32,
) -> Option<InterpolationSegment<'a>> {
  let first = resolved_frames.points.first()?;

  if progress <= first.offset {
    let segment_progress = if first.offset <= 0.0 {
      1.0
    } else {
      progress / first.offset
    };
    return Some(InterpolationSegment::new(
      base_style,
      None,
      &resolved_frames.style(first.style_index).style,
      Some(&resolved_frames.style(first.style_index).mask),
      segment_progress.clamp(0.0, 1.0),
    ));
  }

  for window in resolved_frames.points.windows(2) {
    let [start_point, end_point] = window else {
      continue;
    };
    if progress <= end_point.offset {
      let width = end_point.offset - start_point.offset;
      let segment_progress = if width <= f32::EPSILON {
        1.0
      } else {
        (progress - start_point.offset) / width
      };
      return Some(InterpolationSegment::new(
        &resolved_frames.style(start_point.style_index).style,
        Some(&resolved_frames.style(start_point.style_index).mask),
        &resolved_frames.style(end_point.style_index).style,
        Some(&resolved_frames.style(end_point.style_index).mask),
        segment_progress.clamp(0.0, 1.0),
      ));
    }
  }

  let last = resolved_frames.points.last()?;
  let segment_progress = if last.offset >= 1.0 {
    1.0
  } else {
    (progress - last.offset) / (1.0 - last.offset)
  };
  Some(InterpolationSegment::new(
    &resolved_frames.style(last.style_index).style,
    Some(&resolved_frames.style(last.style_index).mask),
    base_style,
    None,
    segment_progress.clamp(0.0, 1.0),
  ))
}

fn resolve_keyframes(keyframes: &KeyframesRule, base_style: &ComputedStyle) -> ResolvedKeyframes {
  let mut points = keyframes
    .keyframes
    .iter()
    .enumerate()
    .flat_map(|(style_index, keyframe)| {
      keyframe
        .offsets
        .iter()
        .copied()
        .map(move |offset| ResolvedKeyframePoint {
          offset,
          style_index,
        })
    })
    .collect::<Vec<_>>();

  points.sort_by(|lhs, rhs| {
    lhs
      .offset
      .partial_cmp(&rhs.offset)
      .unwrap_or(Ordering::Equal)
  });

  let mut styles = Vec::with_capacity(points.len());
  let mut merged_points: Vec<ResolvedKeyframePoint> = Vec::with_capacity(points.len());
  for point in points {
    if let Some(last_point) = merged_points.last_mut()
      && (last_point.offset - point.offset).abs() <= f32::EPSILON
    {
      merge_keyframe_style(
        &mut styles[last_point.style_index],
        &keyframes.keyframes[point.style_index],
      );
      continue;
    }

    let style_index = styles.len();
    styles.push(resolve_keyframe_style(
      &keyframes.keyframes[point.style_index],
      base_style,
    ));
    merged_points.push(ResolvedKeyframePoint {
      offset: point.offset,
      style_index,
    });
  }

  ResolvedKeyframes {
    points: merged_points,
    styles,
  }
}

#[derive(Debug)]
struct ResolvedKeyframeStyle {
  style: ComputedStyle,
  mask: PropertyMask,
}

impl ResolvedKeyframeStyle {
  fn new(style: ComputedStyle, mask: PropertyMask) -> Self {
    Self { style, mask }
  }
}

#[derive(Debug)]
struct ResolvedKeyframePoint {
  offset: f32,
  style_index: usize,
}

#[derive(Debug)]
struct ResolvedKeyframes {
  points: Vec<ResolvedKeyframePoint>,
  styles: Vec<ResolvedKeyframeStyle>,
}

impl ResolvedKeyframes {
  fn style(&self, index: usize) -> &ResolvedKeyframeStyle {
    &self.styles[index]
  }
}

#[derive(Debug)]
struct InterpolationSegment<'a> {
  from_style: &'a ComputedStyle,
  to_style: &'a ComputedStyle,
  animated_properties: PropertyMask,
  progress: f32,
}

impl<'a> InterpolationSegment<'a> {
  fn new(
    from_style: &'a ComputedStyle,
    from_mask: Option<&'a PropertyMask>,
    to_style: &'a ComputedStyle,
    to_mask: Option<&'a PropertyMask>,
    progress: f32,
  ) -> Self {
    let mut animated_properties = PropertyMask::new();
    if let Some(mask) = from_mask {
      animated_properties.extend(mask.iter());
    }
    if let Some(mask) = to_mask {
      animated_properties.extend(mask.iter());
    }
    Self {
      from_style,
      to_style,
      animated_properties,
      progress,
    }
  }
}

fn resolve_keyframe_style(
  keyframe: &KeyframeRule,
  base_style: &ComputedStyle,
) -> ResolvedKeyframeStyle {
  let mut style = base_style.clone();
  let mut mask = PropertyMask::new();
  apply_keyframe_declarations(&mut style, &mut mask, keyframe);
  ResolvedKeyframeStyle::new(style, mask)
}

fn merge_keyframe_style(style: &mut ResolvedKeyframeStyle, keyframe: &KeyframeRule) {
  apply_keyframe_declarations(&mut style.style, &mut style.mask, keyframe);
}

fn apply_keyframe_declarations(
  style: &mut ComputedStyle,
  mask: &mut PropertyMask,
  keyframe: &KeyframeRule,
) {
  for declaration in keyframe.declarations.iter() {
    declaration.apply_to_computed(style);
    mask.extend(declaration.affected_longhands().iter());
  }
}

macro_rules! impl_passthrough_animatable {
  ($($ty:ty),* $(,)?) => {
    $(
      impl Animatable for $ty {}
    )*
  };
}

impl_passthrough_animatable!(
  BoxSizing,
  AnimationNames,
  AnimationDurations,
  AnimationTimingFunctions,
  AnimationIterationCounts,
  AnimationDirections,
  AnimationFillModes,
  AnimationPlayStates,
  Clear,
  Display,
  Direction,
  Float,
  FlexDirection,
  AlignItems,
  JustifyContent,
  FlexWrap,
  Position,
  BorderStyle,
  Border,
  ObjectFit,
  Overflow,
  BackgroundClip,
  GridAutoFlow,
  GridLine,
  GridTemplateAreas,
  TextOverflow,
  TextTransform,
  FontStyle,
  FontFamily,
  LineHeight,
  FontSynthesis,
  FontSynthesic,
  LineClamp,
  TextAlign,
  TextStroke,
  LineJoin,
  TextDecoration,
  TextDecorationLines,
  TextDecorationStyle,
  TextDecorationSkipInk,
  ImageScalingAlgorithm,
  OverflowWrap,
  WordBreak,
  BasicShape,
  FillRule,
  WhiteSpace,
  WhiteSpaceCollapse,
  TextWrapMode,
  TextWrapStyle,
  TextWrap,
  Isolation,
  Visibility,
  VerticalAlign,
  Flex,
  Background,
  GridTrackSize,
  GridTemplateComponent,
  FontFeature,
  FontVariation,
);

impl<const DEFAULT_AUTO: bool> Animatable for Length<DEFAULT_AUTO> {
  fn interpolate(
    &mut self,
    from: &Self,
    to: &Self,
    progress: f32,
    sizing: &SizingContext,
    _current_color: Color,
  ) {
    *self = interpolate_length(*from, *to, progress)
      .or_else(|| {
        resolve_length_with_sizing(*from, sizing).and_then(|resolved_from| {
          resolve_length_with_sizing(*to, sizing)
            .map(|resolved_to| Length::Px(lerp(resolved_from, resolved_to, progress)))
        })
      })
      .unwrap_or(if progress >= 0.5 { *to } else { *from });
  }
}

fn interpolate_length<const DEFAULT_AUTO: bool>(
  from: Length<DEFAULT_AUTO>,
  to: Length<DEFAULT_AUTO>,
  progress: f32,
) -> Option<Length<DEFAULT_AUTO>> {
  macro_rules! lerp_variants {
    ($($variant:ident),+ $(,)?) => {
      match (from, to) {
        $(
          (Length::$variant(lhs), Length::$variant(rhs)) => {
            Some(Length::$variant(lerp(lhs, rhs, progress)))
          }
        )+
        (Length::Auto, Length::Auto) => Some(Length::Auto),
        _ => None,
      }
    };
  }
  lerp_variants!(
    Percentage, Rem, Em, Vh, Vw, CqH, CqW, CqMin, CqMax, VMin, VMax, Cm, Mm, In, Q, Pt, Pc, Px,
  )
}

fn resolve_length_with_sizing<const DEFAULT_AUTO: bool>(
  value: Length<DEFAULT_AUTO>,
  sizing: &SizingContext,
) -> Option<f32> {
  if matches!(value, Length::Auto) {
    return None;
  }

  Some(value.to_px(
    sizing,
    sizing.viewport.size.width.unwrap_or_default() as f32,
  ))
}

#[cfg(test)]
mod tests {
  use std::rc::Rc;

  use taffy::Size;

  use crate::{
    Viewport,
    style::{
      SizingContext,
      animation::{sample_animation_progress, tailwind_animation_keyframes},
      *,
    },
  };

  fn sizing() -> SizingContext {
    SizingContext {
      viewport: Viewport::new((200, 100)),
      container_size: Size::NONE,
      font_size: 16.0,
      root_font_size: None,
      line_height: 0.0,
      root_line_height: None,
      calc_arena: Rc::new(CalcArena::default()),
    }
  }

  fn current_color() -> Color {
    Color([10, 20, 30, 255])
  }

  #[derive(Clone, Copy, Debug, PartialEq)]
  struct Dummy(u8);

  impl Animatable for Dummy {}

  #[test]
  fn animatable_default_flips_at_half_progress() {
    let mut target = Dummy(9);
    target.interpolate(&Dummy(3), &Dummy(7), 0.25, &sizing(), current_color());
    assert_eq!(target, Dummy(3));

    target.interpolate(&Dummy(3), &Dummy(7), 0.5, &sizing(), current_color());
    assert_eq!(target, Dummy(7));
  }

  #[test]
  fn length_interpolates_continuously() {
    let mut target: Length = Length::zero();
    target.interpolate(
      &Length::Px(10.0),
      &Length::Px(30.0),
      0.25,
      &sizing(),
      current_color(),
    );
    assert_eq!(target, Length::Px(15.0));
  }

  #[test]
  fn mixed_unit_length_interpolates_via_sizing() {
    let mut target: Length = Length::zero();
    target.interpolate(
      &Length::Px(0.0),
      &Length::Percentage(50.0),
      0.5,
      &sizing(),
      current_color(),
    );

    assert_eq!(target, Length::Px(50.0));
  }

  #[test]
  fn option_length_uses_discrete_fallback() {
    let mut target: Option<Length> = None;
    target.interpolate(
      &Some(Length::Px(10.0)),
      &None,
      0.25,
      &sizing(),
      current_color(),
    );
    assert_eq!(target, Some(Length::Px(10.0)));

    target.interpolate(
      &Some(Length::Px(10.0)),
      &None,
      0.75,
      &sizing(),
      current_color(),
    );
    assert_eq!(target, None);
  }

  #[test]
  fn background_position_interpolates_components() {
    let mut target: BackgroundPosition = BackgroundPosition::default();
    target.interpolate(
      &BackgroundPosition(SpacePair::from_pair(
        PositionComponent::KeywordX(PositionKeywordX::Left),
        PositionComponent::KeywordY(PositionKeywordY::Top),
      )),
      &BackgroundPosition(SpacePair::from_pair(
        PositionComponent::KeywordX(PositionKeywordX::Right),
        PositionComponent::KeywordY(PositionKeywordY::Bottom),
      )),
      0.5,
      &sizing(),
      current_color(),
    );

    assert_eq!(
      target,
      BackgroundPosition(SpacePair::from_pair(
        PositionComponent::Length(Length::Percentage(50.0)),
        PositionComponent::Length(Length::Percentage(50.0)),
      ))
    );
  }

  #[test]
  fn color_input_interpolates_using_current_color() {
    let mut target: ColorInput = ColorInput::CurrentColor;
    target.interpolate(
      &ColorInput::CurrentColor,
      &ColorInput::Value(Color([110, 120, 130, 255])),
      0.5,
      &sizing(),
      current_color(),
    );

    assert_eq!(target, ColorInput::Value(Color([57, 67, 77, 255])));
  }

  #[test]
  fn border_radius_interpolates_via_container_impls() {
    let mut target = BorderRadius::default();
    target.interpolate(
      &BorderRadius::from(4.0),
      &BorderRadius::from(12.0),
      0.5,
      &sizing(),
      current_color(),
    );

    assert_eq!(target, BorderRadius::from(8.0));
  }

  #[test]
  fn percentage_number_interpolates() {
    let mut target = PercentageNumber::default();
    target.interpolate(
      &PercentageNumber(0.2),
      &PercentageNumber(0.6),
      0.5,
      &sizing(),
      current_color(),
    );

    assert!((target.0 - 0.4).abs() < f32::EPSILON);
  }

  #[test]
  fn option_angle_interpolates_inner_angle() {
    let mut target: Option<Angle> = None;
    target.interpolate(
      &Some(Angle::new(0.0)),
      &Some(Angle::new(90.0)),
      0.5,
      &sizing(),
      current_color(),
    );

    assert_eq!(target, Some(Angle::new(45.0)));
  }

  #[test]
  fn option_angle_interpolates_from_missing_zero_angle() {
    let mut target: Option<Angle> = None;
    target.interpolate(
      &None,
      &Some(Angle::new(45.0)),
      0.5,
      &sizing(),
      current_color(),
    );

    assert_eq!(target, Some(Angle::new(22.5)));
  }

  #[test]
  fn aspect_ratio_interpolates_ratio_values() {
    let mut target = AspectRatio::Auto;
    target.interpolate(
      &AspectRatio::Ratio(1.0),
      &AspectRatio::Ratio(2.0),
      0.25,
      &sizing(),
      current_color(),
    );

    assert_eq!(target, AspectRatio::Ratio(1.25));
  }

  #[test]
  fn font_stretch_interpolates_percentages() {
    let mut target = FontStretch::from_percentage(0.0);
    target.interpolate(
      &FontStretch::from_percentage(0.75),
      &FontStretch::from_percentage(1.25),
      0.5,
      &sizing(),
      current_color(),
    );

    assert!((target.percentage() - 1.0).abs() < f32::EPSILON);
  }

  #[test]
  fn font_weight_interpolates_numeric_values() {
    let mut target = FontWeight::default();
    target.interpolate(
      &FontWeight::from(400.0),
      &FontWeight::from(700.0),
      0.5,
      &sizing(),
      current_color(),
    );

    assert!((target.value() - 550.0).abs() < f32::EPSILON);
  }

  #[test]
  fn text_decoration_thickness_interpolates_lengths() {
    let mut target = TextDecorationThickness::default();
    target.interpolate(
      &TextDecorationThickness::Length(Length::Px(2.0)),
      &TextDecorationThickness::Length(Length::Px(10.0)),
      0.25,
      &sizing(),
      current_color(),
    );

    assert_eq!(target, TextDecorationThickness::Length(Length::Px(4.0)));
  }

  #[test]
  fn flex_grow_interpolates_numeric_values() {
    let mut target = FlexGrow(0.0);
    target.interpolate(
      &FlexGrow(1.0),
      &FlexGrow(3.0),
      0.5,
      &sizing(),
      current_color(),
    );

    assert!((target.0 - 2.0).abs() < f32::EPSILON);
  }

  #[test]
  fn transform_translate_interpolates_lengths() {
    let mut target = Transform::Translate(Length::zero(), Length::zero());
    target.interpolate(
      &Transform::Translate(Length::Px(0.0), Length::Px(10.0)),
      &Transform::Translate(Length::Px(20.0), Length::Px(30.0)),
      0.5,
      &sizing(),
      current_color(),
    );

    assert_eq!(
      target,
      Transform::Translate(Length::Px(10.0), Length::Px(20.0))
    );
  }

  #[test]
  fn background_size_interpolates_explicit_lengths() {
    let mut target = BackgroundSize::default();
    target.interpolate(
      &BackgroundSize::Explicit {
        width: Length::Px(10.0),
        height: Length::Px(20.0),
      },
      &BackgroundSize::Explicit {
        width: Length::Px(30.0),
        height: Length::Px(60.0),
      },
      0.5,
      &sizing(),
      current_color(),
    );

    assert_eq!(
      target,
      BackgroundSize::Explicit {
        width: Length::Px(20.0),
        height: Length::Px(40.0),
      }
    );
  }

  #[test]
  fn box_shadow_interpolates_lengths_and_color() {
    let mut target = BoxShadow {
      inset: false,
      offset_x: Length::zero(),
      offset_y: Length::zero(),
      blur_radius: Length::zero(),
      spread_radius: Length::zero(),
      color: ColorInput::CurrentColor,
    };
    target.interpolate(
      &BoxShadow {
        inset: false,
        offset_x: Length::Px(0.0),
        offset_y: Length::Px(10.0),
        blur_radius: Length::Px(20.0),
        spread_radius: Length::Px(30.0),
        color: ColorInput::Value(Color([0, 0, 0, 255])),
      },
      &BoxShadow {
        inset: false,
        offset_x: Length::Px(20.0),
        offset_y: Length::Px(30.0),
        blur_radius: Length::Px(40.0),
        spread_radius: Length::Px(50.0),
        color: ColorInput::Value(Color([200, 100, 50, 255])),
      },
      0.5,
      &sizing(),
      current_color(),
    );

    assert_eq!(
      target,
      BoxShadow {
        inset: false,
        offset_x: Length::Px(10.0),
        offset_y: Length::Px(20.0),
        blur_radius: Length::Px(30.0),
        spread_radius: Length::Px(40.0),
        color: ColorInput::Value(Color([76, 34, 13, 255])),
      }
    );
  }

  #[test]
  fn text_shadow_interpolates_lengths_and_color() {
    let mut target = TextShadow {
      offset_x: Length::zero(),
      offset_y: Length::zero(),
      blur_radius: Length::zero(),
      color: ColorInput::CurrentColor,
    };
    target.interpolate(
      &TextShadow {
        offset_x: Length::Px(0.0),
        offset_y: Length::Px(10.0),
        blur_radius: Length::Px(20.0),
        color: ColorInput::Value(Color([0, 0, 0, 255])),
      },
      &TextShadow {
        offset_x: Length::Px(20.0),
        offset_y: Length::Px(30.0),
        blur_radius: Length::Px(40.0),
        color: ColorInput::Value(Color([200, 100, 50, 255])),
      },
      0.5,
      &sizing(),
      current_color(),
    );

    assert_eq!(
      target,
      TextShadow {
        offset_x: Length::Px(10.0),
        offset_y: Length::Px(20.0),
        blur_radius: Length::Px(30.0),
        color: ColorInput::Value(Color([76, 34, 13, 255])),
      }
    );
  }

  #[test]
  fn filter_blur_interpolates_lengths() {
    let mut target = Filter::Blur(Length::zero());
    target.interpolate(
      &Filter::Blur(Length::Px(4.0)),
      &Filter::Blur(Length::Px(12.0)),
      0.5,
      &sizing(),
      current_color(),
    );

    assert_eq!(target, Filter::Blur(Length::Px(8.0)));
  }

  #[test]
  fn animation_progress_uses_next_iteration_start_at_boundaries() {
    let progress = sample_animation_progress(
      1000.0,
      1000.0,
      0.0,
      AnimationIterationCount::Infinite,
      AnimationDirection::Alternate,
      AnimationFillMode::Both,
    );

    assert_eq!(progress, Some(1.0));
  }

  #[test]
  fn animation_progress_keeps_final_state_after_finite_completion() {
    let progress = sample_animation_progress(
      2000.0,
      1000.0,
      0.0,
      AnimationIterationCount::Number(2.0),
      AnimationDirection::Alternate,
      AnimationFillMode::Forwards,
    );

    assert_eq!(progress, Some(0.0));
  }

  #[test]
  fn animation_progress_keeps_fractional_final_iteration_state() {
    let progress = sample_animation_progress(
      1500.0,
      1000.0,
      0.0,
      AnimationIterationCount::Number(1.5),
      AnimationDirection::Normal,
      AnimationFillMode::Forwards,
    );

    assert_eq!(progress, Some(0.5));
  }

  #[test]
  fn animation_progress_uses_end_of_iteration_for_exact_normal_boundaries() {
    let progress = sample_animation_progress(
      1000.0,
      1000.0,
      0.0,
      AnimationIterationCount::Infinite,
      AnimationDirection::Normal,
      AnimationFillMode::Both,
    );

    assert_eq!(progress, Some(1.0));
  }

  #[test]
  fn tailwind_animation_presets_include_built_in_keyframes() {
    assert!(tailwind_animation_keyframes("spin").is_some());
    assert!(tailwind_animation_keyframes("ping").is_some());
    assert!(tailwind_animation_keyframes("pulse").is_some());
    assert!(tailwind_animation_keyframes("bounce").is_some());
  }

  #[test]
  fn vec_animates_pairwise() {
    let mut values: Vec<Length> = vec![Length::Px(0.0), Length::Px(10.0)];
    values.interpolate(
      &vec![Length::Px(0.0), Length::Px(10.0)],
      &vec![Length::Px(20.0), Length::Px(30.0)],
      0.5,
      &sizing(),
      current_color(),
    );

    assert_eq!(values, vec![Length::Px(10.0), Length::Px(20.0)]);
  }

  #[test]
  fn vec_animates_repeatable_lists_to_lcm_length() {
    let mut values: Vec<BackgroundSize> = Vec::new();
    values.interpolate(
      &vec![BackgroundSize::Explicit {
        width: Length::Px(10.0),
        height: Length::Px(20.0),
      }],
      &vec![
        BackgroundSize::Explicit {
          width: Length::Px(30.0),
          height: Length::Px(40.0),
        },
        BackgroundSize::Explicit {
          width: Length::Px(50.0),
          height: Length::Px(60.0),
        },
      ],
      0.5,
      &sizing(),
      current_color(),
    );

    assert_eq!(
      values,
      vec![
        BackgroundSize::Explicit {
          width: Length::Px(20.0),
          height: Length::Px(30.0),
        },
        BackgroundSize::Explicit {
          width: Length::Px(30.0),
          height: Length::Px(40.0),
        },
      ]
    );
  }

  #[test]
  fn boxed_background_lists_animate_repeatable_lists_to_lcm_length() {
    let mut values: Box<[BackgroundSize]> = Box::default();
    values.interpolate(
      &[BackgroundSize::Explicit {
        width: Length::Px(10.0),
        height: Length::Px(20.0),
      }]
      .into(),
      &[
        BackgroundSize::Explicit {
          width: Length::Px(30.0),
          height: Length::Px(40.0),
        },
        BackgroundSize::Explicit {
          width: Length::Px(50.0),
          height: Length::Px(60.0),
        },
      ]
      .into(),
      0.5,
      &sizing(),
      current_color(),
    );

    assert_eq!(
      values,
      [
        BackgroundSize::Explicit {
          width: Length::Px(20.0),
          height: Length::Px(30.0),
        },
        BackgroundSize::Explicit {
          width: Length::Px(30.0),
          height: Length::Px(40.0),
        },
      ]
      .into()
    );
  }

  #[test]
  fn boxed_transform_lists_pad_to_longest_with_neutral_values() {
    let mut values: Box<[Transform]> = Box::default();
    values.interpolate(
      &[Transform::Scale(1.0, 1.0)].into(),
      &[Transform::Scale(2.0, 2.0), Transform::Scale(4.0, 4.0)].into(),
      0.5,
      &sizing(),
      current_color(),
    );

    assert_eq!(
      values,
      [Transform::Scale(1.5, 1.5), Transform::Scale(2.5, 2.5)].into()
    );
  }

  #[test]
  fn apply_interpolated_properties_only_updates_masked_fields() {
    let mut base_style = ComputedStyle {
      width: Length::Px(10.0),
      height: Length::Px(20.0),
      ..ComputedStyle::default()
    };
    let from = ComputedStyle {
      width: Length::Px(10.0),
      height: Length::Px(100.0),
      ..ComputedStyle::default()
    };
    let to = ComputedStyle {
      width: Length::Px(30.0),
      height: Length::Px(200.0),
      ..ComputedStyle::default()
    };
    let animated_properties: PropertyMask = [LonghandId::Width].into_iter().collect();

    base_style.apply_interpolated_properties(
      &from,
      &to,
      &animated_properties,
      0.5,
      &sizing(),
      current_color(),
    );

    assert_eq!(base_style.width, Length::Px(20.0));
    assert_eq!(base_style.height, Length::Px(20.0));
  }

  #[test]
  fn apply_interpolated_properties_interpolates_rotate_from_implicit_none() {
    let mut base_style = ComputedStyle::default();
    let from = ComputedStyle::default();
    let to = ComputedStyle {
      rotate: Some(Angle::new(45.0)),
      ..ComputedStyle::default()
    };
    let animated_properties: PropertyMask = [LonghandId::Rotate].into_iter().collect();

    base_style.apply_interpolated_properties(
      &from,
      &to,
      &animated_properties,
      0.5,
      &sizing(),
      current_color(),
    );

    assert_eq!(base_style.rotate, Some(Angle::new(22.5)));
  }

  #[test]
  fn apply_interpolated_properties_interpolates_flex_grow_from_implicit_zero() {
    let mut base_style = ComputedStyle::default();
    let from = ComputedStyle::default();
    let to = ComputedStyle {
      flex_grow: Some(FlexGrow(4.0)),
      ..ComputedStyle::default()
    };
    let animated_properties: PropertyMask = [LonghandId::FlexGrow].into_iter().collect();

    base_style.apply_interpolated_properties(
      &from,
      &to,
      &animated_properties,
      0.5,
      &sizing(),
      current_color(),
    );

    assert_eq!(base_style.flex_grow, Some(FlexGrow(2.0)));
  }

  #[test]
  fn apply_interpolated_properties_interpolates_flex_shrink_from_implicit_one() {
    let mut base_style = ComputedStyle::default();
    let from = ComputedStyle::default();
    let to = ComputedStyle {
      flex_shrink: Some(FlexGrow(3.0)),
      ..ComputedStyle::default()
    };
    let animated_properties: PropertyMask = [LonghandId::FlexShrink].into_iter().collect();

    base_style.apply_interpolated_properties(
      &from,
      &to,
      &animated_properties,
      0.5,
      &sizing(),
      current_color(),
    );

    assert_eq!(base_style.flex_shrink, Some(FlexGrow(2.0)));
  }

  #[test]
  fn apply_interpolated_properties_interpolates_text_stroke_width_from_implicit_zero() {
    let mut base_style = ComputedStyle::default();
    let from = ComputedStyle::default();
    let to = ComputedStyle {
      webkit_text_stroke_width: Some(Length::Px(6.0)),
      ..ComputedStyle::default()
    };
    let animated_properties: PropertyMask =
      [LonghandId::WebkitTextStrokeWidth].into_iter().collect();

    base_style.apply_interpolated_properties(
      &from,
      &to,
      &animated_properties,
      0.5,
      &sizing(),
      current_color(),
    );

    assert_eq!(base_style.webkit_text_stroke_width, Some(Length::Px(3.0)));
  }

  #[test]
  fn apply_interpolated_properties_interpolates_text_stroke_color_from_current_color() {
    let mut base_style = ComputedStyle::default();
    let from = ComputedStyle::default();
    let to = ComputedStyle {
      webkit_text_stroke_color: Some(ColorInput::Value(Color([110, 120, 130, 255]))),
      ..ComputedStyle::default()
    };
    let animated_properties: PropertyMask =
      [LonghandId::WebkitTextStrokeColor].into_iter().collect();

    base_style.apply_interpolated_properties(
      &from,
      &to,
      &animated_properties,
      0.5,
      &sizing(),
      current_color(),
    );

    assert_eq!(
      base_style.webkit_text_stroke_color,
      Some(ColorInput::Value(Color([57, 67, 77, 255])))
    );
  }

  #[test]
  fn apply_interpolated_properties_interpolates_text_fill_color_from_style_color() {
    let mut base_style = ComputedStyle::default();
    let from = ComputedStyle {
      color: ColorInput::Value(Color([20, 40, 60, 255])),
      ..ComputedStyle::default()
    };
    let to = ComputedStyle {
      color: ColorInput::Value(Color([20, 40, 60, 255])),
      webkit_text_fill_color: Some(ColorInput::Value(Color([120, 140, 160, 255]))),
      ..ComputedStyle::default()
    };
    let animated_properties: PropertyMask = [LonghandId::WebkitTextFillColor].into_iter().collect();

    base_style.apply_interpolated_properties(
      &from,
      &to,
      &animated_properties,
      0.5,
      &sizing(),
      current_color(),
    );

    assert_eq!(
      base_style.webkit_text_fill_color,
      Some(ColorInput::Value(Color([68, 88, 108, 255])))
    );
  }

  #[test]
  fn text_indent_interpolates_amount_continuously() {
    let mut target = TextIndent::default();
    let from = TextIndent {
      amount: LengthDefaultsToZero::Px(10.0),
      each_line: false,
      hanging: false,
    };
    let to = TextIndent {
      amount: LengthDefaultsToZero::Px(30.0),
      each_line: true,
      hanging: true,
    };

    target.interpolate(&from, &to, 0.25, &sizing(), current_color());
    assert_eq!(target.amount, LengthDefaultsToZero::Px(15.0));
    assert!(!target.each_line);
    assert!(!target.hanging);

    target.interpolate(&from, &to, 0.75, &sizing(), current_color());
    assert_eq!(target.amount, LengthDefaultsToZero::Px(25.0));
    assert!(target.each_line);
    assert!(target.hanging);
  }
}
