<?php

/**
 * Tests the MathExpression library of ctools.
 */
class CtoolsMathExpressionTestCase extends DrupalWebTestCase {

  /**
   * {@inheritdoc}
   */
  public static function getInfo() {
    return array(
      'name' => 'Math expressions',
      'description' => 'Test the math expression library of ctools.',
      'group' => 'ctools',
      'dependencies' => array('ctools'),
    );
  }

  /**
   * {@inheritdoc}
   */
  public function setUp(array $modules = array()) {
    $modules[] = 'ctools';
    $modules[] = 'ctools_plugin_test';
    parent::setUp($modules);
  }

  /**
   * Return the sign of the numeric arg $n as an integer -1, 0, 1.
   *
   * Note: Not defined when $n is Infinity or NaN (or NULL or ...)!
   *
   * @param int|float $n
   *   The number to test.
   *
   * @return int
   *   -1 if the $n is negative, 0 if $n is zero or 1 if $n is positive.
   *
   * @see gmp_sign()
   */
  protected static function sign($n) {
    return ($n > 0) - ($n < 0);
  }

  /**
   * Returns a random number between 0 and 1.
   *
   * @return float
   *   A random number between 0 and 1 inclusive.
   */
  protected function rand01() {
    return mt_rand(0, PHP_INT_MAX) / PHP_INT_MAX;
  }

  /**
   * A custom assertion with checks the values in a certain range.
   *
   * @param float $first
   *   A value to check for equality.
   * @param float $second
   *   A value to check for equality.
   * @param string $message
   *   The message describing the correct behaviour, eg. "2/4 equals 1/2". The
   *   default message is used if this value is empty.
   * @param float $delta
   *   The precision with which values must match. This accounts for rounding
   *   errors and imprecise representation errors in the floating point format.
   *   The value passed in should ideally be proportional to the values being
   *   compared.
   * @param string $group
   *   Which group this assert belongs to.
   *
   * @return bool
   *   TRUE if the assertion was correct (that is, $first == $second within the
   *   given limits), FALSE otherwise.
   */
  protected function assertFloat($first, $second, $message = '', $delta = 0.00000001, $group = 'Other') {
    // Check for NaN and Inf because the abs() and sign() code won't like those.
    $equal = FALSE
      // Equal if both an infinity.
      || (is_infinite($first) && is_infinite($second))

      // Equal if both NaN.
      || (is_nan($first) && is_nan($second))

      // Equal if same absolute value (within limits) and same sign.
      || ((abs($first - $second) <= $delta) && (self::sign($first) === self::sign($second)));

    if (empty($message)) {
      $default = t('Value !first is equal to value !second.',
        array(
          '!first' => var_export($first, TRUE),
          '!second' => var_export($second, TRUE),
        ));
      $message = $default;
    }

    return $this->assert($equal, $message, $group);
  }

  /**
   * Test some arithmetic handling.
   */
  public function testArithmetic() {
    $math_expr = new ctools_math_expr();

    $this->assertEqual($math_expr->evaluate('2'), 2, 'Check Literal 2');

    $this->assertEqual($math_expr->e('2+1'), $math_expr->evaluate('2+1'), 'Check that e() and evaluate() are equivalent.');

    foreach (range(1, 4) as $n) {
      // Test constant expressions.
      $random_number = mt_rand(0, 20);
      $this->assertEqual($random_number, $math_expr->evaluate((string) $random_number), "Literal $random_number");

      // Test simple arithmetic.
      $number_a = mt_rand(-55, 777);
      $number_b = mt_rand(-555, 77);
      $this->assertEqual(
        $number_a + $number_b,
        $math_expr->evaluate("$number_a + $number_b"),
        "Addition: $number_a + $number_b");
      $this->assertEqual(
        $number_a - $number_b,
        $math_expr->evaluate("$number_a - $number_b"),
        "Subtraction: $number_a + $number_b");
      $this->assertFloat(
        ($number_a * $number_b),
        $math_expr->evaluate("$number_a * $number_b"),
        "Multiplication: $number_a * $number_b = " . ($number_a * $number_b));
      $this->assertFloat(
        ($number_a / $number_b),
        $math_expr->evaluate("$number_a / $number_b"),
        "Division: $number_a / $number_b = " . ($number_a / $number_b));

      // Test Associative property.
      $number_c = mt_rand(-99, 77);
      $this->assertEqual(
        $math_expr->evaluate("$number_a + ($number_b + $number_c)"),
        $math_expr->evaluate("($number_a + $number_b) + $number_c"),
        "Associative: $number_a + ($number_b + $number_c)");
      $this->assertEqual(
        $math_expr->evaluate("$number_a * ($number_b * $number_c)"),
        $math_expr->evaluate("($number_a * $number_b) * $number_c"),
        "Associative: $number_a * ($number_b * $number_c)");

      // Test Commutative property.
      $this->assertEqual(
        $math_expr->evaluate("$number_a + $number_b"),
        $math_expr->evaluate("$number_b + $number_a"),
        "Commutative: $number_a + $number_b");
      $this->assertEqual(
        $math_expr->evaluate("$number_a * $number_b"),
        $math_expr->evaluate("$number_b * $number_a"),
        "Commutative: $number_a * $number_b");

      // Test Distributive property.
      $this->assertEqual(
        $math_expr->evaluate("($number_a + $number_b) * $number_c"),
        $math_expr->evaluate("($number_a * $number_c + $number_b * $number_c)"),
        "Distributive: ($number_a + $number_b) * $number_c");

      // @todo: Doesn't work with zero or negative powers when number is zero or negative, e.g. 0^0, 0^-2, -2^0, -2^-2.
      $random_number = mt_rand(1, 15);
      $random_power = mt_rand(-15, 15);

      $this->assertFloat(
        pow($random_number, $random_power),
        $math_expr->evaluate("$random_number ^ $random_power"),
        "$random_number ^ $random_power");

      $this->assertFloat(
        pow($random_number, $random_power),
        $math_expr->evaluate("pow($random_number, $random_power)"),
        "pow($random_number, $random_power)");
    }
  }

  /**
   * Test various built-in transcendental and extended functions.
   */
  public function testBuildInFunctions() {
    $math_expr = new ctools_math_expr();

    foreach (range(1, 4) as $n) {
      $random_double = $this->rand01();
      $random_int = mt_rand(-65535, 65535);
      $this->assertFloat(sin($random_double), $math_expr->evaluate("sin($random_double)"), "sin($random_double)");
      $this->assertFloat(cos($random_double), $math_expr->evaluate("cos($random_double)"), "cos($random_double)");
      $this->assertFloat(tan($random_double), $math_expr->evaluate("tan($random_double)"), "tan($random_double)");
      $this->assertFloat(exp($random_double), $math_expr->evaluate("exp($random_double)"), "exp($random_double)");
      $this->assertFloat(sqrt($random_double), $math_expr->evaluate("sqrt($random_double)"), "sqrt($random_double)");
      $this->assertFloat(log($random_double), $math_expr->evaluate("ln($random_double)"), "ln($random_double)");
      $this->assertFloat(round($random_double), $math_expr->evaluate("round($random_double)"), "round($random_double)");

      $random_real = $random_double + $random_int;
      $this->assertFloat(abs($random_real), $math_expr->evaluate('abs(' . $random_real . ')'), "abs($random_real)");
      $this->assertEqual(round($random_real), $math_expr->evaluate('round(' . $random_real . ')'), "round($random_real)");
      $this->assertEqual(ceil($random_real), $math_expr->evaluate('ceil(' . $random_real . ')'), "ceil($random_real)");
      $this->assertEqual(floor($random_real), $math_expr->evaluate('floor(' . $random_real . ')'), "floor($random_real)");
    }

    $this->assertFloat(time(), $math_expr->evaluate('time()'), "time()");

    $random_double_a = $this->rand01();
    $random_double_b = $this->rand01();
    $this->assertFloat(
      max($random_double_a, $random_double_b),
      $math_expr->evaluate("max($random_double_a, $random_double_b)"),
      "max($random_double_a, $random_double_b)");
    $this->assertFloat(
      min($random_double_a, $random_double_b),
      $math_expr->evaluate("min($random_double_a, $random_double_b)"),
      "min($random_double_a, $random_double_b)");
  }

  /**
   * Test variable handling.
   */
  public function testVariables() {
    $math_expr = new ctools_math_expr();

    // We should have a definition of pi:
    $this->assertFloat(pi(), $math_expr->evaluate('pi'));

    // And a definition of e:
    $this->assertFloat(exp(1), $math_expr->evaluate('e'));

    $number_a = 5;
    $number_b = 10;

    // Store the first number and use it on a calculation.
    $math_expr->evaluate("var = $number_a");
    $this->assertEqual($number_a + $number_b, $math_expr->evaluate("var + $number_b"));

    // Change the value and check the new value is used.
    $math_expr->evaluate("var = $number_b");
    $this->assertEqual(
      $number_b + $number_b,
      $math_expr->evaluate("var + $number_b"),
      "var + $number_b");

    // Store another number and use it on a calculation.
    $math_expr->evaluate("var = $number_a");
    $math_expr->evaluate("newvar = $number_a");

    $this->assertEqual(
      $number_a + $number_a,
      $math_expr->evaluate('var + newvar'),
      'var + newvar');

    $this->assertFloat(
      $number_a / $number_b,
      $math_expr->evaluate("var / $number_b"),
      "var / $number_b");
  }

  /**
   * Test custom function handling.
   */
  public function testCustomFunctions() {
    $math_expr = new ctools_math_expr();

    $number_a = mt_rand(5, 10);
    $number_b = mt_rand(5, 10);

    // Create a one-argument function.
    $math_expr->evaluate("f(x) = 2 * x");
    $this->assertEqual($number_a * 2, $math_expr->evaluate("f($number_a)"));
    $this->assertEqual($number_b * 2, $math_expr->evaluate("f($number_b)"));

    // Create a two-argument function.
    $math_expr->evaluate("g(x, y) = 2 * x + y");
    $this->assertEqual(
      $number_a * 2 + $number_b,
      $math_expr->evaluate("g($number_a, $number_b)"),
      "g($number_a, $number_b)");

    // Use a custom function in another function.
    $this->assertEqual(
      ($number_a * 2 + $number_b) * 2,
      $math_expr->evaluate("f(g($number_a, $number_b))"),
      "f(g($number_a, $number_b))");
  }

  /**
   * Test conditional handling.
   */
  public function testIf() {
    $math_expr = new ctools_math_expr();

    $number_a = mt_rand(1, 10);
    $number_b = mt_rand(11, 20);

    foreach (range(1, 4) as $n) {
      // @todo: Doesn't work with negative numbers.
      if ($n == 2 || $n == 4) {
        //$number_a = -$number_a;
      }

      if ($n == 3 || $n == 4) {
        //$number_b = -$number_b;
      }

      $this->assertEqual(
        $number_a,
        $math_expr->evaluate("if(1, $number_a, $number_b)"),
        "if(1, $number_a, $number_b)");

      $this->assertEqual(
        $number_a,
        $math_expr->evaluate("if(1, $number_a)",
          "if(1, $number_a)"));

      $this->assertEqual(
        $number_b,
        $math_expr->evaluate("if(0, $number_a, $number_b)"),
        "if(0, $number_a, $number_b)");

      // Also add an expression so ensure it's evaluated.
      $this->assertEqual(
        $number_b,
        $math_expr->evaluate("if($number_a > $number_b, $number_a, $number_b)"),
        "if($number_a > $number_b, $number_a, $number_b)");

      $this->assertEqual(
        $number_b,
        $math_expr->evaluate("if($number_a < $number_b, $number_b, $number_a)"),
        "if($number_a < $number_b, $number_b, $number_a)");
    }
  }

}
