#!/bin/bash

VALCOM="valgrind -q --leak-check=full --error-exitcode=1 --track-origins=yes"

TOTAL_PASSES=0
TOTAL_FAILS=0
TOTAL_VALGRIND_ERRORS=0
FAILS=0
FAIL_OUTPUT=""
VALGRIND=false
DISABLED=false

if [ "$1" == "-val" ]; then
    VALGRIND=true
    filter="$2"
else
    filter="$1"
fi

FIRST_TITLE=true
title() {
    if ! $FIRST_TITLE; then
        echo ""
        if (($FAILS != 0)); then
            echo -e "$FAIL_OUTPUT"
            FAIL_OUTPUT=""
        fi
        FAILS=0
        DISABLED=false
    else
        FIRST_TITLE=false
    fi
    echo -n "[$1] "
    if [[ "$2" == "disabled" ]]; then
        echo -n "DISABLED"
        DISABLED=true
    fi
}

pass() {
    local color=32
    if [[ "$2" != "0" ]]; then
        ((TOTAL_VALGRIND_ERRORS++))
        color=33
    fi
    echo -n "[1;${color}m✓"
    ((TOTAL_PASSES++))
}

fail() {
    echo -n "X"
    FAIL_OUTPUT="$FAIL_OUTPUT  $1 FAILED\n    When running $2"
    ((FAILS++))
    ((TOTAL_FAILS++))
}


regex="regex"
check() {
    if $DISABLED || ! [[ "$1" =~ $filter ]]; then
        return 1
    fi

    local output
    local exit_code=0
    if $VALGRIND; then
        echo -ne "\n   $1"
        output="$($VALCOM ./pl --ignore-config "(loadfile \"examples/lib.pbl\") $2")"
        exit_code=$?
        if [[ "$exit_code" == "0" ]]; then
            echo -ne "\r "
        fi
    else
        output="$(./pl --ignore-config "(loadfile \"examples/lib.pbl\") $2")"
    fi


    local expected="$3"
    if [ "$3" == "$regex" ]; then
        expected="$4"
        if [[ "$output" =~ ^$4$ ]]; then
            pass "$1" $exit_code
            return
        fi
    elif [ "$output" == "$3" ]; then
        pass "$1" $exit_code
        return
    fi

    fail "$1" "$2"
    FAIL_OUTPUT="${FAIL_OUTPUT}\n    expected '$expected' but received '$output'\n"
}

echo "::SHELL TESTS::"

title "Basic Parsing"
check "Number" "10" "10"
check "String" '"hey"' "hey"
check "Hex Number" "0x0f0f0" "61680"
check "Binary Number" "0b011010011010011" "13523"

title "Arithmetic"
check "Addition" "(+ 1093 102852)" "103945"
check "Multiply" "(* 1000 10000)" "10000000"
check "Division" "(/ 6418425 65)" "98745"
check "Chain Addition" "(+ 3917 17304 1293844 400 100000)" "1415465"
check "Chain Multiplication" "(* 8263 23 123)" "23376027"
check "Chain Division" "(/ 1493856 741 96 7)" "3"

title "Comparison"
check "Greater-Than" "(> 23847123 19375933)" "T"
check "Multiple Greater-Than" "(> 9999 55 1 0)" "T"
check "Less-Than" "(< 23847123 19375933)" "F"
check "Equality" "(= 987654321 987654321 )" "T"
check "String Equality" '(= "Bean" "Bean" )' "T"
check "String Inequality" '(= "Beans" "Bean" )' "F"
check "Null Inequality" '(= "Beans" "" )' "F"

title "Complex Strings"
check "Escaping Quotes" '"\"Hello\""' '"Hello"'
check "Escaping Backslash" '"\\Hello\\"' '\Hello\'
check "Escaping Many" '"\\\"\\\\\\Hello\\\"\\\\\\"' '\"\\\Hello\"\\\'
check "Triple Quoting" '"""Hello"""' 'Hello'
check "Triple Quoting With Quotes" '"""My name is "yoink"."""' 'My name is "yoink".'

title "TypeCheck"
check "Is Num" "(isnum 23847123)" "T"
check "Is String" '(isstr "words")' "T"
check "Empty String Is String" '(isstr "")' "T"
check "String Is Not Num" '(isnum "WORDS")' "F"
check "List Is Not String" '(isstr ("hello"))' "F"
check "Num Is Not String" "(isstr 5)" "F"

title "Ifs/Bools"
check "IfReturn" "(if T 123456789 987654321)" "123456789"
check "IfRetTwo" "(if F 123456789 987654321)" "987654321"
check "EmptyCnd" "(if () T F)" "F"
check "EtyLstLt" "(if (()) T F)" "T"

title "Lists"
check "Normal List" "(1 2 3 4 5)" "( 1 2 3 4 5 )"
check "Heterogenous List" '(10 20 "rascals")' "( 10 20 rascals )"
check "Empty List" "()" "( )"
check "List Index" "(at (+ 1 1) (1 2 1000 4 5))" "1000"
check "Eval List Elements" "((* 10 10) 7)" "( 100 7 )"
check "Append To List" "(ap (1 20 300 4000 50000) 600000)" "( 1 20 300 4000 50000 600000 )"
check "Append To List In Env" "(def l (50000 4000 300 20)) (def l (ap l 1)) l" "( 50000 4000 300 20 1 )"
check "Prepend On List" "(pre (1 20 300 4000 50000) 600000)" "( 600000 1 20 300 4000 50000 )"
check "Prepend On Env List" "(def l (50000 4000 300 20)) (def l (pre l 1)) l" "( 1 50000 4000 300 20 )"
check "Rest Of List" "(def l (50000 4000 300 20)) (rest l)" "( 4000 300 20 )"
check "List Length" "(def l (1 20 3 \"abc\" \"banana\" (+ 10 5))) (len l)" "6"
check "Identifying List" "(islist (1 2 3))" "T"
check "Identifying Empty List" "(islist ())" "T"
check "Identifying Not A List" "(islist 1)" "F"

deep_nesting="10"
for _ in {0..25}; do deep_nesting="( $deep_nesting )"; done
check "DeepNesting" "$deep_nesting" "$deep_nesting" # Above 25 it starts to stack-smash

title "Spacing"
check "Wide Spacing" "(     +     1093      102852       )" "103945"
check "Very Wide Spacing" "       (     +     1093      102852       )       " "103945"
check "Wide Empty List" "(                     )" "( )"

title "DemoFunctions"
check "Squaring" "(sq 9876)" "97535376"
check "Cubing" "(cube 81)" "531441"
check "Exponent" "(exp 9 9)" "387420489"
check "MaxRight" "(max 5 20)" "20"
check "MaxLeft" "(max 135890 98372)" "135890"
check "MinRight" "(min 8429 192449)" "8429"
check "MinLeft" "(min 17294722 17294721)" "17294721"

title "Lambdas"
check "Multi-Statement" "(def yee (fn (a) (* 10 a))) (yee 5)" "50"
check "Pre-Defined Map" "(def yee (fn (a) (* 10 a))) (map yee (5 10 2 (+ 12 0)))" "( 50 100 20 120 )"
check "AnonymousLambda" "\
  (map (fn (a) (* a a)) (5 6 7))" "( 25 36 49 )"
check "FbnciSeq" "\
  (def fib (fn (a) \
    (if (< a 2) \
      a \
      (+ (fib (- a 1)) (fib (- a 2))) \
  )))\
  (fib 11)" "89"
check "Factorial" "\
  (def fac (fn (a) \
    (if (= a 1) \
      1 \
      (* a (fac (- a 1)) ) \
  )))\
  (fac 11)" "39916800"
check "Lambda Clone" "(def y (fn (a) (* 10 a))) (def b y) (def y 12345) ((b 5) y)" "( 50 12345 )"
check "Duplicate" "(def dupe (fn (a) (() (a a a)))) (dupe (* 10 10))" "( 100 100 100 )"

title "Cat"
check "Basic Cat" '(cat "Big" " Kitty")' "Big Kitty"
check "Cat Nums" '(cat "There are " (+ 2 3) " kitties")' "There are 5 kitties"

title "Filtering"
check "Filtering" "(fil (fn (a) (< 321 a)) (30 300 90 1200 135 801))" "( 1200 801 )"
check "FilterEval" "(fil (fn (a) (= 1000 a)) ((+ 450 550) (* 20 50) (/ 30 3) (- 10000 100)))" "( 1000 1000 )"
check "MapFilter" "(fil (fn (a) (< 50 a)) (map sq (1 2 3 4 5 6 7 8 9 10 11 12)))" "( 64 81 100 121 144 )"

title "Structs"
check "Struct Definition" "(struct Post (title body))" "T"
check "Building a struct"\
    '(struct Post (title body)) (Post "A title" "The Body")'\
    '{ title: "A title", body: "The Body" }'
check "Accessing struct fields"\
    "(struct Post (title body)) (def p (Post \"TITLE\" \"BODY\")) (p.title p.body)"\
    "( TITLE BODY )"

title "HigherOrder"
check "Simple Reduction" '(reduce (1 2 3) + 0)' '6'
check "Non-List Reduction" '(reduce 1 + 0)' '1'
check "String Reduction" '(reduce (" my" " friend") cat "Hello,")' 'Hello, my friend'
check "Lambda Returning a Lambda" "(def plusser (fn (outer) (fn (inner) (+ outer inner))))\
    (def plusFive (plusser 5))\
    (plusFive 10)" "15"

title "ShouldError"
check "Len of Not-List" "(len 5)" regex "BAD_PARAMS.*"
check "Map With No List" "(map sq)" regex "NULL_MAP_ARGS.*"
check "Bad Number" "(5df)" regex "BAD_NUMBER.*"
check "Bad Hex" "(0x0zf)" regex "BAD_NUMBER.*"
check "Bad Binary" "(0b01120)" regex "BAD_NUMBER.*"
check "Unsupported Number" "(00000)" regex "UNSUPPORTED_NUMBER.*"
check "Bad Parens 1" "(hey()" regex "MISMATCHED_PARENS.*"
check "Bad Parens 2" "(hey)(" regex "MISMATCHED_PARENS.*"
check "Bad Parens 3" "((hey(" regex "MISMATCHED_PARENS.*"
check "Bad Parens 4" ")))hey" regex "MISMATCHED_PARENS.*"
check "Bad Parens 5" "hey))(" regex "MISMATCHED_PARENS.*"
check "Bad Parens 6" '(ey")"' regex "MISMATCHED_PARENS.*"

title "ListArithmetic" disabled
check "Uneven Lists" "(+ (1 2) (1 2 3))" "LISTS_NOT_SAME_SIZE"
check "List Addition" "(+ 5 (1 2 3 (+ 10 10)))" "( 6 7 8 25 )"
check "List Modulo" "(% 5 (1 2 3 (* 11 11)))" "( 1 2 3 1 )"
check "List Multiplication" "(* (10 20 30) (1 20 300))" "( 10 400 9000 )"

title "Eval"
check "Basic Number Eval" '(eval "5")' "5"
check "Basic Op Eval" '(eval "(+ 5 10)")' "15"
check "Map Filter" \
    '(eval "(fil (fn (a) (< 50 a)) (map sq (1 2 3 4 5 6 7 8 9 10 11 12)))")' \
    "( 64 81 100 121 144 )"

title "Forbble"
check "BasicForbbleOp" '(loadfile "examples/forbble.pbl") (feval "10 10 * .")' "100"
check "Basic Forbble Definition" '(loadfile "examples/forbble.pbl") (feval ": ft 12 * ;") (feval "10 ft .")' "120"
check "FibForbble" '(loadfile "examples/forbble.pbl") (feval "1 1 fib fib fib fib fib fib fib fib fib fib fib fib fib fib fib fib fib fib fib fib fib .")' "28657"
#check "ForbbleDefine" '(loadfile "examples/forbble.pbl") (feval ( : "cubed" dup dup * * $ )) (feval (4 cubed .)) ""' "64"

title "Environment"
check "EnvStressTestEarly" '(def a 1)(def b 20)(def c "yee")(def d "yeehunnid")(def e 3) (a)' "( 1 )"
check "EnvStressTestLate" "(def a 1)(def b 2)(def c 3)(def d 4)(def e 5)(def g 6)(def n 40) n" "40"
hard_test_string="(def n 1000)"
for c in {0..50}; do hard_test_string="(def a$c 1)$hard_test_string"; done
check "HardEnvStressTest" "$hard_test_string n" "1000"

echo ""
echo ""

if [ "$TOTAL_PASSES" -ne "0" ]; then
    echo -n ""
fi
echo -n "$TOTAL_PASSES Tests Passed"
if [ "$TOTAL_VALGRIND_ERRORS" -ne "0" ]; then
    echo -n " ($TOTAL_VALGRIND_ERRORS with valgrind errors)"
fi
echo

if [ "$TOTAL_FAILS" -ne "0" ]; then
    echo -n ""
fi
echo "$TOTAL_FAILS Tests Failed"

echo ""

if $VALGRIND; then
    i=0
    while true; do
        valgrind -q --leak-check=full --error-exitcode=1 --track-origins=yes ./pl --run-test $i
        if [[ "$?" == "255" ]]; then
            break
        fi
        i=$((i+1))
    done
else
    ./pl --run-tests
fi