#!/bin/bash
# Run IREE tests with Bazel.
#
# Usage: iree-bazel-test [options] [targets...] [bazel-args...]
#
# Examples:
#   iree-bazel-test                       # Run all tests (CPU only)
#   iree-bazel-test //compiler/...        # Test compiler only
#   iree-bazel-test --config=asan         # Test with AddressSanitizer
#   iree-bazel-test --coverage //runtime/src/iree/base:printf_test

set -e

# Source shared library.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/iree-bazel-lib"
iree_bazel_init "iree-bazel-test"

# Expand combined short flags (e.g., -nv -> -n -v).
eval "set -- $(iree_expand_combined_flags "$@")"

show_help() {
    cat << 'EOF'
iree-bazel-test - Run IREE tests with Bazel

USAGE
    iree-bazel-test [options] [targets...] [bazel-args...]

OPTIONS
    -n, --dry_run       Show the bazel command without executing
    -v, --verbose       Show the bazel command before executing
    -w, --watch         Watch mode: rerun tests on file changes
    --coverage          Run with source-based code coverage and report
    --coverage-fuzz     Like --coverage, also merge fuzzer corpus coverage
    --coverage-html=DIR Generate HTML coverage report in DIR
    -h, --help          Show this help

    NOTE: Short flags can be combined: -nv is equivalent to -n -v
    NOTE: --keep_going is always enabled for test runs.

COVERAGE
    --coverage builds without sanitizers (for clean instrumentation), runs
    the test binary, and reports line/branch/function coverage using
    llvm-cov. Only same-package source files are included in the report.

    --coverage-fuzz additionally finds a sibling _fuzz target (e.g.,
    printf_test -> printf_fuzz), runs it over its fuzzer corpus with
    -runs=0, and merges the profiles for combined coverage.

    --coverage-html=DIR writes a browsable HTML report to the given
    directory (implies --coverage).

    Examples:
      iree-bazel-test --coverage //runtime/src/iree/base:printf_test
      iree-bazel-test --coverage-fuzz //runtime/src/iree/base:printf_test
      iree-bazel-test --coverage-html=/tmp/cov //runtime/src/iree/base:printf_test

ARGUMENTS
    targets      One or more Bazel targets (at least one required)
    bazel-args   Additional arguments passed to bazel test

EXAMPLES
    # Test all compiler components
    iree-bazel-test //compiler/...

    # Test a specific test target
    iree-bazel-test //runtime/src/iree/base:status_test

    # Test multiple targets at once
    iree-bazel-test //runtime/src/iree/tokenizer/... //tools/test:iree-tokenize.txt

    # Test all tools
    iree-bazel-test //tools/...

    # Test with AddressSanitizer
    iree-bazel-test //compiler/... --config=asan

    # Watch and rerun on changes (great for TDD)
    iree-bazel-test -w //runtime/src/iree/base:status_test

    # Show command without executing (verbose dry-run)
    iree-bazel-test -nv //compiler/...

    # Coverage report
    iree-bazel-test --coverage //runtime/src/iree/base:printf_test

DEFAULT BEHAVIOR
    - Runs tests using drivers configured in 'iree-bazel-configure'
    - Continues on test failures (--keep_going)

CONFIGURING DRIVERS
    Configure drivers ONCE, then test repeatedly:

    IREE_HAL_DRIVER_CUDA=ON iree-bazel-configure   # Enable CUDA
    iree-bazel-test //runtime/...                   # Tests include CUDA

    IREE_HAL_DRIVER_CUDA=OFF iree-bazel-configure  # Disable CUDA
    iree-bazel-test //runtime/...                   # CUDA tests skipped

    All configuration happens during 'iree-bazel-configure'.

CONFIGURATIONS
    --config=localdev        Local dev optimizations (disk cache, skymeld)
    --config=debug           Debug build tests
    --config=asan            Address Sanitizer
    --config=msan            Memory Sanitizer
    --config=tsan            Thread Sanitizer

SEE ALSO
    iree-bazel-configure, iree-bazel-build
EOF
}

# Parse arguments.
WATCH_MODE=0
COVERAGE_MODE=0
COVERAGE_FUZZ=0
COVERAGE_HTML_DIR=""
TARGETS=()
BAZEL_ARGS=()

while [[ $# -gt 0 ]]; do
    case "${1}" in
        -h|--help)
            show_help
            exit 0
            ;;
        --agent-md|--agent_md)
            iree_show_agent_md
            exit 0
            ;;
        -n|--dry_run|--dry-run)
            IREE_BAZEL_DRY_RUN=1
            shift
            ;;
        -v|--verbose)
            IREE_BAZEL_VERBOSE=1
            shift
            ;;
        -w|--watch)
            WATCH_MODE=1
            shift
            ;;
        --coverage)
            COVERAGE_MODE=1
            shift
            ;;
        --coverage-fuzz|--coverage_fuzz)
            COVERAGE_MODE=1
            COVERAGE_FUZZ=1
            shift
            ;;
        --coverage-html=*|--coverage_html=*)
            COVERAGE_MODE=1
            COVERAGE_HTML_DIR="${1#*=}"
            shift
            ;;
        -*)
            BAZEL_ARGS+=("${1}")
            shift
            ;;
        *)
            TARGETS+=("${1}")
            shift
            ;;
    esac
done

# Set up worktree (after arg parsing so --help works anywhere).
iree_setup_worktree

# At least one target is required.
if [[ ${#TARGETS[@]} -eq 0 ]]; then
    iree_error "Target is required for bazel test"
    echo ""
    echo "Examples:"
    echo "  iree-bazel-test //compiler/..."
    echo "  iree-bazel-test //runtime/src/iree/base:status_test"
    echo "  iree-bazel-test //tools/..."
    exit 1
fi

# =============================================================================
# Coverage mode: build with instrumentation, run, report with llvm-cov
# =============================================================================

if [[ "${COVERAGE_MODE}" == "1" ]]; then
    # Coverage requires a single concrete target (not //...).
    if [[ ${#TARGETS[@]} -ne 1 ]]; then
        iree_error "--coverage requires exactly one target"
        exit 1
    fi
    TARGET="${TARGETS[0]}"

    # Validate it looks like a concrete target (has a colon).
    if [[ "${TARGET}" != *":"* ]]; then
        iree_error "--coverage requires a concrete target (e.g., //path:target_test), not a wildcard"
        exit 1
    fi

    # Extract package and target name.
    TARGET_PACKAGE="${TARGET%%:*}"   # //runtime/src/iree/base
    TARGET_NAME="${TARGET##*:}"      # printf_test

    # Determine source directory for coverage filtering.
    # Strip leading // to get the filesystem path.
    SOURCE_DIR="${TARGET_PACKAGE#//}"

    # Coverage instrumentation flags. No sanitizers — they distort inlining
    # and code generation, which changes what llvm-cov attributes to each line.
    COVERAGE_COPT=(
        --copt=-fprofile-instr-generate
        --copt=-fcoverage-mapping
        --linkopt=-fprofile-instr-generate
    )

    # Filter out any --config=asan/msan/tsan from BAZEL_ARGS — sanitizers
    # and coverage instrumentation don't mix well.
    FILTERED_BAZEL_ARGS=()
    for arg in "${BAZEL_ARGS[@]}"; do
        case "${arg}" in
            --config=asan|--config=msan|--config=tsan|--config=fuzzer)
                iree_warn "Stripping ${arg} (incompatible with --coverage)"
                ;;
            *)
                FILTERED_BAZEL_ARGS+=("${arg}")
                ;;
        esac
    done

    # Build targets to collect coverage from.
    BUILD_TARGETS=("${TARGET}")
    FUZZ_TARGET=""
    FUZZ_CORPUS_DIR=""

    if [[ "${COVERAGE_FUZZ}" == "1" ]]; then
        # Derive fuzz target name: foo_test -> foo_fuzz.
        FUZZ_NAME="${TARGET_NAME%_test}_fuzz"
        FUZZ_TARGET="${TARGET_PACKAGE}:${FUZZ_NAME}"

        # Check if the fuzz target exists.
        BAZEL_BIN=$(iree_get_bazel_command)
        if "${BAZEL_BIN}" query "${FUZZ_TARGET}" >/dev/null 2>&1; then
            BUILD_TARGETS+=("${FUZZ_TARGET}")

            # Find the fuzzer corpus directory (mirrors iree-bazel-fuzz layout).
            FUZZ_CACHE_DIR="${IREE_FUZZ_CACHE:-${HOME}/.cache/iree-fuzz-cache}"
            FUZZ_CORPUS_DIR="${FUZZ_CACHE_DIR}/${SOURCE_DIR}/${FUZZ_NAME}/corpus"

            if [[ -d "${FUZZ_CORPUS_DIR}" ]]; then
                local_corpus_count=$(find "${FUZZ_CORPUS_DIR}" -maxdepth 1 -type f 2>/dev/null | wc -l)
                iree_info "Fuzz corpus: ${local_corpus_count} files in ${FUZZ_CORPUS_DIR}"
            else
                iree_warn "No fuzz corpus found at ${FUZZ_CORPUS_DIR}"
                iree_warn "Run iree-bazel-fuzz ${FUZZ_TARGET} first to build a corpus"
                FUZZ_TARGET=""
                FUZZ_CORPUS_DIR=""
            fi
        else
            iree_warn "Fuzz target ${FUZZ_TARGET} not found, skipping"
            FUZZ_TARGET=""
        fi
    fi

    # Build all targets with coverage instrumentation.
    iree_info "Building with coverage instrumentation..."
    iree_bazel_build_default_configs
    BUILD_CMD_ARGS=("${IREE_BAZEL_DEFAULT_CONFIGS[@]}" "${COVERAGE_COPT[@]}" "${FILTERED_BAZEL_ARGS[@]}")

    if iree_is_dry_run; then
        iree_info "Would build: ${BUILD_TARGETS[*]}"
        iree_info "Build args: ${BUILD_CMD_ARGS[*]}"
        exit 0
    fi

    BAZEL_BIN=$(iree_get_bazel_command)
    BUILD_LOG=$(mktemp /tmp/iree-cov-build.XXXXXX)
    if ! "${BAZEL_BIN}" build "${BUILD_CMD_ARGS[@]}" "${BUILD_TARGETS[@]}" >"${BUILD_LOG}" 2>&1; then
        tail -20 "${BUILD_LOG}"
        rm -f "${BUILD_LOG}"
        iree_error "Coverage build failed"
        exit 1
    fi
    tail -5 "${BUILD_LOG}"
    rm -f "${BUILD_LOG}"

    # Get binary paths.
    TEST_BINARY=$(iree_bazel_get_binary_path "${TARGET}" "${BUILD_CMD_ARGS[@]}")
    if [[ -z "${TEST_BINARY}" ]] || [[ ! -x "${TEST_BINARY}" ]]; then
        iree_error "Could not find test binary for ${TARGET}"
        exit 1
    fi

    FUZZ_BINARY=""
    if [[ -n "${FUZZ_TARGET}" ]]; then
        FUZZ_BINARY=$(iree_bazel_get_binary_path "${FUZZ_TARGET}" "${BUILD_CMD_ARGS[@]}")
        if [[ -z "${FUZZ_BINARY}" ]] || [[ ! -x "${FUZZ_BINARY}" ]]; then
            iree_warn "Could not find fuzz binary for ${FUZZ_TARGET}, skipping"
            FUZZ_BINARY=""
        fi
    fi

    # Set up temp directory for profiling data.
    PROF_DIR=$(mktemp -d /tmp/iree-coverage.XXXXXX)
    trap "rm -rf '${PROF_DIR}'" EXIT

    # Run the test binary with profiling.
    iree_info "Running test: ${TARGET_NAME}..."
    LLVM_PROFILE_FILE="${PROF_DIR}/test.profraw" "${TEST_BINARY}" >/dev/null 2>&1
    TEST_EXIT=$?
    if [[ ${TEST_EXIT} -ne 0 ]]; then
        iree_warn "Test exited with code ${TEST_EXIT} (coverage may be partial)"
    fi

    # Collect profraw files and binary objects for llvm-cov.
    PROFRAW_FILES=("${PROF_DIR}/test.profraw")
    OBJECT_ARGS=("${TEST_BINARY}")

    # Run the fuzzer over its corpus if available.
    if [[ -n "${FUZZ_BINARY}" ]] && [[ -n "${FUZZ_CORPUS_DIR}" ]]; then
        iree_info "Running fuzzer over corpus..."
        LLVM_PROFILE_FILE="${PROF_DIR}/fuzz.profraw" "${FUZZ_BINARY}" "${FUZZ_CORPUS_DIR}" -runs=0 >/dev/null 2>&1 || true
        if [[ -f "${PROF_DIR}/fuzz.profraw" ]]; then
            PROFRAW_FILES+=("${PROF_DIR}/fuzz.profraw")
            OBJECT_ARGS+=("-object" "${FUZZ_BINARY}")
        fi
    fi

    # Merge profiles.
    iree_info "Merging profiles..."
    if ! llvm-profdata merge -sparse "${PROFRAW_FILES[@]}" -o "${PROF_DIR}/merged.profdata" 2>/dev/null; then
        iree_error "llvm-profdata merge failed (is llvm-profdata installed?)"
        exit 1
    fi

    # Discover source files for coverage filtering.
    #
    # Convention: foo_test tests foo.c (and possibly foo_impl.c, foo_internal.c).
    # We include the test source itself plus any same-directory source files
    # whose name starts with the base name (target name without _test suffix).
    #
    # Bazel stores coverage mapping paths with /proc/self/cwd/ prefix (the
    # sandbox root), so we must use that prefix for llvm-cov source matching.
    iree_info "Discovering source files..."
    SOURCE_FILES=()

    # The test source file itself.
    for ext in .cc .cpp .c; do
        CANDIDATE="${SOURCE_DIR}/${TARGET_NAME}${ext}"
        if [[ -f "${IREE_BAZEL_WORKTREE_DIR}/${CANDIDATE}" ]]; then
            SOURCE_FILES+=("/proc/self/cwd/${CANDIDATE}")
            break
        fi
    done

    # Source files for the library under test: strip _test suffix and find
    # matching source files in the same directory. Excludes _test and _fuzz
    # files since those aren't library sources.
    BASE_NAME="${TARGET_NAME%_test}"
    if [[ "${BASE_NAME}" != "${TARGET_NAME}" ]]; then
        for candidate in "${IREE_BAZEL_WORKTREE_DIR}/${SOURCE_DIR}/${BASE_NAME}"*.c \
                         "${IREE_BAZEL_WORKTREE_DIR}/${SOURCE_DIR}/${BASE_NAME}"*.cc \
                         "${IREE_BAZEL_WORKTREE_DIR}/${SOURCE_DIR}/${BASE_NAME}"*.cpp; do
            if [[ -f "${candidate}" ]]; then
                CANDIDATE_NAME="${candidate##*/}"
                # Skip test and fuzz files.
                case "${CANDIDATE_NAME}" in
                    *_test.*|*_fuzz.*) continue ;;
                esac
                REL_PATH="${candidate#${IREE_BAZEL_WORKTREE_DIR}/}"
                SOURCE_FILES+=("/proc/self/cwd/${REL_PATH}")
            fi
        done
    fi

    if [[ ${#SOURCE_FILES[@]} -eq 0 ]]; then
        iree_warn "No source files discovered, showing all coverage"
    else
        # Print just filenames for readability.
        BASENAMES=()
        for src in "${SOURCE_FILES[@]}"; do BASENAMES+=("${src##*/}"); done
        iree_info "Source files: ${BASENAMES[*]}"
    fi

    # Generate report.
    echo ""
    if ! llvm-cov report "${OBJECT_ARGS[0]}" "${OBJECT_ARGS[@]:1}" \
        -instr-profile="${PROF_DIR}/merged.profdata" \
        "${SOURCE_FILES[@]}" 2>/dev/null; then
        iree_error "llvm-cov report failed (is llvm-cov installed?)"
        exit 1
    fi

    # Generate HTML report if requested.
    if [[ -n "${COVERAGE_HTML_DIR}" ]]; then
        mkdir -p "${COVERAGE_HTML_DIR}"
        iree_info "Generating HTML report in ${COVERAGE_HTML_DIR}..."
        llvm-cov show "${OBJECT_ARGS[0]}" "${OBJECT_ARGS[@]:1}" \
            -instr-profile="${PROF_DIR}/merged.profdata" \
            "${SOURCE_FILES[@]}" \
            -format=html -output-dir="${COVERAGE_HTML_DIR}" 2>/dev/null
        iree_info "HTML report: ${COVERAGE_HTML_DIR}/index.html"
    fi

    exit 0
fi

# =============================================================================
# Normal test mode
# =============================================================================

# Build tag filters based on environment.
TAG_FILTERS="-nodocker"
BUILD_FILTERS=""

# Driver filtering is controlled by configured.bazelrc (written by iree-bazel-configure).
# No runtime env var checks - configuration happens once during configure.

# Build the command with default configs before user args.
iree_bazel_build_default_configs
BAZEL_BIN=$(iree_get_bazel_command "${WATCH_MODE}")
CMD=("${BAZEL_BIN}" test "${IREE_BAZEL_DEFAULT_CONFIGS[@]}" --keep_going
    --test_tag_filters="${TAG_FILTERS}"
    --build_tag_filters="${BUILD_FILTERS}"
    "${TARGETS[@]}"
    "${BAZEL_ARGS[@]}")

# Execute or show.
if iree_is_verbose || iree_is_dry_run; then
    iree_info "Command: ${CMD[*]}"
fi

if iree_is_dry_run; then
    exit 0
fi

iree_info "Testing ${TARGETS[*]}"
iree_info "Tag filters: ${TAG_FILTERS}"
exec "${CMD[@]}"
