#!/bin/bash
#
#   Makesmoke — Build smoke.flat from .mux test files.
#
#   Uses muxscript to create a fresh database, pipe in the unformatted
#   .mux commands, then export to smoke.flat via dbconvert.
#
#   On success: only smoke.flat survives.
#   On failure: artifacts collected in smoke.fail/.
#
#   Usage: cd testcases && ./tools/Makesmoke [--workspace DIR] [--output FILE] [test_name ...]
#
set -euo pipefail

SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
REPO_ROOT=$(cd "$SCRIPT_DIR/../.." && pwd)
TC=$(cd "$SCRIPT_DIR/.." && pwd)
WORKSPACE="$TC"
OUTPUT_FLAT=""
TEST_NAMES=()

while [ $# -gt 0 ]; do
    case "$1" in
        --workspace)
            WORKSPACE="$2"
            shift 2
            ;;
        --output)
            OUTPUT_FLAT="$2"
            shift 2
            ;;
        --)
            shift
            while [ $# -gt 0 ]; do
                TEST_NAMES+=("$1")
                shift
            done
            ;;
        *)
            TEST_NAMES+=("$1")
            shift
            ;;
    esac
done

mkdir -p "$WORKSPACE"
WORKSPACE=$(cd "$WORKSPACE" && pwd)
cd "$WORKSPACE"

GAMENAME=smoke
BIN="$REPO_ROOT/mux/game/bin"
DATA=./$GAMENAME.d
SQLITE_BASE=$DATA/$GAMENAME
TMP_FILES=()

# GNU `timeout` is `timeout` on Linux/BSD/Cygwin and `gtimeout` from
# Homebrew coreutils on macOS (which doesn't ship a native one).
if command -v timeout >/dev/null 2>&1; then
    TIMEOUT_CMD=timeout
elif command -v gtimeout >/dev/null 2>&1; then
    TIMEOUT_CMD=gtimeout
else
    echo "ERROR: neither 'timeout' nor 'gtimeout' found." >&2
    echo "  On macOS: brew install coreutils" >&2
    exit 1
fi
if [ -n "$OUTPUT_FLAT" ]; then
    case "$OUTPUT_FLAT" in
        /*) ;;
        *) OUTPUT_FLAT="$WORKSPACE/$OUTPUT_FLAT" ;;
    esac
else
    OUTPUT_FLAT="$WORKSPACE/smoke.flat"
fi

make_temp() {
    local tmp
    tmp=$(mktemp)
    TMP_FILES+=("$tmp")
    printf '%s\n' "$tmp"
}

# Collect what a failure left behind, then clear the runtime state.
#
# A function rather than a tail block because not every failure reaches the
# end of the script: the upload no-match gate exits early (#1516), and an
# early exit that skips this leaves smoke.fail/ holding the PREVIOUS run's
# artifacts -- which, now that a clean run no longer deletes them (#1544),
# would be silently misleading rather than merely absent.
collect_failure_artifacts() {
    echo "Collecting failure artifacts into smoke.fail/..."
    rm -rf smoke.fail
    mkdir -p smoke.fail
    for f in netmux.log smoke.conf; do
        [ -r "$f" ] && cp "$f" smoke.fail/ 2>/dev/null || true
    done
    cp "$DATA"/*.log smoke.fail/ 2>/dev/null || true
    cp "$DATA"/*.sqlite smoke.fail/ 2>/dev/null || true
    rm -f smoke.conf alias.conf compat.conf shutdown.status
    rm -rf "$DATA" logs text
    rm -rf bin
}

cleanup_temps() {
    if [ ${#TMP_FILES[@]} -gt 0 ]; then
        rm -f "${TMP_FILES[@]}"
    fi
}

TMP=$(make_temp)
SUITE_TMP=$(make_temp)
trap cleanup_temps EXIT

EXIT_CODE=0

# ---------------------------------------------------------------------------
# Preflight checks.
# ---------------------------------------------------------------------------

if [ ! -x "$BIN/muxscript" ]; then
    echo "ERROR: $BIN/muxscript not found. Run Build.sh first."
    exit 1
fi

if [ ! -x "$BIN/dbconvert" ]; then
    echo "ERROR: $BIN/dbconvert not found. Run Build.sh first."
    exit 1
fi

if [ ! -x "$SCRIPT_DIR/unformat" ]; then
    echo "Building unformat..."
    if command -v make >/dev/null 2>&1; then
        make -C "$SCRIPT_DIR"
    elif [ -x "$SCRIPT_DIR/build-msvc.sh" ]; then
        # Windows boxes that can build TinyMUX have Visual Studio but no
        # make, gcc or cc, so tools/Makefile cannot run there (#1347).
        echo "make not found; falling back to MSVC."
        "$SCRIPT_DIR/build-msvc.sh" unformat
    else
        echo "ERROR: cannot build unformat: no make, and no build-msvc.sh."
        exit 1
    fi
fi

# The build above may have failed without the shell noticing, and an unformat
# that is missing here produces an empty flatfile a hundred lines later, which
# reads like a corpus problem rather than a toolchain one.  [ -x ] resolves
# the .exe suffix transparently under Git Bash, so this covers both platforms.
if [ ! -x "$SCRIPT_DIR/unformat" ]; then
    echo "ERROR: unformat still not present after the build step."
    exit 1
fi

# ---------------------------------------------------------------------------
# Clean ALL prior state unconditionally.
# ---------------------------------------------------------------------------

# smoke.fail is deliberately NOT removed here (#1446's lesson, second
# instance).  It is the only surviving record of the previous failure, and
# the failure-collection path below already clears it before writing a new
# one -- so a new failure still replaces the old, but a *successful* run no
# longer destroys evidence on its way past.
#
# This matters most for exactly the bugs it kept eating: #1433 is an
# intermittent SEGV at roughly 0.4% per process and #1340 is a crash that
# stopped reproducing, and for both of those a backtrace and the tail of
# smoke.log are the whole game.  Observed live -- a route-2 SEGV was
# collected into smoke.fail/ and then deleted by the next (clean) run
# before it could be read.
#
rm -rf "$DATA" logs text
rm -f smoke.conf alias.conf compat.conf shutdown.status
rm -f "$OUTPUT_FLAT" netmux.log
# rm -rf, not rm -f: `ln -s` on a directory makes a real copy on some
# platforms (Git Bash on Windows), and `rm -f` cannot remove a directory.
# Left behind, the [ ! -e bin ] guard below would then skip refreshing it
# and every later run would silently test the FIRST run's binaries.
rm -rf bin

# ---------------------------------------------------------------------------
# Create fresh environment.
# ---------------------------------------------------------------------------

mkdir "$DATA"
mkdir -p logs text

# Unconditional: a stale bin/ must never survive into a later run.
rm -rf bin && ln -s "$BIN" bin
cp "$REPO_ROOT/mux/game/alias.conf" .
cp "$REPO_ROOT/mux/game/compat.conf" .

cat > smoke.conf <<'_EOF'
# smoke.conf - TinyMUX configuration file for Makesmoke.
input_database	smoke.d/smoke.db
output_database	smoke.d/smoke.db.new
crash_database	smoke.d/smoke.db.CRASH
mail_database   smoke.d/mail.db
comsys_database smoke.d/comsys.db
port 2860
mud_name SmokeMUX
command_quota_increment 10000
command_quota_max 10000
player_queue_limit 100000
include alias.conf
include compat.conf
_EOF

# ---------------------------------------------------------------------------
# Unformat .mux files.
# ---------------------------------------------------------------------------

echo "Unformatting .mux test files..."
# The manifest is the generator's own record of what it emitted, written
# straight to the workspace rather than through the upload.  Smoke checks
# the database's idea of the suite against it; see #1387, where the upload
# silently kept an older list and the runtime count agreed with it.
SMOKE_SUITE_MANIFEST="$PWD/suite.manifest"
export SMOKE_SUITE_MANIFEST
rm -f "$SMOKE_SUITE_MANIFEST"
python3 "$SCRIPT_DIR/generate_smoke_suite.py" ${TEST_NAMES[@]+"${TEST_NAMES[@]}"} > "$SUITE_TMP"
"$SCRIPT_DIR/unformat" "$TC"/*.mux "$SUITE_TMP" > "$TMP"
if [ ! -s "$TMP" ]; then
    echo "ERROR: unformat produced no output."
    exit 1
fi

# ---------------------------------------------------------------------------
# Run muxscript to create and populate the database.
#
# muxscript starts with no database (LoadGame creates a fresh one with
# #0 Limbo and #1 Wizard).  The unformatted .mux commands are piped
# to stdin.  After EOF, muxscript drains the queue (including the
# @shutdown at the end of the upload), saves, and exits.
# ---------------------------------------------------------------------------

echo "Running muxscript to create database..."

LD_LIBRARY_PATH=$BIN
export LD_LIBRARY_PATH

# Append @shutdown — the .mux files don't include it for the Makesmoke phase.
# (upload.tcl used to send @shutdown after seeing "Uploaded.")
echo "@shutdown" >> "$TMP"

if ! "$TIMEOUT_CMD" 120 "$BIN/muxscript" -g . -c smoke.conf < "$TMP" > netmux.log 2>&1; then
    echo "ERROR: muxscript failed or timed out."
    EXIT_CODE=1
fi

# A command that could not find its target during the upload (#1391, item 4).
#
# `&attr obj=...` resolves a bare name through the executor's inventory and
# the contents of its room -- there is no location-independent lookup.  So any
# file that leaves #1 outside #0 makes every later `&attr smoke=...` a silent
# no-op, and the generated suite override is the last thing in the stream.
#
# That is exactly how #1387 lost 46 of 307 test files while reporting
# "Dispatched: 261 / 261 expected": the failures are visible in this log as a
# run of "I don't see that here." and nothing was reading them.  #1386 fixed
# the file that did it and #1395 removed the coupling, but `@open` REQUIRES
# moving the opener, so this remains a natural thing for a future test to do.
#
# Checking the log catches the whole class rather than that one instance.
#
# Fatal, as of the baseline reaching zero.  It was introduced as a warning
# because 9 occurrences existed whose consequences nobody had triaged, and
# reddening a green build on untriaged evidence trains people to ignore the
# check.  Those 9 turned out to be one bug: route_fn's create() calls were
# missing their cost argument, so the zone rooms were never created and the
# @tel targets that followed resolved to nothing (#1460).  With the cause
# fixed rather than tolerated, a warning nobody has to act on is worth less
# than a gate, so this is now an error.
#
# If a legitimate case for a non-zero count ever appears, do not reintroduce a
# numeric baseline: a threshold silently accepts a NEW loss whenever an old one
# is fixed, which is how the count would drift back up unnoticed.  Fix the file
# or exclude it explicitly by name.
#
# The apostrophe is U+2019, not ASCII, so match on the ASCII tail only.
#
NOMATCH=$(grep -c "see that here" netmux.log 2>/dev/null || true)
: "${NOMATCH:=0}"
if [ "$NOMATCH" -gt 0 ]; then
    echo "ERROR: $NOMATCH upload command(s) could not find their target."
    echo "       A .mux file that moves #1 out of #0 and does not put it back"
    echo "       makes every later '&attr <obj>=' a silent no-op -- which is"
    echo "       how #1387 lost 46 of 307 test files while reporting a clean"
    echo "       'Dispatched: 261 / 261'.  The offending file is the one whose"
    echo "       output immediately precedes the first occurrence in"
    echo "       netmux.log:"
    echo
    grep -n -B6 "see that here" netmux.log 2>/dev/null | head -20 | sed 's/^/         /'
    echo
    echo "       A test that must move #1 has to put it back -- save the"
    echo "       location first (@tel me=[loc(me)] restores nothing once you"
    echo "       have already moved) rather than assuming #0."
    collect_failure_artifacts
    echo "=== Makesmoke: FAILED (artifacts in smoke.fail/) ==="
    exit 1
fi

# ---------------------------------------------------------------------------
# Export flatfile from SQLite via dbconvert.
# ---------------------------------------------------------------------------

if [ $EXIT_CODE -eq 0 ]; then
    echo "Exporting flatfile..."
    rm -f "$OUTPUT_FLAT"

    if [ ! -r "$SQLITE_BASE.sqlite" ]; then
        set -- "$DATA"/*.sqlite
        if [ "$1" = "$DATA/*.sqlite" ]; then
            echo "ERROR: No SQLite database found in $DATA."
            EXIT_CODE=1
        elif [ $# -ne 1 ]; then
            echo "ERROR: Expected one SQLite database, found $#."
            EXIT_CODE=1
        else
            SQLITE_BASE=${1%.sqlite}
        fi
    fi

    if [ $EXIT_CODE -eq 0 ]; then
        if ! "$BIN/dbconvert" -d "$SQLITE_BASE" -o "$OUTPUT_FLAT" -u 2>&1; then
            echo "ERROR: Export from database failed."
            EXIT_CODE=1
        fi
    fi
fi

# ---------------------------------------------------------------------------
# Scrub timestamps.
# ---------------------------------------------------------------------------

if [ $EXIT_CODE -eq 0 ] && [ -r "$OUTPUT_FLAT" ]; then
    "$SCRIPT_DIR/scrubflat.sed" < "$OUTPUT_FLAT" > "$TMP"
    mv "$TMP" "$OUTPUT_FLAT"
    TMP=$(make_temp)
    flat_bytes=$(wc -c < "$OUTPUT_FLAT")
    echo "smoke.flat created (${flat_bytes} bytes)."
fi

# ---------------------------------------------------------------------------
# Validate flatfile with omega.
# ---------------------------------------------------------------------------

OMEGA="$REPO_ROOT/mux/convert/omega"
if [ $EXIT_CODE -eq 0 ] && [ -r "$OUTPUT_FLAT" ] && [ -x "$OMEGA" ]; then
    echo "Validating smoke.flat with omega..."
    if ! "$OMEGA" -i t5x "$OUTPUT_FLAT" > /dev/null 2>&1; then
        echo "ERROR: omega rejected smoke.flat."
        EXIT_CODE=1
    else
        echo "  omega: valid."
    fi
elif [ $EXIT_CODE -eq 0 ] && [ -r "$OUTPUT_FLAT" ]; then
    echo "WARNING: omega not found at $OMEGA; skipping smoke.flat validation."
fi

# ---------------------------------------------------------------------------
# Success or failure.
# ---------------------------------------------------------------------------

if [ $EXIT_CODE -eq 0 ] && [ -r "$OUTPUT_FLAT" ]; then
    # Success: remove everything except smoke.flat.
    rm -f smoke.conf alias.conf compat.conf shutdown.status netmux.log
    rm -rf "$DATA" logs text
    rm -rf bin
    echo "=== Makesmoke: SUCCESS ==="
else
    collect_failure_artifacts
    echo "=== Makesmoke: FAILED (artifacts in smoke.fail/) ==="
    exit 1
fi
