assert
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © Geoff Hammond

//@version=5

// @description Production ready assertions and auto-reporting for unit testing pine scripts
library("assert")


// ▄▀█ █▀ █▀ █▀▀ █▀█ ▀█▀ █ █▀█ █▄ █   █   █ █▄▄ █▀█ ▄▀█ █▀█ █▄█
// █▀█ ▄█ ▄█ ██▄ █▀▄  █  █ █▄█ █ ▀█   █▄▄ █ █▄█ █▀▄ █▀█ █▀▄  █

// @TODO: compact report.


// ██╗  ██╗███████╗██╗     ██████╗ ███████╗██████╗  ██████╗
// ██║  ██║██╔════╝██║     ██╔══██╗██╔════╝██╔══██╗██╔════╝
// ███████║█████╗  ██║     ██████╔╝█████╗  ██████╔╝╚█████╗
// ██╔══██║██╔══╝  ██║     ██╔═══╝ ██╔══╝  ██╔══██╗ ╚═══██╗
// ██║  ██║███████╗███████╗██║     ███████╗██║  ██║██████╔╝
// ╚═╝  ╚═╝╚══════╝╚══════╝╚═╝     ╚══════╝╚═╝  ╚═╝╚═════╝

// COPY THESE HELPERS INTO YOUR SCRIPT

// __ASSERTS = assert.init()
//
// unittest_your_script(string[] case) =>
//     case = assert.new_case(case, 'Test Case')
//
//     // Your case assertions go here
//     assert.equal(1, 1, case, 'For example, one does equal one!')
//
// unittest(bool verbose = false) =>
//     var bool result = na
//     if assert.once()
//         unittest_your_script(__ASSERTS)
//         result := assert.report(__ASSERTS, verbose)
//     result
//
// result = unittest()


// QUICK REFERENCE

// case = assert.init()
//
// new_case(case, 'Asserts for floats and ints')
// assert.equal(a, b, case, 'a == b')
// assert.not_equal(a, b, case, 'a != b')
// assert.nan(a, case, 'a == na')
// assert.not_nan(a, case, 'a != na')
// assert.is_in(a, b, case, 'a in b[]')
// assert.is_not_in(a, b, case, 'a not in b[]')
// assert.array_equal(a, b, case, 'a[] == b[]')
//
// new_case(case, 'Asserts for ints only')
// assert.int_in(a, b, case, 'a in b[]')
// assert.int_not_in(a, b, case, 'a not in b[]')
// assert.int_array_equal(a, b, case, 'a[] == b[]')
//
// new_case(case, 'Asserts for bools only')
// assert.is_true(a, case, 'a == true')
// assert.is_false(a, case, 'a == false')
// assert.bool_equal(a, b, case, 'a == b')
// assert.bool_not_equal(a, b, case, 'a != b')
// assert.bool_nan(a, case, 'a == na')
// assert.bool_not_nan(a, case, 'a != na')
// assert.bool_array_equal(a, b, case, 'a[] == b[]')
//
// new_case(case, 'Asserts for strings only')
// assert.str_equal(a, b, case, 'a == b')
// assert.str_not_equal(a, b, case, 'a != b')
// assert.str_nan(a, case, 'a == na')
// assert.str_not_nan(a, case, 'a != na')
// assert.str_in(a, b, case, 'a in b[]')
// assert.str_not_in(a, b, case, 'a not in b[]')
// assert.str_array_equal(a, b, case, 'a[] == b[]')
//
// new_case(case, 'Asserts for colors only')
// assert.color_equal(#000000, #000000, case, 'black == black')
// assert.color_not_equal(#000000, #FFFFFF, case, 'black != white')
//
// assert.report(case)


//  ██████╗  ██╗       █████╗  ██████╗   █████╗  ██╗       ██████╗
// ██╔════╝  ██║      ██╔══██╗ ██╔══██╗ ██╔══██╗ ██║      ██╔════╝
// ██║  ██╗  ██║      ██║  ██║ ██████╦╝ ███████║ ██║      ╚█████╗
// ██║  ╚██╗ ██║      ██║  ██║ ██╔══██╗ ██╔══██║ ██║       ╚═══██╗
// ╚██████╔╝ ███████╗ ╚█████╔╝ ██████╦╝ ██║  ██║ ███████╗ ██████╔╝
//  ╚═════╝  ╚══════╝  ╚════╝  ╚═════╝  ╚═╝  ╚═╝ ╚══════╝ ╚═════╝

var FORMAT_COLOR_STRING = '[r:{0} g:{1} b:{2}]'
var FORMAT_ARRAY_STRING = '{0} (+{1})'
var FORMAT_SINGLE = '[ {0} ] {1}: "{2}" {3}.'
var FORMAT_DOUBLE = '[ {0} ] {1}: "{2}" {3} "{4}".'
var FORMAT_REPORT_HEADER = 'Unit Test {0} ({1}/{2}):'

var EQUAL = 'is equal to'
var NOT_EQUAL = 'is not equal to'
var GREATER = 'is greater than'
var LESSER = 'is lesser than'
var GREATER_OR_EQUAL = 'is greater than or equal to'
var LESSER_OR_EQUAL = 'is lesser than or equal to'
var TRUE = 'is true'
var NOT_TRUE = 'is not true'
var FALSE = 'is false'
var NOT_FALSE = 'is not false'
var NAN = 'is NaN'
var NOT_NAN = 'is not NaN'
var IN = 'is in'
var NOT_IN = 'is not in'

var DEFAULT_CASE = 'Unit Test'


// ███████╗ ██╗   ██╗ ███╗  ██╗  █████╗  ████████╗ ██╗  █████╗  ███╗  ██╗  ██████╗
// ██╔════╝ ██║   ██║ ████╗ ██║ ██╔══██╗ ╚══██╔══╝ ██║ ██╔══██╗ ████╗ ██║ ██╔════╝
// █████╗   ██║   ██║ ██╔██╗██║ ██║  ╚═╝    ██║    ██║ ██║  ██║ ██╔██╗██║ ╚█████╗
// ██╔══╝   ██║   ██║ ██║╚████║ ██║  ██╗    ██║    ██║ ██║  ██║ ██║╚████║  ╚═══██╗
// ██║      ╚██████╔╝ ██║ ╚███║ ╚█████╔╝    ██║    ██║ ╚█████╔╝ ██║ ╚███║ ██████╔╝
// ╚═╝       ╚═════╝  ╚═╝  ╚══╝  ╚════╝     ╚═╝    ╚═╝  ╚════╝  ╚═╝  ╚══╝ ╚═════╝

array_get(arr, offset) =>
    if offset < array.size(arr)
        array.get(arr, offset)
    else
        na

array_trim_end(arr, size) =>
    if array.size(arr) > size
        for i = array.size(arr) - 1 to size
            array.remove(arr, i)

tuple_push(tuples, a, b) => array.push(tuples, a), array.push(tuples, b)

tuple_pop(tuples) => b = array.pop(tuples), a = array.pop(tuples), [a, b]

tuple_set(tuples, offset, a, b) => array.set(tuples, offset * 2, a), array.set(tuples, (offset * 2) + 1, b)

tuple_get(tuples, offset) => [array_get(tuples, offset * 2), array_get(tuples, (offset * 2) + 1)]

tuple_size(tuples) => math.floor(array.size(tuples) / 2)

tuple_last(tuples) => tuple_get(tuples, tuple_size(tuples) - 1)

tuple_remove(tuples, offset) => array.remove(tuples, offset * 2), array.remove(tuples, offset * 2)

tuple_trim(tuples, size) => array_trim_end(tuples, math.round(size * 2))

str_increment(string value) => str.tostring(math.round(str.tonumber(value)) + 1)

str_decrement(string value) => str.tostring(math.round(str.tonumber(value)) - 1)

color_tostring(color _color) => str.format(FORMAT_COLOR_STRING, color.r(_color), color.g(_color), color.b(_color))

array_tostring(id, int limit = 20) => array.size(id) > limit ? str.format(FORMAT_ARRAY_STRING, str.tostring(array.slice(id, 0, limit)), array.size(id) - limit) : str.tostring(id)


//  █████╗   ██████╗  ██████╗ ███████╗ ██████╗  ████████╗
// ██╔══██╗ ██╔════╝ ██╔════╝ ██╔════╝ ██╔══██╗ ╚══██╔══╝
// ███████║ ╚█████╗  ╚█████╗  █████╗   ██████╔╝    ██║
// ██╔══██║  ╚═══██╗  ╚═══██╗ ██╔══╝   ██╔══██╗    ██║
// ██║  ██║ ██████╔╝ ██████╔╝ ███████╗ ██║  ██║    ██║
// ╚═╝  ╚═╝ ╚═════╝  ╚═════╝  ╚══════╝ ╚═╝  ╚═╝    ╚═╝

append_single(string[] case, string name, a, bool result, string pass, string fail) =>
    group = array.get(case, 0), count = str_increment(array.get(case, 1)), array.set(case, 1, count)
    msg = str.format(FORMAT_SINGLE, group, str.length(name) ? name : '#' + count, a, result ? pass : fail)
    tuple_push(case, msg, result ? '.' : 'x')
    result

append_double(string[] case, string name, a, b, bool result, string pass, string fail) =>
    group = array.get(case, 0), count = str_increment(array.get(case, 1)), array.set(case, 1, count)
    msg = str.format(FORMAT_DOUBLE, group, str.length(name) ? name : '#' + count, a, result ? pass : fail, b)
    tuple_push(case, msg, result ? '.' : 'x')
    result

assert_equal(a, b, string[] case, string name = '') => append_double(case, name, a, b, a == b, EQUAL, NOT_EQUAL)

assert_not_equal(a, b, string[] case, string name = '') => append_double(case, name, a, b, a != b, NOT_EQUAL, EQUAL)

assert_greater(a, b, string[] case, string name = '') => append_double(case, name, a, b, a > b, GREATER, LESSER)

assert_lesser(a, b, string[] case, string name = '') => append_double(case, name, a, b, a < b, LESSER, GREATER)

assert_greater_or_equal(a, b, string[] case, string name = '') => append_double(case, name, a, b, a >= b, GREATER_OR_EQUAL, LESSER_OR_EQUAL)

assert_lesser_or_equal(a, b, string[] case, string name = '') => append_double(case, name, a, b, a <= b, LESSER_OR_EQUAL, GREATER_OR_EQUAL)

assert_true(a, string[] case, string name = '') => append_single(case, name, str.tostring(a), a == true, TRUE, NOT_TRUE)

assert_false(a, string[] case, string name = '') => append_single(case, name, str.tostring(a), a == false, FALSE, NOT_FALSE)

assert_nan(a, string[] case, string name = '') => append_single(case, name, a, na(a), NAN, NOT_NAN)

assert_not_nan(a, string[] case, string name = '') => append_single(case, name, a, not na(a), NOT_NAN, NAN)

assert_in(a, b, string[] case, string name = '') => append_double(case, name, a, array_tostring(b), array.includes(b, a), IN, NOT_IN)

assert_not_in(a, b, string[] case, string name = '') => append_double(case, name, a, array_tostring(b), not array.includes(b, a), NOT_IN, IN)

assert_array_equal(a, b, string[] case, string name = '') =>
    size = array.size(a)
    equal = size == array.size(b)
    if equal and size
        for i = 0 to size - 1
            if array.get(a, i) != array.get(b, i)
                equal := false
                break
    append_double(case, name, array_tostring(a, 10), array_tostring(b, 10), equal, EQUAL, NOT_EQUAL)

process_results(string[] case, bool clear = true) =>
    results = array.size(case) > 2 ? array.slice(case, 2, array.size(case)) : array.new_string()
    total = tuple_size(case) - 1
    pass = array.new_string()
    fail = array.new_string()

    if total
        for i = 0 to total - 1
            [msg, result] = tuple_get(results, i)
            if result == '.'
                tuple_push(pass, msg, result)
            else if result == 'x'
                tuple_push(fail, msg, result)

    pass_count = tuple_size(pass)
    fail_count = tuple_size(fail)

    if clear
        array.clear(case)
        tuple_push(case, DEFAULT_CASE, '0')

    [total, pass_count, fail_count, pass, fail]


// ██╗      ██╗ ██████╗  ██████╗   █████╗  ██████╗  ██╗   ██╗
// ██║      ██║ ██╔══██╗ ██╔══██╗ ██╔══██╗ ██╔══██╗ ╚██╗ ██╔╝
// ██║      ██║ ██████╦╝ ██████╔╝ ███████║ ██████╔╝  ╚████╔╝
// ██║      ██║ ██╔══██╗ ██╔══██╗ ██╔══██║ ██╔══██╗   ╚██╔╝
// ███████╗ ██║ ██████╦╝ ██║  ██║ ██║  ██║ ██║  ██║    ██║
// ╚══════╝ ╚═╝ ╚═════╝  ╚═╝  ╚═╝ ╚═╝  ╚═╝ ╚═╝  ╚═╝    ╚═╝

// @function Restrict execution to only happen once. Usage: if assert.once()\n    happens_once()
// @returns bool, true on first execution within scope, false subsequently
export once() =>
    varip pending = true
    if pending
        pending := false
        true
    else
        false

// @function Restrict execution to happen a set number of times. Usage: if assert.only(5)\n    happens_five_times()
// @param repeat int, the number of times to return true
// @returns bool, true for the set number of times within scope, false subsequently
export only(int repeat = 1) =>
    var done = 0
    if done < repeat
        done += 1
        true
    else
        false

// @function Initialises the asserts array
// @returns string[], tuple based array to contain all unit test results and current case details (__ASSERTS)
export init() => array.from(DEFAULT_CASE, '0')

// @function Numeric assert equal. Usage: assert.equal(1, 1, case, 'one == one')
// @param a float, numeric value "a" to compare equal to "b"
// @param b float, numeric value "b" to compare equal to "a"
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
// @param name string, the current unit test name, if undefined the test index of the current case is used
// @returns bool, true if the assertion passes, false otherwise
export equal(float a, float b, string[] case, string name = '') => assert_equal(a, b, case, name)

// @function Numeric assert not equal. Usage: assert.not_equal(1, 2, case, 'one != two')
// @param a float, numeric value "a" to compare not equal "b"
// @param b float, numeric value "b" to compare not equal "a"
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
// @param name string, the current unit test name, if undefined the test index of the current case is used
// @returns bool, true if the assertion passes, false otherwise
export not_equal(float a, float b, string[] case, string name = '') => assert_not_equal(a, b, case, name)

// @function Numeric assert greater than. Usage: assert.greater(2, 1, case, 'two > one')
// @param a float, numeric value "a" to compare greater than "b"
// @param b float, numeric value "b" to compare lesser than "a"
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
// @param name string, the current unit test name, if undefined the test index of the current case is used
// @returns bool, true if the assertion passes, false otherwise
export greater(float a, float b, string[] case, string name = '') => assert_greater(a, b, case, name)

// @function Numeric assert lesser than. Usage: assert.not_equal(1, 2, case, 'one < two')
// @param a float, numeric value "a" to compare lesser than "b"
// @param b float, numeric value "b" to compare greater than "a"
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
// @param name string, the current unit test name, if undefined the test index of the current case is used
// @returns bool, true if the assertion passes, false otherwise
export lesser(float a, float b, string[] case, string name = '') => assert_lesser(a, b, case, name)

// @function Numeric assert greater than or equal to. Usage: assert.greater(2, 1, case, 'two >= one')
// @param a float, numeric value "a" to compare greater or equal to "b"
// @param b float, numeric value "b" to compare lesser than or equal to "a"
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
// @param name string, the current unit test name, if undefined the test index of the current case is used
// @returns bool, true if the assertion passes, false otherwise
export greater_or_equal(float a, float b, string[] case, string name = '') => assert_greater_or_equal(a, b, case, name)

// @function Numeric assert lesser than or equal to. Usage: assert.not_equal(1, 2, case, 'one <= two')
// @param a float, numeric value "a" to compare lesser than or equal to "b"
// @param b float, numeric value "b" to compare greater than or equal to "a"
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
// @param name string, the current unit test name, if undefined the test index of the current case is used
// @returns bool, true if the assertion passes, false otherwise
export lesser_or_equal(float a, float b, string[] case, string name = '') => assert_lesser_or_equal(a, b, case, name)

// @function Numeric assert is NaN. Usage: assert.nan(float(na), case, 'number is NaN')
// @param a float, numeric value "a" to check is NaN
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
// @param name string, the current unit test name, if undefined the test index of the current case is used
// @returns bool, true if the assertion passes, false otherwise
export nan(float a, string[] case, string name = '') => assert_nan(a, case, name)

// @function Numeric assert is not NaN. Usage: assert.not_nan(1, case, 'number is not NaN')
// @param a float, numeric value "a" to  check is not NaN
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
// @param name string, the current unit test name, if undefined the test index of the current case is used
// @returns bool, true if the assertion passes, false otherwise
export not_nan(float a, string[] case, string name = '') => assert_not_nan(a, case, name)

// @function Numeric assert value in float array. Usage: assert.is_in(1, array.from(1.0), case, '1 is in [1.0]')
// @param a float, numeric value "a" to check is in array "b"
// @param b float[], array "b" to check contains "a"
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
// @param name string, the current unit test name, if undefined the test index of the current case is used
// @returns bool, true if the assertion passes, false otherwise
export is_in(float a, float[] b, string[] case, string name = '') => assert_in(a, b, case, name)

// @function Numeric assert value not in float array. Usage: assert.is_not_in(2, array.from(1.0), case, '2 is not in [1.0]')
// @param a float, numeric value "a" to check is not in array "b"
// @param b float[], array "b" to check does not contain "a"
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
// @param name string, the current unit test name, if undefined the test index of the current case is used
// @returns bool, true if the assertion passes, false otherwise
export is_not_in(float a, float[] b, string[] case, string name = '') => assert_not_in(a, b, case, name)

// @function Float assert arrays are equal. Usage: assert.array_equal(array.from(1.0), array.from(1.0), case, '[1.0] == [1.0]')
// @param a float[], array "a" to check is identical to array "b"
// @param b float[], array "b" to check is identical to array "a"
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
// @param name string, the current unit test name, if undefined the test index of the current case is used
// @returns bool, true if the assertion passes, false otherwise
export array_equal(float[] a, float[] b, string[] case, string name = '') => assert_array_equal(a, b, case, name)

// @function Integer assert value in integer array. Usage: assert.int_in(1, array.from(1), case, '1 is in [1]')
// @param a int, value "a" to check is in array "b"
// @param b int[], array "b" to check contains "a"
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
// @param name string, the current unit test name, if undefined the test index of the current case is used
// @returns bool, true if the assertion passes, false otherwise
export int_in(int a, int[] b, string[] case, string name = '') => assert_in(a, b, case, name)

// @function Integer assert value not in integer array. Usage: assert.int_not_in(2, array.from(1), case, '2 is not in [1]')
// @param a int, value "a" to check is not in array "b"
// @param b int[], array "b" to check does not contain "a"
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
// @param name string, the current unit test name, if undefined the test index of the current case is used
// @returns bool, true if the assertion passes, false otherwise
export int_not_in(int a, int[] b, string[] case, string name = '') => assert_not_in(a, b, case, name)

// @function Integer assert arrays are equal. Usage: assert.int_array_equal(array.from(1), array.from(1), case, '[1] == [1]')
// @param a int[], array "a" to check is identical to array "b"
// @param b int[], array "b" to check is identical to array "a"
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
// @param name string, the current unit test name, if undefined the test index of the current case is used
// @returns bool, true if the assertion passes, false otherwise
export int_array_equal(int[] a, int[] b, string[] case, string name = '') => assert_array_equal(a, b, case, name)

// @function Boolean assert is true. Usage: assert.is_true(true, case, 'is true')
// @param a bool, value "a" to check is true
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
// @param name string, the current unit test name, if undefined the test index of the current case is used
// @returns bool, true if the assertion passes, false otherwise
export is_true(bool a, string[] case, string name = '') => assert_true(a, case, name)

// @function Boolean assert is false. Usage: assert.is_false(false, case, 'is false')
// @param a bool, value "a" to check is false
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
// @param name string, the current unit test name, if undefined the test index of the current case is used
// @returns bool, true if the assertion passes, false otherwise
export is_false(bool a, string[] case, string name = '') => assert_false(a, case, name)

// @function Boolean assert equal. Usage: assert.bool_equal(true, true, case, 'true == true')
// @param a bool, value "a" to compare equal to "b"
// @param b bool, value "b" to compare equal to "a"
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
// @param name string, the current unit test name, if undefined the test index of the current case is used
// @returns bool, true if the assertion passes, false otherwise
export bool_equal(bool a, bool b, string[] case, string name = '') => assert_equal(a, b, case, name)

// @function Boolean assert not equal. Usage: assert.bool_not_equal(true, false, case, 'true != false')
// @param a bool, value "a" to compare not equal "b"
// @param b bool, value "b" to compare not equal "a"
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
// @param name string, the current unit test name, if undefined the test index of the current case is used
// @returns bool, true if the assertion passes, false otherwise
export bool_not_equal(bool a, bool b, string[] case, string name = '') => assert_not_equal(a, b, case, name)

// @function Boolean assert is NaN. Usage: assert.bool_nan(bool(na), case, 'bool is NaN')
// @param a bool, value "a" to check is NaN
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
// @param name string, the current unit test name, if undefined the test index of the current case is used
// @returns bool, true if the assertion passes, false otherwise
export bool_nan(bool a, string[] case, string name = '') => assert_nan(a, case, name)

// @function Boolean assert is not NaN. Usage: assert.bool_not_nan(true, case, 'bool is not NaN')
// @param a bool, value "a" to check is not NaN
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
// @param name string, the current unit test name, if undefined the test index of the current case is used
// @returns bool, true if the assertion passes, false otherwise
export bool_not_nan(bool a, string[] case, string name = '') => assert_not_nan(a, case, name)

// @function Boolean assert arrays are equal. Usage: assert.bool_array_equal(array.from(true), array.from(true), case, '[true] == [true]')
// @param a bool[], array "a" to check is identical to array "b"
// @param b bool[], array "b" to check is identical to array "a"
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
// @param name string, the current unit test name, if undefined the test index of the current case is used
// @returns bool, true if the assertion passes, false otherwise
export bool_array_equal(bool[] a, bool[] b, string[] case, string name = '') => assert_array_equal(a, b, case, name)

// @function String assert equal. Usage: assert.str_equal('hi', 'hi', case, '"hi" == "hi"')
// @param a string, value "a" to compare equal to "b"
// @param b string, value "b" to compare equal to "a"
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
// @param name string, the current unit test name, if undefined the test index of the current case is used
// @returns bool, true if the assertion passes, false otherwise
export str_equal(string a, string b, string[] case, string name = '') => assert_equal(a, b, case, name)

// @function String assert not equal. Usage: assert.str_not_equal('hi', 'bye', case, '"hi" != "bye"')
// @param a string, value "a" to compare not equal "b"
// @param b string, value "b" to compare not equal "a"
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
// @param name string, the current unit test name, if undefined the test index of the current case is used
// @returns bool, true if the assertion passes, false otherwise
export str_not_equal(string a, string b, string[] case, string name = '') => assert_not_equal(a, b, case, name)

// @function String assert is NaN. Usage: assert.str_nan(string(na), case, 'string is NaN')
// @param a string, value "a" to check is NaN
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
// @param name string, the current unit test name, if undefined the test index of the current case is used
// @returns bool, true if the assertion passes, false otherwise
export str_nan(string a, string[] case, string name = '') => assert_nan(a, case, name)

// @function String assert is not NaN. Usage: assert.str_not_nan('hi', case', 'string is not NaN')
// @param a string, value "a" to check is not NaN
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
// @param name string, the current unit test name, if undefined the test index of the current case is used
// @returns bool, true if the assertion passes, false otherwise
export str_not_nan(string a, string[] case, string name = '') => assert_not_nan(a, case, name)

// @function String assert value in string array. Usage: assert.str_in('hi', array.from('hi'), case, '"hi" in ["hi"]')
// @param a string, value "a" to check is in array "b"
// @param b string[], array "b" to check contains "a"
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
// @param name string, the current unit test name, if undefined the test index of the current case is used
// @returns bool, true if the assertion passes, false otherwise
export str_in(string a, string[] b, string[] case, string name = '') => assert_in(a, b, case, name)

// @function String assert value not in string array. Usage: assert.str_in('hi', array.from('bye'), case, '"hi" in ["bye"]')
// @param a string, value "a" to check is not in array "b"
// @param b string[], array "b" to check does not contain "a"
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
// @param name string, the current unit test name, if undefined the test index of the current case is used
// @returns bool, true if the assertion passes, false otherwise
export str_not_in(string a, string[] b, string[] case, string name = '') => assert_not_in(a, b, case, name)

// @function String assert arrays are equal. Usage: assert.str_array_equal(array.from('hi'), array.from('hi'), case, '["hi"] == ["hi"]')
// @param a string[], array "a" to check is identical to array "b"
// @param b string[], array "b" to check is identical to array "a"
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
// @param name string, the current unit test name, if undefined the test index of the current case is used
// @returns bool, true if the assertion passes, false otherwise
export str_array_equal(string[] a, string[] b, string[] case, string name = '') => assert_array_equal(a, b, case, name)

// @function Color assert equal. Usage: assert.color_equal(#000000, #000000, case, 'black == black')
// @param a color, value "a" to compare equal to "b"
// @param b color, value "b" to compare equal to "a"
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
// @param name string, the current unit test name, if undefined the test index of the current case is used
// @returns bool, true if the assertion passes, false otherwise
export color_equal(color a, color b, string[] case, string name = '') => assert_equal(color_tostring(a), color_tostring(b), case, name)

// @function Color assert not equal. Usage: assert.str_not_equal(#000000, #FFFFFF, case, 'black != white')
// @param a color, value "a" to compare not equal "b"
// @param b color, value "b" to compare not equal "a"
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
// @param name string, the current unit test name, if undefined the test index of the current case is used
// @returns bool, true if the assertion passes, false otherwise
export color_not_equal(color a, color b, string[] case, string name = '') => assert_not_equal(color_tostring(a), color_tostring(b), case, name)

// @function Assign a new test case name, for the next set of unit tests. Usage: assert.new_case(case, 'My tests')
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
// @param name string, the case name for the next suite of tests
export new_case(string[] case, string name) =>
    if tuple_size(case) == 0
        runtime.error('[Assert:new_case] Unable to assign new case to empty __ASSERTS array!')
    array.set(case, 0, name)
    array.set(case, 1, '0')

// @function Clear all stored unit tests from all cases. Usage: assert.clear(case)
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
export clear(string[] case) => tuple_trim(case, 1), array.set(case, 1, '0')

// @function Revert the previous unit test. Usage: [string msg, bool result] = assert.revert(case)
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
// @returns [msg string, result bool], tuple containing the msg and result of the reverted test
export revert(string[] case) =>
    if tuple_size(case) == 1
        runtime.error(str.format('[Assert:revert] Unable to revert empty test case "{0}"!', array.get(case, 0)))
    count = str_decrement(array.get(case, 1))
    array.set(case, 1, count == '-1' ? '0' : count)
    [msg, result] = tuple_pop(case)
    [msg, result == '.']


// @function Check if the last unit test has passed. Usage: bool success = assert.passed(case)
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
// @param revert bool, optionally revert the test
// @returns bool, true only if the test passed
export passed(string[] case, bool revert = false) =>
    if tuple_size(case) == 1
        runtime.error(str.format('[Assert:passed] No tests found for test case "{0}"!', array.get(case, 0)))
    [msg, result] = tuple_last(case)
    if revert
        revert(case)
    result == '.'

// @function Check if the last unit test has failed. Usage: bool failure = assert.failed(case)
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
// @param revert bool, optionally revert the test
// @returns bool, true only if the test failed
export failed(string[] case, bool revert = false) =>
    if tuple_size(case) == 1
        runtime.error(str.format('[Assert:failed] No tests found for test case "{0}"!', array.get(case, 0)))
    [msg, result] = tuple_last(case)
    if revert
        revert(case)
    result == 'x'

// @function Report the outcome of unit tests that fail. Usage: bool passed = assert.report(case)
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
// @param verbose bool, toggle displaying the full report that includes the outcome of all tests
// @param position string, the position for the report (global position vars)
// @returns bool, true only if all tests passed
export report(string[] case, bool verbose = false, string position = na) =>
    [total, pass_count, fail_count, pass, fail] = process_results(case)
    text_size = verbose ? size.small : size.large

    if verbose and pass_count > 0
        // array.concat(fail, pass) // Bug? This reverses the order!
        for i = 0 to array.size(pass) - 1
            array.push(fail, array.get(pass, i))

    if (verbose and pass_count > 0) or fail_count > 0
        var tbl = table.new(na(position) ? position.top_center : position, 2, (verbose ? total : fail_count) + 1, #000000, fail_count ? #FF0000 : #00FF00, 1)
        title = str.format(FORMAT_REPORT_HEADER, verbose ? 'Results' : 'Failures', verbose ? pass_count : fail_count, total)
        table.cell(tbl, 0, 0, title, text_color=fail_count ? #FF0000 : #00FF00, text_halign=text.align_left, text_size=size.huge)
        for i = 0 to tuple_size(fail) - 1
            [msg, result] = tuple_get(fail, i)
            table.cell(tbl, 0, i + 1, '- ' + msg, text_color=#FFFFFF, text_halign=text.align_left, text_size=text_size)
            table.cell(tbl, 1, i + 1, result == '.' ? '✔️' : '❌', text_halign=text.align_left, text_size=text_size)

    fail_count > 0


// ██╗   ██╗ ███╗  ██╗ ██╗ ████████╗    ████████╗ ███████╗  ██████╗ ████████╗  ██████╗
// ██║   ██║ ████╗ ██║ ██║ ╚══██╔══╝    ╚══██╔══╝ ██╔════╝ ██╔════╝ ╚══██╔══╝ ██╔════╝
// ██║   ██║ ██╔██╗██║ ██║    ██║          ██║    █████╗   ╚█████╗     ██║    ╚█████╗
// ██║   ██║ ██║╚████║ ██║    ██║          ██║    ██╔══╝    ╚═══██╗    ██║     ╚═══██╗
// ╚██████╔╝ ██║ ╚███║ ██║    ██║          ██║    ███████╗ ██████╔╝    ██║    ██████╔╝
//  ╚═════╝  ╚═╝  ╚══╝ ╚═╝    ╚═╝          ╚═╝    ╚══════╝ ╚═════╝     ╚═╝    ╚═════╝

// @function Assert module unit tests, for inclusion in parent script test suite. Usage: assert.unittest_assert(__ASSERTS)
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
export unittest_assert(string[] case) =>
    [start_total, start_pass_count, start_fail_count, start_pass, start_fail] = process_results(case, clear = false)

    new_case(case, 'Assert:once')
    assert_true(once(), case)
    assert_true(once(), case)
    for i = 1 to 3
        trigger = once()
        if i == 1
            assert_true(trigger, case)
        else
            assert_false(trigger, case)

    new_case(case, 'Assert:only')
    assert_true(only(), case)
    assert_false(only(0), case) // but why?
    assert_true(only(1), case)
    assert_true(only(2), case)

    for i = 1 to 3
        trigger = only(2)
        if i <= 2
            assert_true(trigger, case)
        else
            assert_false(trigger, case)

    new_case(case, 'Assert:str_increment')
    assert_equal(str_increment('-1'), '0', case)
    assert_equal(str_increment('0'), '1', case)
    assert_equal(str_increment('1'), '2', case)
    assert_equal(str_increment('10'), '11', case)

    new_case(case, 'Assert:str_decrement')
    assert_equal(str_decrement('11'), '10', case)
    assert_equal(str_decrement('2'), '1', case)
    assert_equal(str_decrement('1'), '0', case)
    assert_equal(str_decrement('0'), '-1', case)

    new_case(case, 'Assert:color_tostring')
    assert_equal(color_tostring(#000000), str.format(FORMAT_COLOR_STRING, 0, 0, 0), case)
    assert_equal(color_tostring(#FFFFFF), str.format(FORMAT_COLOR_STRING, 255, 255, 255), case)

    new_case(case, 'Assert:array_tostring')
    assert_equal(array_tostring(array.new_float()), '[]', case)
    assert_equal(array_tostring(array.from(1, 2, 3)), '[1, 2, 3]', case)
    assert_equal(array_tostring(array.from('a', 'b', 'c')), '[a, b, c]', case)
    assert_equal(array_tostring(array.from(1, 2, 3, 4, 5), 3), str.format(FORMAT_ARRAY_STRING, array.from(1, 2, 3), 2), case)

    new_case(case, 'Assert:init')
    test_case = init()
    assert_array_equal(test_case, array.from('Unit Test', '0'), case, 'init')

    new_case(case, 'Assert:new_case')
    new_case(test_case, 'New Case')
    assert_equal(1, 1, test_case, 'pass')
    new_case1 = str.format(FORMAT_DOUBLE, 'New Case', 'pass', 1, EQUAL, 1)
    assert_array_equal(test_case, array.from('New Case', '1', new_case1, '.'), case, 'new_case #1')
    assert_true(passed(test_case), case, 'passed')
    new_case(test_case, 'Next Case')
    assert_equal(1, 2, test_case, 'fail')
    assert_true(failed(test_case), case, 'failed')
    next_case1 = str.format(FORMAT_DOUBLE, 'Next Case', 'fail', 1, NOT_EQUAL, 2)
    assert_array_equal(test_case, array.from('Next Case', '1', new_case1, '.', next_case1, 'x'), case, 'new_case #2')

    new_case(case, 'Assert:revert')
    revert(test_case)
    assert_array_equal(test_case, array.from('Next Case', '0', new_case1, '.'), case, 'revert #1')
    revert(test_case)
    assert_array_equal(test_case, array.from('Next Case', '0'), case, 'revert #2')

    assert_equal(1, 1, test_case)
    assert_equal(1, 2, test_case)

    new_case(case, 'Assert:names')
    num_case1 = str.format(FORMAT_DOUBLE, 'Next Case', '#1', 1, EQUAL, 1)
    num_case2 = str.format(FORMAT_DOUBLE, 'Next Case', '#2', 1, NOT_EQUAL, 2)
    assert_array_equal(test_case, array.from('Next Case', '2', num_case1, '.', num_case2, 'x'), case, 'auto-numeric')

    new_case(case, 'Assert:clear')
    clear(test_case)
    assert_array_equal(test_case, array.from('Next Case', '0'), case, 'reset')

    assert_equal(1, 1, test_case, 'pass')
    assert_equal(1, 2, test_case, 'fail')

    new_case(case, 'Assert:process_results')
    [test_total, test_pass_count, test_fail_count, test_pass, test_fail] = process_results(test_case)
    assert_equal(test_total, 2, case, 'test total')
    assert_equal(test_pass_count, 1, case, 'test pass count')
    assert_equal(test_fail_count, 1, case, 'test fail count')
    assert_array_equal(test_pass, array.from('[ Next Case ] pass: "1" is equal to "1".', '.'), case, 'pass array')
    assert_array_equal(test_fail, array.from(next_case1, 'x'), case, 'fail array')

    new_case(case, 'Assert:assertions')
    assert_equal(1 + 1, 2, case, "equal")
    assert_equal(1 + 2, 2, case, "equal fail")
    assert_true(failed(case, true), case, "equal failed")
    assert_not_equal(1 + 2, 2, case, "not equal")
    assert_not_equal(1 + 1, 2, case, "not equal fail")
    assert_true(failed(case, true), case, "not equal failed")
    assert_greater(2, 1, case, 'greater')
    assert_greater(1, 1, case, 'greater fail')
    assert_true(failed(case, true), case, 'greater failed')
    assert_lesser(1, 2, case, 'lesser')
    assert_lesser(1, 1, case, 'lesser fail')
    assert_true(failed(case, true), case, 'lesser failed')
    assert_greater_or_equal(2, 1, case, 'greater or equal #1')
    assert_greater_or_equal(1, 1, case, 'greater or equal #2')
    assert_greater_or_equal(1, 2, case, 'greater or equal fail')
    assert_true(failed(case, true), case, 'greater or equal failed')
    assert_lesser_or_equal(1, 2, case, 'lesser or equal #1')
    assert_lesser_or_equal(1, 1, case, 'lesser or equal #2')
    assert_lesser_or_equal(2, 1, case, 'lesser or equal fail')
    assert_true(failed(case, true), case, 'lesser or equal failed')
    assert_true(true, case, "true")
    assert_true(false, case, "true fail")
    assert_true(failed(case, true), case, "true failed")
    assert_false(false, case, "false")
    assert_false(true, case, "false fail")
    assert_true(failed(case, true), case, "false failed")
    assert_nan(int(na), case, "nan")
    assert_nan(int(1), case, "nan fail")
    assert_true(failed(case, true), case, "nan failed")
    assert_not_nan(int(1), case, "not nan")
    assert_not_nan(int(na), case, "not nan fail")
    assert_true(failed(case, true), case, "not nan failed")
    assert_in(1, array.from(1), case, "array")
    assert_in(2, array.from(1), case, "array fail")
    assert_true(failed(case, true), case, "array failed")
    assert_array_equal(array.new_float(), array.new_float(), case, 'array equal #1')
    assert_array_equal(array.new_float(1), array.new_float(1), case, 'array equal #2')
    assert_array_equal(array.new_float(1), array.new_float(2), case, 'array equal fail')
    assert_true(failed(case, true), case, "array equal failed")

    new_case(case, 'Assert:export assertions')
    equal(1, 1, case, 'equal')
    not_equal(1, 2, case, 'not_equal')
    greater(2, 1, case, 'greater')
    lesser(1, 2, case, 'lesser')
    greater_or_equal(2, 1, case, 'greater_or_equal')
    lesser_or_equal(1, 2, case, 'lesser_or_equal')
    nan(float(na), case, 'nan')
    not_nan(1, case, 'not_nan')
    is_in(1, array.from(1.0, 2.0), case, 'is_in')
    is_not_in(3, array.from(1.0, 2.0), case, 'is_not_in')
    array_equal(array.from(1.0, 2.0), array.from(1.0, 2.0), case, 'array_equal')
    int_in(1, array.from(1, 2), case, 'int_in')
    int_not_in(3, array.from(1, 2), case, 'int_not_in')
    int_array_equal(array.from(1, 2), array.from(1, 2), case, 'int_array_equal')
    is_true(true, case, 'is_true')
    is_false(false, case, 'is_false')
    bool_equal(true, true, case, 'bool_equal')
    bool_not_equal(true, false, case, 'bool_not_equal')
    bool_nan(bool(na), case, 'bool_nan')
    bool_not_nan(false, case, 'bool_not_nan')
    bool_array_equal(array.from(true, false), array.from(true, false), case, 'bool_array_equal')
    str_equal('hi', 'hi', case, 'str_equal')
    str_not_equal('hi', 'bye', case, 'str_not_equal')
    str_nan(string(na), case, 'str_nan #1')
    str_nan('', case, 'str_nan #2') // An empty string is NaN apparently...
    str_not_nan('hi', case, 'str_not_nan')
    str_in('yes', array.from('yes', 'no'), case, 'str_in')
    str_not_in('maybe', array.from('yes', 'no'), case, 'str_not_in')
    str_array_equal(array.from('yes', 'no'), array.from('yes', 'no'), case, 'str_array_equal')
    color_equal(#000000, #000000, case, 'color_equal')
    color_not_equal(#000000, #FFFFFF, case, 'color_not_equal')

    new_case(case, 'Assert:totals')
    [total, pass_count, fail_count, pass, fail] = process_results(case, clear = false)
    assert_equal(total - start_total, 98, case, "total")
    assert_equal(pass_count - start_pass_count, 98, case, "pass count")
    assert_equal(fail_count - start_fail_count, 0, case, "fail count")

// @function Run the assert module unit tests as a stand alone. Usage: assert.unittest()
// @param verbose bool, optionally disable the full report to only display failures
export unittest(bool verbose = true) =>
    if once()
        __ASSERTS = init()
        unittest_assert(__ASSERTS)
        report(__ASSERTS, verbose)


// ██████╗  ██╗   ██╗ ███╗  ██╗
// ██╔══██╗ ██║   ██║ ████╗ ██║
// ██████╔╝ ██║   ██║ ██╔██╗██║
// ██╔══██╗ ██║   ██║ ██║╚████║
// ██║  ██║ ╚██████╔╝ ██║ ╚███║
// ╚═╝  ╚═╝  ╚═════╝  ╚═╝  ╚══╝

unittest()

//       _v_    Do you trust the bots?
//      (* *)  /
//     __)#(__
//    ( _..._ )
//    || |_| ||
// >==() |_| ()==<
//     _(___)_
//    [-]   [-]
