/// Test `$function` with `$spec` test suite
///
/// @author Hugo Giraudel
///
/// @param {String} $function - Name of function to test
/// @param {Map} $spec - Test suite to run `$function` against
///
/// @return {Map}
///   * `function`: `$function`
///   * `length`: the length of `$tests`
///   * `pass`: number of passing tests out of `length`
///   * `fail`: number of failing tests out of `length`
///   * `tests`: list of maps containing:
///       * `input`: test input (key from `$tests`)
///       * `expected`: expected result from `input`
///       * `actual`: actual result from `input`
///       * `pass`: whether the test passed or not
///       * `fail`: whether the test failed or not
///
/// @example scss - Testing of a `double` function
///   @function double($value) { @return $value * 2 }
///
///   $test: test('double', (
///     1: 2,
///     2: 4,
///     3: 6,
///     4: 8,
///   ));
///
/// @example scss - Result of `double` tests
///   (
///     'function': 'double',
///     'length': 4,
///     'pass': 4,
///     'fail': 0,
///     'tests': ( ... ),
///   )
///
/// @example scss - `tests` value from result of `double` tests
///   (
///     (
///       'input': 1,
///       'expected': 2,
///       'actual': 2,
///       'pass': true,
///       'fail': false,
///     ),
///     // ...
///   )
@function test($function, $spec) {
  $passing-tests: 0;
  $tests: ();

  @each $arguments, $expected-result in $spec {
    $actual-result: call($function, $arguments...);
    $passed: $actual-result == $expected-result;

    $tests: append($tests, (
      'input': $arguments,
      'expected': $expected-result,
      'actual': $actual-result,
      'pass': $passed,
      'fail': not $passed,
    ), 'comma');

    @if $passed {
      $passing-tests: $passing-tests + 1;
    }
  }

  @return (
    'function': $function,
    'length': length($tests),
    'tests': $tests,
    'pass': $passing-tests,
    'fail': length($tests) - $passing-tests,
  );
}

/// Mixin decorating the result of `test(..)` function to throw it as an error
///
/// @author Hugo Giraudel
///
/// @param {Map} $data - Return of `test(..)` function
///
/// @example scss - Printing the result of `double` function test
///   @include run(test('double', $tests-double));
///
/// @example scss - Result of `double` function test
///   Started tests for function `double`
///   ----------
///   Test 1 out of 4... ✔
///   Test 2 out of 4... ✔
///   Test 3 out of 4... ✔
///   Test 4 out of 4... ✔
///   ----------
///   Finished: 0 test(s) failing out of 4
@mixin run($data) {
  $output: '';
  $length: map-get($data, 'length');
  $tests: map-get($data, 'tests');

  @each $test in $tests {
    $output: $output
      + 'Test #{index($tests, $test)} out of #{$length}... '
      + if(map-get($test, 'pass'), '✔', '✘\a   Expected : `#{map-get($test, "expected")}`\a   Actual   : `#{map-get($test, "actual")}`') + '\a ';
  }

  $message: 'Started tests for function `#{map-get($data, "function")}`\a '
    + '----------\a '
    + $output + '\a '
    + '----------\a '
    + 'Finished: #{map-get($data, "fail")} test(s) failing out of #{$length}';

  @if map-get($data, "fail") > 0 {
    @error $message;
  } @else {
    @warn $message;
  }
}
