#!/usr/bin/env bash
# Overnight fuzz run: all soliton targets in parallel.
# Covers both core (soliton) and CAPI (soliton_capi) fuzz targets.
#
# Usage: ./fuzz_overnight.sh [hours] [workers]
#   hours    — how long to fuzz (default: 8)
#   workers  — libFuzzer workers per target (default: 1)
#              1 worker → 32 total threads (one per target)
#              2 workers → 64 total threads (two per target)
set -euo pipefail

if ! command -v parallel &>/dev/null; then
    echo "ERROR: GNU parallel is required." >&2
    exit 1
fi

HOURS="${1:-8}"
WORKERS="${2:-1}"
SECONDS_TOTAL=$((HOURS * 1))

CORE_DIR="soliton"
CAPI_DIR="soliton_capi"
LOG_BASE="fuzz_logs"

CORE_TARGETS=(
    fuzz_ed25519_verify
    fuzz_hybrid_verify
    # fuzz_xwing_roundtrip  # Excluded: 7 corpus entries after 90.5B execs, fully saturated (keygen→encap→decap, no adversarial input)
    fuzz_ratchet_decrypt
    fuzz_ratchet_decrypt_stateful
    fuzz_ratchet_encrypt
    fuzz_kex_receive_session
    fuzz_kex_verify_bundle
    fuzz_identity_sign_verify
    # fuzz_auth_respond  # Excluded: 4 corpus entries after 68B execs, fully saturated (single decap→HMAC)
    fuzz_storage_decrypt_blob
    # fuzz_identity_from_bytes  # Excluded: 107 edges, 3 length checks only, saturates instantly
    fuzz_decrypt_first_message
    fuzz_storage_encrypt_blob
    # fuzz_verification_phrase  # Excluded: 88 edges, 29 corpus entries, trivial surface (SHA3→wordlist index)
    fuzz_ratchet_roundtrip
    fuzz_session_init_roundtrip
    fuzz_call_derive
    # fuzz_auth_verify  # Excluded: 88 edges, saturated after ~15 corpus entries (single ct_eq call)
    fuzz_ratchet_from_bytes_epoch
    fuzz_kex_decode_receive
    fuzz_dm_queue_roundtrip
    fuzz_dm_queue_decrypt_blob
    fuzz_argon2_params
    fuzz_stream_decrypt
    fuzz_stream_decrypt_at
    fuzz_stream_encrypt_decrypt
    fuzz_stream_encrypt_at
    fuzz_ratchet_state_machine
)

CAPI_TARGETS=(
    fuzz_capi_ratchet_from_bytes
    fuzz_capi_storage_decrypt
    fuzz_capi_decode_session_init
    fuzz_capi_dm_queue_decrypt
    fuzz_capi_stream_decrypt
    fuzz_capi_stream_decrypt_at
    fuzz_capi_stream_encrypt_at
)

ALL_TARGETS=("${CORE_TARGETS[@]}" "${CAPI_TARGETS[@]}")

# Create timestamped log directory + "latest" symlink for fuzz_stats.sh.
# Must be absolute — parallel commands cd into different dirs before tee.
LOG_DIR="$(pwd)/${LOG_BASE}/$(date +%Y-%m-%d_%H%M%S)"
mkdir -p "${LOG_DIR}"
ln -sfn "$(basename "${LOG_DIR}")" "${LOG_BASE}/latest"

THREADS=$(( ${#ALL_TARGETS[@]} * WORKERS ))
echo "Fuzzing for ${HOURS}h (${SECONDS_TOTAL}s), ${WORKERS} worker(s) per target"
echo "  Core targets (${#CORE_TARGETS[@]}): ${CORE_TARGETS[*]}"
echo "  CAPI targets (${#CAPI_TARGETS[@]}): ${CAPI_TARGETS[*]}"
echo "  Total CPU threads: ${THREADS}"
echo "  Logs: ${LOG_DIR}/"
echo "---"

# Ensure seed corpus exists for both core and CAPI targets.
# gen_corpus writes to both soliton/fuzz/corpus/ and soliton_capi/fuzz/corpus/.
# Cheap and idempotent — safe to re-run.
echo "Generating seed corpus (core + CAPI)..."
(cd "${CORE_DIR}/fuzz" && cargo +nightly run --bin gen_corpus --quiet)
echo "---"

# Build a list of "fuzz_dir target" pairs for GNU parallel.
# -max_len=65536: required for targets with large minimum input (hybrid_verify needs 6541 bytes;
# libFuzzer's default max_len is 4096, which would make those targets produce zero progress).
# -print_final_stats=1: emit machine-parseable stats at end (for fuzz_stats.sh).
# Output is tee'd to per-target log files and also shown live via parallel --lb.
PAIRS=()
for t in "${CORE_TARGETS[@]}"; do
    PAIRS+=("${CORE_DIR} ${t}")
done
for t in "${CAPI_TARGETS[@]}"; do
    PAIRS+=("${CAPI_DIR} ${t}")
done

printf '%s\n' "${PAIRS[@]}" | parallel --lb --colsep ' ' --tagstring '{= s/.*fuzz_// =}' \
    "cd {1} && cargo +nightly fuzz run {2} -- -max_total_time=${SECONDS_TOTAL} -jobs=${WORKERS} -workers=${WORKERS} -max_len=65536 -print_final_stats=1 2>&1 | tee ${LOG_DIR}/{2}.log"

echo "---"
echo "Done. Logs saved to: ${LOG_DIR}/"
echo "Run ./fuzz_stats.sh for summary."
