{ lib, pkgs, depot, ... }:

# Run a nix testsuite.
#
# The tests are simple assertions on the nix level,
# and can use derivation outputs if IfD is enabled.
#
# You build a testsuite by bundling assertions into
# “it”s and then bundling the “it”s into a testsuite.
#
# Running the testsuite will abort evaluation if
# any assertion fails.
#
# Example:
#
#   runTestsuite "myFancyTestsuite" [
#     (it "does an assertion" [
#       (assertEq "42 is equal to 42" "42" "42")
#       (assertEq "also 23" 23 23)
#     ])
#     (it "frmbls the brlbr" [
#       (assertEq true false)
#     ])
#   ]
#
# will fail the second it group because true is not false.

let
  inherit (depot.nix.yants) sum struct string any unit defun list;

  # rewrite the builtins.partition result
  # to use `ok` and `err` instead of `right` and `wrong`.
  partitionTests = pred: xs:
    let res = builtins.partition pred xs;
    in {
      ok = res.right;
      err = res.wrong;
    };

  # The result of an assert,
  # either it’s true (yep) or false (nope).
  # If it’s nope, we return the left and right
  # side of the assert, together with the description.
  AssertResult =
    sum "AssertResult" {
      yep = struct "yep" {
        test = string;
      };
      nope = struct "nope" {
        test = string;
        left = any;
        right = any;
      };
    };

  # Result of an it. An it is a bunch of asserts
  # bundled up with a good description of what is tested.
  ItResult =
    struct "ItResult" {
      it-desc = string;
      asserts = list AssertResult;
    };

  # assert that left and right values are equal
  assertEq = defun [ string any any AssertResult ]
    (desc: left: right:
      if left == right
      then { yep = { test = desc; }; }
      else { nope = {
        test = desc;
        inherit left right;
      };
    });

  # Annotate a bunch of asserts with a descriptive name
  it = desc: asserts: {
    it-desc = desc;
    inherit asserts;
  };

  # Run a bunch of its and check whether all asserts are yep.
  # If not, abort evaluation with `throw`
  # and print the result of the test suite.
  #
  # Takes a test suite name as first argument.
  runTestsuite = defun [ string (list ItResult) unit ]
    (name: itResults:
      let
        goodAss = ass: {
          good = AssertResult.match ass {
            yep = _: true;
            nope = _: false;
          };
          x = ass;
        };
        goodIt = it: {
          inherit (it) it-desc;
          asserts = partitionTests (ass:
            AssertResult.match ass {
              yep = _: true;
              nope = _: false;
            }) it.asserts;
        };
        goodIts = partitionTests (it: (goodIt it).asserts.err == []);
        res = goodIts itResults;
      in
        if res.err == []
        then {}
        # TODO(Profpatsch): pretty printing of results
        # and probably also somewhat easier to read output
        else throw
          ( "testsuite ${name} failed!\n"
          + lib.generators.toPretty {} res));

in {
  inherit
    assertEq
    it
    runTestsuite
    ;
}
