#!/bin/bash
# Large-scale differential fuzzing for croniter-rs
# Uses bash-level parallelism to saturate all CPU cores
# Usage: ./fuzz.sh [duration_hours] [num_workers]

set -e

DURATION_HOURS=${1:-24}
NUM_WORKERS=${2:-$(( $(nproc) - 2 ))}
VENV="/home/ubuntu-exp/cronproject/.venv"
WORKDIR="/home/ubuntu-exp/cronproject/croniter-rs"
RESULTS_DIR="$WORKDIR/tests/differential/fuzzing_results"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
RUN_DIR="$RESULTS_DIR/run_$TIMESTAMP"

mkdir -p "$RUN_DIR"

echo "============================================"
echo "Croniter-rs Differential Fuzzing"
echo "============================================"
echo "Duration: ${DURATION_HOURS}h"
echo "Workers: $NUM_WORKERS"
echo "Results: $RUN_DIR"
echo "Start: $(date)"
echo "============================================"

# Worker script
cat > "$RUN_DIR/worker.py" << 'WORKER_EOF'
#!/usr/bin/env python3
import sys
import os
import time
import json
import random
import hashlib
from datetime import datetime

# Add test directory to path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))

import croniter as py_croniter
import croniter_rs as rs_croniter

FIELDS_MIN = [0, 0, 1, 1, 0, 0, 1970]
FIELDS_MAX = [59, 23, 31, 12, 6, 59, 2099]

OPERATORS = ['*', 'value', 'range', 'step', 'list', 'range_step']
SPECIAL_DAY = ['L', '*', 'value', 'range', 'step']
SPECIAL_DOW = ['value', 'range', 'step', 'list', 'nth', 'last']

def rand_field(idx):
    op = random.choice(OPERATORS)
    lo, hi = FIELDS_MIN[idx], FIELDS_MAX[idx]
    if op == '*':
        return '*'
    elif op == 'value':
        return str(random.randint(lo, hi))
    elif op == 'range':
        a, b = sorted(random.sample(range(lo, hi+1), 2))
        return f'{a}-{b}'
    elif op == 'step':
        s = random.randint(1, max(1, (hi-lo)//2))
        return f'*/{s}'
    elif op == 'list':
        n = random.randint(2, min(5, hi-lo+1))
        vals = sorted(random.sample(range(lo, hi+1), n))
        return ','.join(str(v) for v in vals)
    elif op == 'range_step':
        a, b = sorted(random.sample(range(lo, hi+1), 2))
        s = random.randint(1, max(1, (b-a)//2))
        return f'{a}-{b}/{s}'
    return '*'

def rand_day():
    op = random.choice(SPECIAL_DAY)
    if op == 'L':
        return 'L'
    return rand_field(2)

def rand_dow():
    op = random.choice(SPECIAL_DOW)
    if op == 'nth':
        dow = random.randint(0, 6)
        nth = random.randint(1, 5)
        return f'{dow}#{nth}'
    elif op == 'last':
        dow = random.randint(0, 6)
        return f'l{dow}'
    return rand_field(4)

def rand_expr():
    nfields = random.choice([5, 5, 5, 6, 7])  # bias toward 5-field
    if nfields == 5:
        return f'{rand_field(0)} {rand_field(1)} {rand_day()} {rand_field(3)} {rand_dow()}'
    elif nfields == 6:
        return f'{rand_field(0)} {rand_field(1)} {rand_day()} {rand_field(3)} {rand_dow()} {rand_field(5)}'
    else:
        return f'{rand_field(0)} {rand_field(1)} {rand_day()} {rand_field(3)} {rand_dow()} {rand_field(5)} {rand_field(6)}'

def rand_start():
    y = random.randint(2000, 2030)
    m = random.randint(1, 12)
    d = random.randint(1, 28)
    h = random.randint(0, 23)
    mi = random.randint(0, 59)
    return datetime(y, m, d, h, mi)

def compare(expr, start, iterations=5):
    """Compare Python and Rust implementations. Returns (passed, detail)."""
    # Init
    py_err, rs_err = None, None
    py_cron, rs_cron = None, None

    try:
        py_cron = py_croniter.croniter(expr, start)
    except Exception as e:
        py_err = type(e).__name__

    try:
        rs_cron = rs_croniter.croniter(expr, start)
    except Exception as e:
        rs_err = type(e).__name__

    # Both error
    if py_err and rs_err:
        return True, None

    # One errors, other doesn't
    if py_err or rs_err:
        return False, f'init: py={py_err or "ok"} rs={rs_err or "ok"}'

    # Compare get_next
    for i in range(iterations):
        try:
            py_val = py_cron.get_next(float)
        except Exception:
            py_val = 'ERR'
        try:
            rs_val = rs_cron.get_next(float)
        except Exception:
            rs_val = 'ERR'

        if py_val == 'ERR' and rs_val == 'ERR':
            continue
        if py_val == 'ERR' or rs_val == 'ERR':
            return False, f'next[{i}]: py={py_val} rs={rs_val}'
        if isinstance(py_val, float) and isinstance(rs_val, float):
            if abs(py_val - rs_val) > 0.001:
                return False, f'next[{i}]: py={py_val} rs={rs_val}'

    return True, None

def main():
    worker_id = int(sys.argv[1])
    duration_secs = int(sys.argv[2])
    output_file = sys.argv[3]

    random.seed(worker_id * 1000 + int(time.time()) % 10000)

    start_time = time.time()
    end_time = start_time + duration_secs

    passed = 0
    failed = 0
    errors = 0
    failures = []
    total = 0

    while time.time() < end_time:
        expr = rand_expr()
        start = rand_start()
        total += 1

        try:
            ok, detail = compare(expr, start)
            if ok:
                passed += 1
            else:
                failed += 1
                if len(failures) < 1000:
                    failures.append({'expr': expr, 'start': str(start), 'detail': detail})
        except Exception as e:
            errors += 1

        if total % 5000 == 0:
            elapsed = time.time() - start_time
            rate = total / elapsed
            remaining = end_time - time.time()
            print(f'W{worker_id}: {total} tests ({rate:.0f}/s) P={passed} F={failed} E={errors} rem={remaining/3600:.1f}h', flush=True)

    result = {
        'worker_id': worker_id,
        'total': total,
        'passed': passed,
        'failed': failed,
        'errors': errors,
        'duration_secs': time.time() - start_time,
        'rate': total / (time.time() - start_time),
        'failures': failures[:100]
    }

    with open(output_file, 'w') as f:
        json.dump(result, f, indent=2)

    print(f'W{worker_id} DONE: {total} tests, {passed} passed, {failed} failed, {errors} errors', flush=True)

if __name__ == '__main__':
    main()
WORKER_EOF

# Launch workers
DURATION_SECS=$(( DURATION_HOURS * 3600 ))
PIDS=()

echo "Launching $NUM_WORKERS workers..."
for i in $(seq 0 $(( NUM_WORKERS - 1 ))); do
    LOG="$RUN_DIR/worker_${i}.log"
    OUT="$RUN_DIR/worker_${i}_result.json"
    source "$VENV/bin/activate" && python3 "$RUN_DIR/worker.py" "$i" "$DURATION_SECS" "$OUT" > "$LOG" 2>&1 &
    PIDS+=($!)
    echo "  Worker $i: PID $!"
done

echo ""
echo "All workers launched. Monitoring..."
echo "Logs: $RUN_DIR/worker_*.log"
echo "To monitor: tail -f $RUN_DIR/worker_0.log"
echo "To stop: kill ${PIDS[*]}"
echo ""

# Save PIDs for monitoring
echo "${PIDS[*]}" > "$RUN_DIR/pids.txt"

# Monitor loop
while true; do
    sleep 300  # Check every 5 minutes
    ALIVE=0
    for pid in "${PIDS[@]}"; do
        if kill -0 "$pid" 2>/dev/null; then
            ALIVE=$((ALIVE + 1))
        fi
    done

    if [ "$ALIVE" -eq 0 ]; then
        echo "All workers completed."
        break
    fi

    echo "[$(date)] $ALIVE/$NUM_WORKERS workers alive"
done

# Aggregate results
echo ""
echo "============================================"
echo "Aggregating results..."
echo "============================================"

python3 - "$RUN_DIR" << 'AGG_EOF'
import sys, json, glob, os

run_dir = sys.argv[1]
total = passed = failed = errors = 0
all_failures = []
rates = []

for f in sorted(glob.glob(os.path.join(run_dir, 'worker_*_result.json'))):
    try:
        with open(f) as fh:
            r = json.load(fh)
        total += r['total']
        passed += r['passed']
        failed += r['failed']
        errors += r['errors']
        rates.append(r['rate'])
        all_failures.extend(r.get('failures', []))
    except Exception as e:
        print(f'Error reading {f}: {e}')

print(f'Total tests: {total:,}')
print(f'Passed: {passed:,} ({100*passed/max(total,1):.4f}%)')
print(f'Failed: {failed:,} ({100*failed/max(total,1):.4f}%)')
print(f'Errors: {errors:,}')
print(f'Avg rate: {sum(rates)/max(len(rates),1):.0f} tests/sec/worker')
print(f'Total rate: {sum(rates):.0f} tests/sec')

summary = {
    'total': total, 'passed': passed, 'failed': failed, 'errors': errors,
    'avg_rate': sum(rates)/max(len(rates),1),
    'total_rate': sum(rates),
    'num_workers': len(rates),
    'failures_sample': all_failures[:200]
}

with open(os.path.join(run_dir, 'summary.json'), 'w') as f:
    json.dump(summary, f, indent=2)

if all_failures:
    print(f'\nFirst 10 failures:')
    for fail in all_failures[:10]:
        print(f'  {fail["expr"]} @ {fail["start"]}: {fail["detail"]}')

print(f'\nFull summary: {os.path.join(run_dir, "summary.json")}')
AGG_EOF

echo ""
echo "Fuzzing complete at $(date)"
