import { feet, kilo } from "../..";
import { MeasureFormatter } from "../genericMeasure";
import { Measure } from "../numberMeasure";

describe("Number measures", () => {
  const meters = Measure.dimension("length", "m");
  const seconds = Measure.dimension("time", "s");
  const kilograms = Measure.dimension("mass", "kg");
  const mps = meters.per(seconds);
  const mps2 = mps.per(seconds);

  describe("dimension", () => {
    it("should create dimensions with value 1", () => {
      expect(Measure.dimension("foo", "f")).toEqual({
        value: 1,
        unit: { foo: ["f", "1"] },
        symbol: "f",
      });
    });
  });

  describe("construction", () => {
    it("should construct from a number of and a unit", () => {
      const measure = Measure.of(10, mps2);
      expect(measure.value).toBe(10);
      expect(measure.unit).toEqual(mps2.unit);
    });

    it("should construct from a number and another measure", () => {
      const kilometer = Measure.of(1000, meters);
      const measure = Measure.of(5.2, kilometer);
      expect(measure.value).toBe(5200);
      expect(measure.unit).toEqual(meters.unit);
    });

    it("should construct dimensionless values", () => {
      const dimensionless = Measure.dimensionless(3);
      expect(dimensionless.value).toBe(3);
      expect(dimensionless.unit).toEqual({});
    });
  });

  describe("prefixes", () => {
    const kilo = Measure.prefix("k", 1000);

    it("should scale the base unit", () => {
      const km = kilo(meters);
      expect(km.unit).toEqual(meters.unit);
      expect(km.value).toBe(1000);
    });

    it("should apply a prefix when a symbol is present on the base unit", () => {
      expect(kilo(meters).symbol).toBe("km");
    });

    it("should not apply a prefix when a symbol is not present on the base unit", () => {
      const blargs = Measure.of(1e-3, meters);
      const kblargs = kilo(blargs);
      expect(kblargs.symbol).toBeUndefined();
      expect(kblargs.value).toBe(1);
    });
  });

  describe("math", () => {
    it("arithmetic", () => {
      expect(Measure.add(Measure.of(5, mps), Measure.of(-5, mps))).toEqual(
        Measure.of(0, mps),
      );
      expect(Measure.subtract(Measure.of(5, mps), Measure.of(-5, mps))).toEqual(
        Measure.of(10, mps),
      );
      expect(
        Measure.multiply(Measure.of(5, mps), Measure.of(10, seconds)),
      ).toEqual(Measure.of(50, meters));
      expect(
        Measure.divide(Measure.of(50, meters), Measure.of(10, seconds)),
      ).toEqual(Measure.of(5, mps));
    });

    it("abs", () => {
      expect(Measure.abs(Measure.of(-10, mps))).toEqual(Measure.of(10, mps));
    });

    it("cbrt", () => {
      expect(Measure.cbrt(Measure.of(64, seconds.cubed()))).toEqual(
        Measure.of(4, seconds),
      );
    });

    it("ceil", () => {
      expect(Measure.ceil(Measure.of(3.4, mps))).toEqual(Measure.of(4, mps));
    });

    it("floor", () => {
      expect(Measure.floor(Measure.of(7.8, mps))).toEqual(Measure.of(7, mps));
    });

    it("hypot", () => {
      expect(
        Measure.hypot(Measure.of(3, meters), Measure.of(4, meters)),
      ).toEqual(Measure.of(5, meters));
    });

    it("max", () => {
      expect(
        Measure.max(
          Measure.of(10, mps),
          Measure.of(5, mps),
          Measure.of(15, mps),
        ),
      ).toEqual(Measure.of(15, mps));
    });

    it("min", () => {
      expect(
        Measure.min(
          Measure.of(10, mps),
          Measure.of(5, mps),
          Measure.of(15, mps),
        ),
      ).toEqual(Measure.of(5, mps));
    });

    it("pow", () => {
      expect(Measure.pow(Measure.of(3, meters), "4")).toEqual(
        Measure.of(81, meters.toThe("4")),
      );
    });

    it("round", () => {
      expect(Measure.round(Measure.of(7.8, mps))).toEqual(Measure.of(8, mps));
    });

    it("sqrt", () => {
      expect(Measure.sqrt(Measure.of(25, meters.squared()))).toEqual(
        Measure.of(5, meters),
      );
    });

    it("sum", () => {
      expect(
        Measure.sum(
          Measure.of(10, mps),
          Measure.of(5, mps),
          Measure.of(15, mps),
        ),
      ).toEqual(Measure.of(30, mps));
    });

    it("trunc", () => {
      expect(Measure.trunc(Measure.of(-7.8, mps))).toEqual(Measure.of(-7, mps));
    });
  });

  describe("arithmetic", () => {
    it("should negate", () => {
      const value = Measure.of(10, mps);
      expect(value.negate()).toEqual(Measure.of(-10, mps));
    });

    it("should add", () => {
      const left = Measure.of(10, mps);
      const right = Measure.of(5, mps);
      expect(left.plus(right)).toEqual(Measure.of(15, mps));
    });

    it("should subtract", () => {
      const left = Measure.of(10, seconds);
      const right = Measure.of(5, seconds);
      expect(left.minus(right)).toEqual(Measure.of(5, seconds));
    });

    it("should multiply", () => {
      const left = Measure.of(10, mps);
      const right = Measure.of(5, seconds);
      expect(left.times(right)).toEqual(Measure.of(50, meters));
    });

    it("should divide", () => {
      const left = Measure.of(10, mps);
      const right = Measure.of(5, seconds);
      expect(left.over(right)).toEqual(Measure.of(2, mps2));
      expect(left.per(right)).toEqual(Measure.of(2, mps2));
      expect(left.div(right)).toEqual(Measure.of(2, mps2));
    });

    it("should scale", () => {
      const value = Measure.of(10, mps);
      expect(value.scale(2)).toEqual(Measure.of(20, mps));
    });

    it("should exponentiate", () => {
      const value = Measure.of(10, meters);

      expect(value.inverse()).toEqual(Measure.of(0.1, meters.inverse()));
      expect(value.reciprocal()).toEqual(Measure.of(0.1, meters.inverse()));
      expect(value.toThe("0")).toEqual(Measure.dimensionless(1));
      expect(value.toThe("1")).toEqual(Measure.of(10, meters));
      expect(value.squared()).toEqual(Measure.of(100, meters.squared()));
      expect(value.cubed()).toEqual(Measure.of(1000, meters.cubed()));
    });
  });

  describe("comparison", () => {
    const zero = Measure.of(0, meters);
    const five = Measure.of(5, meters);
    const ten = Measure.of(10, meters);

    it("should compare less than", () => {
      expect(five.lt(zero)).toBe(false);
      expect(five.lt(five)).toBe(false);
      expect(five.lt(ten)).toBe(true);
    });

    it("should compare less than or equal to", () => {
      expect(five.lte(zero)).toBe(false);
      expect(five.lte(five)).toBe(true);
      expect(five.lte(ten)).toBe(true);
    });

    it("should compare equal to", () => {
      expect(five.eq(zero)).toBe(false);
      expect(five.eq(five)).toBe(true);
      expect(five.eq(ten)).toBe(false);
    });

    it("should compare not equal to", () => {
      expect(five.neq(zero)).toBe(true);
      expect(five.neq(five)).toBe(false);
      expect(five.neq(ten)).toBe(true);
    });

    it("should compare greater than or equal to", () => {
      expect(five.gte(zero)).toBe(true);
      expect(five.gte(five)).toBe(true);
      expect(five.gte(ten)).toBe(false);
    });

    it("should compare greater than", () => {
      expect(five.gt(zero)).toBe(true);
      expect(five.gt(five)).toBe(false);
      expect(five.gt(ten)).toBe(false);
    });
  });

  describe("symbols", () => {
    it("should assign a symbol via .of()", () => {
      expect(Measure.of(1000, meters, "km").symbol).toBe("km");
    });

    it("should copy assign a symbol via .withSymbol()", () => {
      const original = Measure.of(1000, meters);
      const result = original.withSymbol("km");
      expect(result).not.toBe(original);
      expect(original.symbol).toBeUndefined();
      expect(result.symbol).toBe("km");
    });

    it("should not pass along symbols through operations", () => {
      const km = Measure.of(1000, meters.squared()).withSymbol("km2");
      const dm = Measure.of(10, meters.squared()).withSymbol("dm2");
      expect(km.negate().symbol).toBeUndefined();
      expect(km.squared().symbol).toBeUndefined();
      expect(km.inverse().symbol).toBeUndefined();
      expect(km.plus(dm).symbol).toBeUndefined();
      expect(km.minus(dm).symbol).toBeUndefined();
      expect(km.times(dm).symbol).toBeUndefined();
      expect(km.over(dm).symbol).toBeUndefined();
    });
  });

  describe("formatting", () => {
    function expectFormat(
      unit: Measure<any>,
      formatted: string,
      formatter?: MeasureFormatter<number>,
    ): void {
      expect(unit.toString(formatter)).toBe(formatted);
    }

    it("should format dimensionless units", () => {
      expectFormat(Measure.dimensionless(10), "10");
    });

    it("should format base units", () => {
      expectFormat(meters, "1 m");
    });

    it("should format units with only positive exponents", () => {
      expectFormat(meters.squared(), "1 m^2");
      expectFormat(meters.times(seconds), "1 m * s");
      expectFormat(meters.cubed().times(seconds.squared()), "1 m^3 * s^2");
    });

    it("should format units with only negative exponents", () => {
      expectFormat(seconds.inverse(), "1 s^-1");
      expectFormat(seconds.toThe("-2"), "1 s^-2");
      expectFormat(
        seconds.toThe("-2").times(meters.toThe("-3")),
        "1 m^-3 * s^-2",
      );
    });

    it("should format units with positive exponents and one negative exponent", () => {
      expectFormat(meters.per(seconds), "1 m / s");
      expectFormat(meters.squared().per(seconds.squared()), "1 m^2 / s^2");
      expectFormat(
        meters.times(kilograms).per(seconds.squared()),
        "1 kg * m / s^2",
      );
    });

    it("should format units with positive exponents and negative exponents", () => {
      expectFormat(meters.per(seconds.times(kilograms)), "1 m / (kg * s)");
      expectFormat(
        meters.squared().per(seconds.squared().times(kilograms)),
        "1 m^2 / (kg * s^2)",
      );
      expectFormat(
        meters
          .cubed()
          .times(kilograms)
          .per(kilograms.cubed())
          .per(seconds.squared()),
        "1 m^3 / (kg^2 * s^2)",
      );
    });

    it("should have consistent formatting no matter how the unit is constructed", () => {
      const metersTimesSecond = "1 m * s";
      expectFormat(meters.times(seconds), metersTimesSecond);
      expectFormat(seconds.times(meters), metersTimesSecond);

      const metersPerSecond = "1 m / s";
      expectFormat(meters.per(seconds), metersPerSecond);
      expectFormat(seconds.inverse().times(meters), metersPerSecond);
    });

    it("should not format using symbol even if present", () => {
      expect(Measure.of(5, meters.squared()).withSymbol("m2").toString()).toBe(
        "5 m^2",
      );
      expect(Measure.dimensionless(0).withSymbol("rad").toString()).toBe("0");
    });

    it("should format measures as other measures with symbols", () => {
      const kilometers = Measure.of(1000, meters, "km");
      expect(Measure.of(10000, meters).in(kilometers)).toBe("10 km");
    });

    it("should use normal formatting if the other measure has no symbol", () => {
      const kilometers = Measure.of(1000, meters);
      expect(Measure.of(1000, meters).in(kilometers)).toBe("1000 m");
    });

    it("should use base unit symbols to format when available", () => {
      const m = Measure.dimension("test-length", "meter");
      const s = Measure.dimension("test-time", "second");
      expect(m.toString()).toBe("1 meter");
      expect(Measure.of(1, m.per(s)).toString()).toBe("1 meter / second");
      expect(Measure.of(1, m.squared().per(s.squared())).toString()).toBe(
        "1 meter^2 / second^2",
      );
    });

    it("should use a custom formatter for values if provided", () => {
      expectFormat(Measure.of(3.14159, meters), "3.14 m", {
        formatValue: (value) => value.toPrecision(3),
      });
    });

    it("should use a custom formatter for units if provided", () => {
      expectFormat(Measure.of(3.14159, meters), "3.14159 meters", {
        formatUnit: () => "meters",
      });
    });

    it("should use both custom formatters if provided", () => {
      expectFormat(Measure.of(3.14159, meters), "3.142 meters", {
        formatValue: (value) => value.toPrecision(4),
        formatUnit: () => "meters",
      });
    });

    it("should not use a custom formatter for units when expressing in terms of another measure", () => {
      const kilometers = Measure.of(1000, meters, "km");
      expect(
        Measure.of(20, kilometers).in(kilometers, {
          formatValue: (value) => value.toExponential(),
          formatUnit: () => "kilometers",
        }),
      ).toBe("2e+1 km");
    });

    it("should use a custom formatter for units when expressing in terms of another unit with no symbol", () => {
      const kilometers = Measure.of(1000, meters);
      expect(
        Measure.of(20, kilometers).in(kilometers, {
          formatUnit: () => "meters",
        }),
      ).toBe("20000 meters");
    });
  });

  describe("conversion", () => {
    it("should calculate meters to kilometers correctly", () => {
      expect(Measure.of(10000, meters).valueIn(kilo(meters))).toBe(10);
    });

    it("should calculate feet to meters correctly", () => {
      expect(Measure.of(10, feet).valueIn(meters)).toBeCloseTo(3.048);
    });
  });

  describe("utils", () => {
    it("should clone", () => {
      const original = Measure.of(100, meters);
      const copy = original.clone();
      expect(original).not.toBe(copy);
      expect(original).toEqual(copy);
    });
  });
});
