#!/usr/bin/env bash

#------------------------------------------------------------------------------
# RediSearch Unit Tests Runner
#
# This script runs unit tests for the RediSearch project. It supports running
# all unit tests with options for debugging and sanitizer support.
#
# Author: RediSearch Team
#------------------------------------------------------------------------------

# Get script location and set up paths
PROGNAME="${BASH_SOURCE[0]}"
SCRIPT_DIR="$(cd "$(dirname "$PROGNAME")" &>/dev/null && pwd)"
ROOT_DIR=$(cd $SCRIPT_DIR/.. && pwd)

cd $SCRIPT_DIR

#------------------------------------------------------------------------------
# Print separator line for better readability
#------------------------------------------------------------------------------
print_separator() {
    local cols=80
    # Try to get terminal width
    if command -v tput >/dev/null 2>&1; then
        cols=$(tput cols 2>/dev/null || echo 80)
    fi
    printf "\n%s\n" "$(printf '%0.s-' $(seq 1 $((cols-1))))"
}

#------------------------------------------------------------------------------
# Result tracking helpers
#------------------------------------------------------------------------------
CURRENT_BLOCK=""
record_pass() { eval "${CURRENT_BLOCK}_PASSED+=(\"\$1\")"; }
record_fail() { eval "${CURRENT_BLOCK}_FAILED+=(\"\$1\")"; }

#------------------------------------------------------------------------------
# Display help information
#------------------------------------------------------------------------------
show_help() {
    cat <<'END'
        RediSearch Unit Tests Runner

        Usage: [ARGVARS...] unit-tests [--help|help]

        Arguments:
        BINDIR=path   Path to repo binary dir
        TEST=name      Run only the specified test
        VERBOSE=1      Show more detailed output
        GDB=1          Run tests with interactive gdb debugger (stops on crashes)
        HELP=1         Show this help message
END
}

#------------------------------------------------------------------------------
# Configure sanitizer options for memory error detection
#------------------------------------------------------------------------------
setup_sanitizer() {
    if [[ -n $SAN ]]; then
        ASAN_LOG=${LOGS_DIR}/${TEST_NAME}.asan.log
        export ASAN_OPTIONS="detect_odr_violation=0:alloc_dealloc_mismatch=0:halt_on_error=0:detect_leaks=1:log_path=${ASAN_LOG}:verbosity=0"
        export LSAN_OPTIONS="suppressions=$ROOT_DIR/tests/memcheck/asan.supp:verbosity=0"
    fi
}

#------------------------------------------------------------------------------
# Detect system architecture and OS using get-platform script
#------------------------------------------------------------------------------
detect_platform() {
    # Use the get-platform script to detect platform information
    ARCH=$($SCRIPT_DIR/get-platform --arch)
    OS=$($SCRIPT_DIR/get-platform --os)
    OSNICK=$($SCRIPT_DIR/get-platform --osnick)

    if [[ $VERBOSE == 1 ]]; then
        echo "Platform: $OS ($OSNICK) on $ARCH"
    fi
}

#------------------------------------------------------------------------------
# Create GDB command file (once per script run)
#------------------------------------------------------------------------------
create_gdb_command_file() {
    if [[ -z "$GDB_CMD_FILE" ]]; then
        GDB_CMD_FILE=$(mktemp)
        cat > "$GDB_CMD_FILE" << 'EOF'
set confirm off
set pagination off
set height 0
set width 0
set startup-quietly on
set verbose off
handle SIGSEGV stop print nopass
handle SIGABRT stop print nopass
define hook-stop
  if $_exitcode != -1
    quit
  end
  echo \n=== Program stopped due to signal ===\n
  bt
  echo \n=== Use 'continue' to proceed, 'quit' to exit ===\n
end
run
EOF
        # Register cleanup function to remove the file on exit
        trap 'rm -f "$GDB_CMD_FILE"' EXIT
    fi
}

#------------------------------------------------------------------------------
# Run a test with GDB for debugging crashes
#------------------------------------------------------------------------------
run_with_gdb() {
    local test_name="$1"
    shift
    local test_command=("$@")

    echo "Running test with gdb: $test_name"
    echo "GDB will stop on crashes/signals for debugging. Test will exit automatically on success."
    echo "Starting GDB session for: $test_name"
    echo "----------------------------------------"

    # Create GDB command file if it doesn't exist
    create_gdb_command_file

    # Use environment variables to disable paging completely and reduce startup text
    LINES=50000 COLUMNS=200 PAGER= GDB_COLORS= gdb --quiet -iex "set pagination off" -iex "set height 0" -iex "set width 0" -iex "set startup-quietly on" -iex "set verbose off" -x "$GDB_CMD_FILE" --args "${test_command[@]}"
    local test_result=$?
    (( EXIT_CODE |= $test_result ))
    echo "----------------------------------------"
    echo "GDB session ended for: $test_name"

    return $test_result
}

start_group() {
    local title="$1"
    if [[ -n $GITHUB_ACTIONS ]]; then
	    echo "::group::$title"
	else
	    printf "# Running $title:\n"
	fi
}

end_group() {
    if [[ -n $GITHUB_ACTIONS ]]; then
	    echo "::endgroup::"
	fi
}


#------------------------------------------------------------------------------
# Run a single test and report results
#------------------------------------------------------------------------------
run_single_test() {
    local test_path=$1
    local test_name=$(basename $test_path)
    local log_prefix=$2

    # We always run all tests
    # (TEST_LEAK option has been removed)

    # Setup test environment
    TEST_NAME="$test_name" setup_sanitizer
    LOG_FILE="${LOGS_DIR}/${log_prefix}${test_name}.log"

    # Run the test
    if [[ $GDB == 1 ]]; then
        echo -n "Running test: $test_name (with GDB) ... "
        if run_with_gdb "$test_name" "$test_path"; then
            echo "PASS"
            record_pass "$test_name"
        else
            echo "FAIL"
            record_fail "$test_name"
        fi
    else
        echo -n "Running test: $test_name (log: $LOG_FILE) ... "
        { $test_path > "$LOG_FILE" 2>&1; test_result=$?; (( EXIT_CODE |= $test_result )); } || true
        # Report results
        if [[ $test_result -eq 0 ]]; then
            echo "PASS"
            record_pass "$test_name"
        else
            echo "FAIL"
            record_fail "$test_name"
            echo "Test failed! Log output:"
            cat "$LOG_FILE"
        fi
    fi
}

#------------------------------------------------------------------------------
# Run C unit tests
#------------------------------------------------------------------------------
run_c_tests() {
    CURRENT_BLOCK=C_TESTS
    print_separator
    C_TESTS_DIR="$BINDIR/tests/ctests"
    if [[ ! -d "$C_TESTS_DIR" ]]; then
        echo "C tests directory not found: $C_TESTS_DIR"
        return 0
    fi

    start_group "C unit tests"
    cd $ROOT_DIR/tests/ctests

    if [[ -z $TEST ]]; then
        # Run all C tests
        for test in $(find $C_TESTS_DIR -maxdepth 1 -name "test_*" -type f -perm -u+x -print); do
            run_single_test "$test" ""
        done
    elif [[ -f $C_TESTS_DIR/$TEST ]]; then
        # Run single C test
        run_single_test "$C_TESTS_DIR/$TEST" ""
    else
        echo "Test not found: $TEST in $C_TESTS_DIR"
    fi
    end_group
}

#------------------------------------------------------------------------------
# Run C++ unit tests
#------------------------------------------------------------------------------
run_cpp_tests() {
    CURRENT_BLOCK=CPP_TESTS
    print_separator
    CPP_TESTS_DIR="$BINDIR/tests/cpptests"
    if [[ ! -d "$CPP_TESTS_DIR" ]]; then
        echo "C++ tests directory not found: $CPP_TESTS_DIR"
        return 0
    fi
    start_group "C++ unit tests"
    cd $ROOT_DIR/tests/cpptests
    TEST_NAME=rstest setup_sanitizer

    LOG_FILE="${LOGS_DIR}/rstest.log"

    if [[ -z $TEST ]]; then
        # Run all C++ tests
        if [[ $GDB == 1 ]]; then
            if run_with_gdb "rstest (all C++ tests)" "$CPP_TESTS_DIR/rstest"; then
                echo "C++ tests: PASS"
                record_pass "rstest (all)"
            else
                echo "C++ tests: FAIL"
                record_fail "rstest (all)"
            fi
        else
            echo "Running all C++ tests via ctest (log: $LOG_FILE)"
            { cd "$BINDIR" && ctest --test-dir tests/cpptests --output-on-failure -j $(nproc) 2>&1 | tee "$LOG_FILE"; test_result=${PIPESTATUS[0]}; (( EXIT_CODE |= $test_result )); } || true
            parse_cpp_test_results "$LOG_FILE"
        fi
    else
        # Run single C++ test if requested
        LOG_FILE="${LOGS_DIR}/rstest_${TEST}.log"
        if [[ $GDB == 1 ]]; then
            run_with_gdb "rstest --gtest_filter=$TEST" "$CPP_TESTS_DIR/rstest" "--gtest_filter=$TEST"
            test_result=$?
            if [[ $test_result -eq 0 ]]; then
                echo "C++ test $TEST: PASS"
                record_pass "$TEST"
            else
                echo "C++ test $TEST: FAIL"
                record_fail "$TEST"
            fi
        else
            echo "Running C++ test: $TEST via ctest (log: $LOG_FILE)"
            { cd "$BINDIR" && ctest --test-dir tests/cpptests -R "$TEST" --output-on-failure 2>&1 | tee "$LOG_FILE"; test_result=${PIPESTATUS[0]}; (( EXIT_CODE |= $test_result )); } || true
            parse_cpp_test_results "$LOG_FILE"
        fi
    fi
    end_group
}

#------------------------------------------------------------------------------
# Parse and display C++ test results
#------------------------------------------------------------------------------
parse_cpp_test_results() {
    local log_file=$1
    echo "Individual test results:"

    # CTest output format: "N/M Test #N: TestName .... Passed/Failed X.XX sec"
    # Use process substitution instead of pipe to avoid subshell variable scoping issues
    while read -r line; do
        # Extract test name by removing the prefix and the suffix (dots + status + time)
        test_name=$(echo "$line" | sed -E 's/^[0-9]+\/[0-9]+ Test +#[0-9]+: //' | sed -E 's/ \.{3,}.*//')
        # Extract the status field: everything after the row of dots
        local status=$(echo "$line" | sed -E 's/.*\.{3,}//')

        if [[ $status == *"Exception"* ]]; then
            local reason=$(echo "$status" | sed -E 's/.*Exception: //' | sed -E 's/ +[0-9]+\.[0-9]+ sec$//')
            echo "$test_name ... CRASH ($reason)"
            record_fail "$test_name"
        elif [[ $status == *"Timeout"* ]]; then
            echo "$test_name ... TIMEOUT"
            record_fail "$test_name"
        elif [[ $status == *"Failed"* ]]; then
            echo "$test_name ... FAIL"
            record_fail "$test_name"
        elif [[ $status == *"Skipped"* ]]; then
            echo "$test_name ... SKIPPED"
        elif [[ $status == *"Passed"* ]]; then
            echo "$test_name ... PASS"
            record_pass "$test_name"
        fi
    done < <(grep -E "Test\s+#[0-9]+:" "$log_file")

    # Print detailed gtest output for each failed test (not timeouts)
    if grep -q "Failed" "$log_file"; then
        printf "\n=============== FAILED TEST DETAILS ===============\n"
        awk '
            /Note: Google Test filter =/ {
                capture = 1
                buffer = ""
                # Extract test name from this line
                sub(/.*Note: Google Test filter = /, "")
                current_test = $0
            }
            capture {
                buffer = buffer $0 "\n"
            }
            /FAILED TEST/ {
                if (capture && buffer != "") {
                    print "------- " current_test " -------"
                    print ""
                    print buffer
                }
                capture = 0
                buffer = ""
            }
        ' "$log_file"
        printf "====================================================\n"
    fi

    # Show summary from CTest
    printf "\nTest summary:\n"
    grep -E "^[0-9]+% tests passed" "$log_file" || true

    # Show failed/timeout/crash test details if any
    # CTest failure reasons include: Failed, Timeout, SEGFAULT, Subprocess aborted, etc.
    if grep -qE "The following tests FAILED:" "$log_file"; then
        printf "\nFailed tests:\n"
        # Print all lines after "The following tests FAILED:" until an empty line or end
        sed -n '/The following tests FAILED:/,/^$/p' "$log_file" | tail -n +2 || true
    fi
}

#------------------------------------------------------------------------------
# Run coordinator unit tests
#------------------------------------------------------------------------------
run_coordinator_tests() {
    CURRENT_BLOCK=C_COORD_TESTS
    print_separator
    start_group "coordinator unit tests"

    # Define test directories with proper existence checking
    declare -a TEST_DIRS=()

    # Check C coordinator tests directory
    C_COORD_TESTS_DIR="$BINDIR/tests/ctests/coord_tests"
    if [[ -d "$C_COORD_TESTS_DIR" ]]; then
        TEST_DIRS+=("$C_COORD_TESTS_DIR")
    fi

    # Check C++ coordinator tests directory (for individual test binaries)
    CPP_COORD_TESTS_DIR="$BINDIR/tests/cpptests"
    if [[ -d "$CPP_COORD_TESTS_DIR" ]]; then
        TEST_DIRS+=("$CPP_COORD_TESTS_DIR")
    fi

    if [[ ${#TEST_DIRS[@]} -eq 0 ]]; then
        echo "No coordinator test directories found"
        end_group
        return 0
    fi

    # Track if we found the specific test when TEST is specified
    local test_found=0

    for TESTS_DIR in "${TEST_DIRS[@]}"; do
        if [[ -z $TEST ]]; then
            # Run all coordinator tests
            for test in $(find "$TESTS_DIR" -maxdepth 1 -name "test_*" -type f -perm -u+x -print); do
                run_single_test "$test" "coord_"
            done
        elif [[ -f "$TESTS_DIR/$TEST" ]]; then
            # Run single coordinator test
            run_single_test "$TESTS_DIR/$TEST" "coord_"
            test_found=1
        fi
    done

    # Only show error if we were looking for a specific test and didn't find it
    if [[ -n $TEST && $test_found -eq 0 ]]; then
        echo "Coordinator test not found: $TEST"
    fi

    end_group
}

#------------------------------------------------------------------------------
# Run C++ coordinator unit tests
#------------------------------------------------------------------------------
run_cpp_coord_tests() {
    CURRENT_BLOCK=CPP_COORD_TESTS
    print_separator
    start_group "C++ coordinator unit tests"

    CPP_COORD_TESTS_DIR="$BINDIR/tests/cpptests/coord_tests"
    if [[ ! -d "$CPP_COORD_TESTS_DIR" ]]; then
        echo "C++ coordinator tests directory not found: $CPP_COORD_TESTS_DIR"
        end_group
        return 0
    fi

    cd $ROOT_DIR/tests/cpptests/coord_tests
    TEST_NAME=rstest_coord setup_sanitizer

    LOG_FILE="${LOGS_DIR}/rstest_coord.log"

    if [[ -z $TEST ]]; then
        # Run all C++ coordinator tests
        if [[ $GDB == 1 ]]; then
            if run_with_gdb "rstest_coord (all C++ coordinator tests)" "$CPP_COORD_TESTS_DIR/rstest_coord"; then
                echo "C++ coordinator tests: PASS"
                record_pass "rstest_coord (all)"
            else
                echo "C++ coordinator tests: FAIL"
                record_fail "rstest_coord (all)"
            fi
        else
            echo "Running all C++ coordinator tests via ctest (log: $LOG_FILE)"
            { cd "$BINDIR" && ctest --test-dir tests/cpptests/coord_tests --output-on-failure -j $(nproc) 2>&1 | tee "$LOG_FILE"; test_result=${PIPESTATUS[0]}; (( EXIT_CODE |= $test_result )); } || true
            parse_cpp_test_results "$LOG_FILE"
        fi
    else
        # Run single C++ coordinator test if requested
        LOG_FILE="${LOGS_DIR}/rstest_coord_${TEST}.log"
        if [[ $GDB == 1 ]]; then
            run_with_gdb "rstest_coord --gtest_filter=$TEST" "$CPP_COORD_TESTS_DIR/rstest_coord" "--gtest_filter=$TEST"
            test_result=$?
            if [[ $test_result -eq 0 ]]; then
                echo "C++ coordinator test $TEST: PASS"
                record_pass "$TEST"
            else
                echo "C++ coordinator test $TEST: FAIL"
                record_fail "$TEST"
            fi
        else
            echo "Running C++ coordinator test: $TEST via ctest (log: $LOG_FILE)"
            { cd "$BINDIR" && ctest --test-dir tests/cpptests/coord_tests -R "$TEST" --output-on-failure 2>&1 | tee "$LOG_FILE"; test_result=${PIPESTATUS[0]}; (( EXIT_CODE |= $test_result )); } || true
            parse_cpp_test_results "$LOG_FILE"
        fi
    fi
    end_group
}

#------------------------------------------------------------------------------
# Run all unit tests
#------------------------------------------------------------------------------
run_all_tests() {
    # Run C tests
    run_c_tests

    # Run C++ tests
    run_cpp_tests

    # Run C coordinator tests
    run_coordinator_tests

    # Run C++ coordinator tests
    run_cpp_coord_tests
}

#------------------------------------------------------------------------------
# Collect diagnostics and logs if needed
#------------------------------------------------------------------------------
collect_diagnostics() {
    # Run memory check summary if needed
    if [[ -n $SAN || $VG == 1 ]]; then
        { UNIT=1 $ROOT_DIR/sbin/memcheck-summary; (( EXIT_CODE |= $? )); } || true
    fi

    # Collect logs if requested (before summary)
    if [[ $COLLECT_LOGS == 1 ]]; then
        cd $ROOT_DIR
        mkdir -p bin/artifacts/tests
        test_tar="bin/artifacts/tests/unit-tests-logs-${ARCH}-${OSNICK}.tgz"
        rm -f "$test_tar"
        find tests/logs -name "*.log*" | tar -czf "$test_tar" -T -
        echo "Tests logs:"
        du -ah --apparent-size bin/artifacts/tests
    fi
}

#------------------------------------------------------------------------------
# Print consolidated test results summary
#------------------------------------------------------------------------------
print_test_summary() {
    local total_passed=0 total_failed=0
    local any_failed=0

    printf "\n===============================================================================\n"
    printf "  TEST RESULTS SUMMARY\n"
    printf "===============================================================================\n\n"

    # Helper to print one block's results
    # Args: label, block_name
    _print_block_summary() {
        local label="$1"
        local block="$2"
        local p f t

        eval "p=\${#${block}_PASSED[@]}"
        eval "f=\${#${block}_FAILED[@]}"
        t=$((p + f))

        total_passed=$((total_passed + p))
        total_failed=$((total_failed + f))

        if [[ $t -eq 0 ]]; then
            printf "  %-44s [SKIPPED]\n" "$label"
        elif [[ $f -eq 0 ]]; then
            printf "  %-44s PASSED (%d/%d)\n" "$label" "$p" "$t"
        else
            printf "  %-44s FAILED (%d/%d passed)\n" "$label" "$p" "$t"
            any_failed=1
            local i name
            for ((i=0; i<f; i++)); do
                eval "name=\${${block}_FAILED[$i]}"
                printf "    - %s\n" "$name"
                if [[ -n $GITHUB_ACTIONS ]]; then
                    echo "::error::${label} failed: ${name}"
                fi
            done
        fi
    }

    _print_block_summary "C   Unit Tests"              C_TESTS
    _print_block_summary "C++ Unit Tests"              CPP_TESTS
    _print_block_summary "C   Coordinator Unit Tests"  C_COORD_TESTS
    _print_block_summary "C++ Coordinator Unit Tests"  CPP_COORD_TESTS

    local grand_total=$((total_passed + total_failed))
    printf "\n-------------------------------------------------------------------------------\n"
    printf "  TOTAL: %d passed, %d failed, %d total\n" "$total_passed" "$total_failed" "$grand_total"
    if [[ $any_failed -eq 1 ]]; then
        printf "  STATUS: SOME TESTS FAILED\n"
    elif [[ $EXIT_CODE -ne 0 ]]; then
        printf "  STATUS: FAILED (exit code %d, but no individual failures were captured)\n" "$EXIT_CODE"
    else
        printf "  STATUS: ALL TESTS PASSED\n"
    fi
    printf "===============================================================================\n"
}

#------------------------------------------------------------------------------
# Main execution starts here
#------------------------------------------------------------------------------

# Check for help request
[[ $1 == --help || $1 == help || $HELP == 1 ]] && { show_help; exit 0; }

# Detect platform information
detect_platform

# Setup paths and variables
# Calculate BINDIR from BINROOT if not already set
if [[ -z $BINDIR ]]; then
    if [[ -n $BINROOT ]]; then
        # Default to OSS build path - make it absolute
        if [[ "$BINROOT" = /* ]]; then
            # BINROOT is already absolute
            BINDIR="$BINROOT/search-community"
        else
            # BINROOT is relative to ROOT_DIR
            BINDIR="$ROOT_DIR/$BINROOT/search-community"
        fi
    else
        echo "Error: Neither BINDIR nor BINROOT is set"
        exit 1
    fi
fi

export EXT_TEST_PATH=${BINDIR}/example_extension/libexample_extension.so

# No test scope configuration needed

# Set up logs directory
LOGS_DIR=$ROOT_DIR/tests/logs
if [[ $CLEAR_LOGS != 0 ]]; then
    rm -rf $LOGS_DIR
fi
mkdir -p $LOGS_DIR

# Initialize exit code
EXIT_CODE=0

# Initialize result tracking arrays
declare -a C_TESTS_PASSED=() C_TESTS_FAILED=()
declare -a CPP_TESTS_PASSED=() CPP_TESTS_FAILED=()
declare -a C_COORD_TESTS_PASSED=() C_COORD_TESTS_FAILED=()
declare -a CPP_COORD_TESTS_PASSED=() CPP_COORD_TESTS_FAILED=()

# Run all tests
run_all_tests

# Collect diagnostics and handle logs
collect_diagnostics

# Print consolidated test results summary
print_test_summary

# Exit with the accumulated status code
exit $EXIT_CODE
