// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.

package org.wpilib.units;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.Test;
import org.wpilib.units.measure.Angle;
import org.wpilib.units.measure.AngularMomentum;
import org.wpilib.units.measure.Distance;
import org.wpilib.units.measure.LinearVelocity;
import org.wpilib.units.measure.Per;
import org.wpilib.units.measure.Time;

class MeasureTest {
  @Test
  void testBasics() {
    DistanceUnit unit = Units.Feet;
    double magnitude = 10;
    Distance m = unit.of(magnitude);
    assertEquals(unit, m.unit(), "Wrong units");
    assertEquals(magnitude, m.magnitude(), 0, "Wrong magnitude");
  }

  @Test
  void testMultiply() {
    Distance m = Units.Feet.of(1);
    Distance m2 = m.times(10);
    assertEquals(10, m2.in(Units.Feet), 1e-12);
    assertNotSame(m2, m); // make sure state wasn't changed
  }

  @Test
  void testTimesConversionFactor() {
    Distance m = Units.Feet.of(10);

    Per<AngleUnit, DistanceUnit> conversion = Units.Degrees.of(10).div(Units.Feet.of(1));
    Angle result = m.timesConversionFactor(conversion);
    assertEquals(Units.Degrees.of(100), result);
  }

  @Test
  void testTimesConversionFactorComplex() {
    Distance m = Units.Feet.of(1);

    // Using a complex compound unit here
    // (Per<Mult<Mult<Mass, Per<Distance, Time>>, Distance>, Distance>)
    Per<AngularMomentumUnit, DistanceUnit> conversion =
        Units.KilogramMetersSquaredPerSecond.of(1).div(Units.Foot.one());

    AngularMomentum result = m.timesConversionFactor(conversion);
    assertEquals(Units.KilogramMetersSquaredPerSecond.of(1), result);
  }

  @Test
  void testTimesVelocityConversionFactor() {
    Time m = Units.Seconds.of(10);

    LinearVelocity conversion = Units.MetersPerSecond.of(10);
    Distance result = m.timesConversionFactor(conversion);
    assertEquals(Units.Meters.of(100), result);
  }

  @Test
  void testDivide() {
    Distance m = Units.Meters.of(1);
    Distance m2 = m.div(10);
    assertEquals(0.1, m2.magnitude(), 0);
    assertNotSame(m2, m);
  }

  @Test
  void testAdd() {
    Distance m1 = Units.Feet.of(1);
    Distance m2 = Units.Inches.of(2);
    assertTrue(m1.plus(m2).isEquivalent(Units.Feet.of(1 + 2 / 12d)));
    assertTrue(m2.plus(m1).isEquivalent(Units.Inches.of(14)));
  }

  @Test
  void testSubtract() {
    Distance m1 = Units.Feet.of(1);
    Distance m2 = Units.Inches.of(2);
    assertTrue(m1.minus(m2).isEquivalent(Units.Feet.of(1 - 2 / 12d)));
    assertTrue(m2.minus(m1).isEquivalent(Units.Inches.of(-10)));
  }

  @Test
  void testUnaryMinus() {
    Distance m = Units.Feet.of(123);
    Distance negated = m.unaryMinus();
    assertEquals(-123, negated.in(Units.Feet), 1e-12);
    assertEquals(Units.Feet, negated.unit());
  }

  @Test
  void testEquivalency() {
    Distance inches = Units.Inches.of(12);
    Distance feet = Units.Feet.of(1);
    assertTrue(inches.isEquivalent(feet));
    assertTrue(feet.isEquivalent(inches));
  }

  @Test
  void testAs() {
    Distance m = Units.Inches.of(12);
    assertEquals(1, m.in(Units.Feet), Measure.EQUIVALENCE_THRESHOLD);
  }

  @Test
  void testPerMeasureTime() {
    var measure = Units.Kilograms.of(144);
    var dt = Units.Milliseconds.of(53);

    // 144 Kg / (53 ms) = (1000 / 53) * 144 Kg/s = (144,000 / 53) Kg/s

    var result = measure.div(dt);
    assertEquals(144_000.0 / 53, result.baseUnitMagnitude(), 1e-5);
    assertEquals(Units.Kilograms.per(Units.Milliseconds), result.unit());
  }

  @Test
  void testPerUnitTime() {
    var measure = Units.Kilograms.of(144);
    var result = measure.per(Units.Millisecond);

    assertEquals(VelocityUnit.class, result.unit().getClass());
    assertEquals(144_000.0, result.baseUnitMagnitude(), 1e-5);
    assertEquals(Units.Kilograms.per(Units.Milliseconds), result.unit());
  }

  @Test
  void testDivideMeasure() {
    // Dimensionless divide
    var m1 = Units.Meters.of(6);
    var m2 = Units.Value.of(3);
    var result = m1.div(m2);
    assertEquals(2, m1.div(m2).magnitude());
    assertEquals(Units.Meters, result.unit());
    // Velocity divide
    var m3 = Units.Meters.of(8);
    var m4 = Units.Meters.per(Units.Second).of(4);
    var time = m3.div(m4);
    assertEquals(2, time.magnitude());
    assertEquals(Units.Second, time.unit());
    // PerUnit divide
    var m5 = Units.Volts.of(6);
    var m6 = Units.Volts.per(Units.Meter).of(2);

    // Voltage/(Voltage/Distance) -> Voltage * Distance/Voltage -> Distance
    var dist = m5.div(m6);
    assertEquals(3, dist.magnitude());
    assertEquals(Units.Meter, dist.unit());
  }

  @Test
  void testToShortString() {
    var measure = Units.Volts.of(343);
    assertEquals("3.430e+02 V", measure.toShortString());
  }

  @Test
  void testToLongString() {
    var measure = Units.Volts.of(343);
    assertEquals("343.0 Volt", measure.toLongString());
    assertEquals("343.0001 Volt", Units.Volts.of(343.0001).toLongString());
    assertEquals("1.2345678912345678E8 Volt", Units.Volts.of(123456789.12345678).toLongString());
  }

  @Test
  void testOfBaseUnits() {
    var unit = new ExampleUnit(16);
    var measure = unit.ofBaseUnits(1);
    assertEquals(unit, measure.unit());
    assertEquals(1, measure.baseUnitMagnitude());
    assertEquals(1 / 16.0, measure.magnitude());
  }

  @Test
  @SuppressWarnings("SelfComparison")
  void testCompare() {
    var unit = new ExampleUnit(7);
    var base = unit.of(1);
    var less = unit.of(0.5);
    var more = unit.of(2);

    assertEquals(0, base.compareTo(base));
    assertEquals(-1, base.compareTo(more));
    assertEquals(1, base.compareTo(less));

    // lt, lte, gt, gte helper functions

    assertTrue(base.lt(more));
    assertTrue(base.lte(more));
    assertFalse(base.gt(more));
    assertFalse(base.gte(more));

    assertTrue(base.gt(less));
    assertTrue(base.gte(less));
    assertFalse(base.lt(less));
    assertFalse(base.lte(less));

    assertTrue(base.lte(base));
    assertTrue(base.gte(base));
    assertFalse(base.lt(base));
    assertFalse(base.gt(base));
  }

  @Test
  void testMinNoArgs() {
    var min = Measure.min();
    assertNull(min);
  }

  @Test
  void testMin() {
    var unit = new ExampleUnit(56.1);
    var one = unit.of(1);
    var two = unit.of(2);
    var zero = unit.of(0);
    var veryNegative = unit.of(-12839712);

    var min = Measure.min(one, two, zero, veryNegative);
    assertSame(veryNegative, min);
  }

  @Test
  void testMaxNoArgs() {
    var min = Measure.max();
    assertNull(min);
  }

  @Test
  void testMax() {
    var unit = new ExampleUnit(6.551);
    var one = unit.of(1);
    var two = unit.of(2);
    var zero = unit.of(0);
    var veryLarge = unit.of(8217234);

    var max = Measure.max(one, two, zero, veryLarge);
    assertSame(veryLarge, max);
  }

  @Test
  void testIsNearVarianceThreshold() {
    var unit = new ExampleUnit(92);
    var measureA = unit.of(1.21);
    var measureB = unit.ofBaseUnits(64);
    // A = 1.21 * 92 base units, or 111.32
    // B = 64 base units
    // ratio = 111.32 / 64 = 1.739375 = 173.9375%

    assertTrue(measureA.isNear(measureA, 0));
    assertTrue(measureB.isNear(measureB, 0));

    assertFalse(measureA.isNear(measureB, 0));
    assertFalse(measureA.isNear(measureB, 0.50));
    assertFalse(measureA.isNear(measureB, 0.739370));
    assertTrue(measureA.isNear(measureB, 0.739375));
    assertTrue(measureA.isNear(measureB, 100)); // some stupidly large range +/- 10000%

    var measureC = unit.of(-1.21);
    var measureD = unit.ofBaseUnits(-64);

    assertTrue(measureC.isNear(measureC, 0));
    assertTrue(measureD.isNear(measureD, 0));

    assertFalse(measureC.isNear(measureD, 0));
    assertFalse(measureC.isNear(measureD, 0.50));
    assertFalse(measureC.isNear(measureD, 0.739370));
    assertTrue(measureC.isNear(measureD, 0.739375));
    assertTrue(measureC.isNear(measureD, 100)); // some stupidly large range +/- 10000%

    var measureE = Units.Meters.of(1);
    var measureF = Units.Feet.of(-3.28084);

    assertTrue(measureE.isNear(measureF, 2.01));
    assertFalse(measureE.isNear(measureF, 1.99));

    assertTrue(measureF.isNear(measureE, 2.01));
    assertFalse(measureF.isNear(measureE, 1.99));

    assertTrue(Units.Feet.zero().isNear(Units.Millimeters.zero(), 0.001));
    assertFalse(Units.Feet.of(2).isNear(Units.Millimeters.of(0), 0.001));
  }

  @Test
  void testIsNearMeasureTolerance() {
    var measureCompared = Units.Meters.of(1);
    var measureComparing = Units.Meters.of(1.2);

    // Positive value with positive tolerance
    assertTrue(measureCompared.isNear(measureComparing, Units.Millimeters.of(300)));
    assertFalse(measureCompared.isNear(measureComparing, Units.Centimeters.of(10)));

    measureCompared = measureCompared.unaryMinus();
    measureComparing = measureComparing.unaryMinus();

    // Negative value with positive tolerance
    assertTrue(measureCompared.isNear(measureComparing, Units.Millimeters.of(300)));
    assertFalse(measureCompared.isNear(measureComparing, Units.Centimeters.of(10)));

    measureCompared = measureCompared.unaryMinus();
    measureComparing = measureComparing.unaryMinus();

    // Positive value with negative tolerance
    assertTrue(measureCompared.isNear(measureComparing, Units.Millimeters.of(-300)));
    assertFalse(measureCompared.isNear(measureComparing, Units.Centimeters.of(-10)));

    measureCompared = measureCompared.unaryMinus();
    measureComparing = measureComparing.unaryMinus();

    // Negative value with negative tolerance.
    assertTrue(measureCompared.isNear(measureComparing, Units.Millimeters.of(-300)));
    assertFalse(measureCompared.isNear(measureComparing, Units.Centimeters.of(-10)));

    measureCompared = measureCompared.unaryMinus();
    measureComparing = measureComparing.unaryMinus();

    // Tolerance exact difference between measures.
    assertTrue(measureCompared.isNear(measureComparing, Units.Millimeters.of(200)));
    assertTrue(measureCompared.isNear(measureComparing, Units.Centimeters.of(-20)));
  }
}
