#!/usr/bin/env bash
set -Eeuo pipefail

MODE="${1:-}"
ROOT="${2:-}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
FIXTURE_DIR="$SCRIPT_DIR/fixtures/lot2"
MARKER='.soria-ir-detection-engineering'
DECISION='DETECTION_ENGINEERING_READY_WITH_ACTIONS'

fail(){ printf 'ERROR: %s\n' "$*" >&2; exit 1; }
need(){ command -v "$1" >/dev/null 2>&1 || fail "missing command: $1"; }

safe_root(){
  [[ -n "$ROOT" ]] || return 1
  case "$ROOT" in
    /tmp/soria-ir-*|"$HOME"/soria-ir-*) return 0 ;;
    *) return 1 ;;
  esac
}
require_safe_root(){ safe_root || fail "unsafe lab root: ${ROOT:-<empty>}"; }
require_marker(){ [[ -f "$ROOT/$MARKER" ]] || fail "missing marker: $ROOT/$MARKER"; }
fixture_count(){ find "$1" -type f | wc -l | tr -d ' '; }

build_manifest(){
  local tree="$1" out="$2"
  (
    cd "$tree"
    find . -type f -print | LC_ALL=C sort | while IFS= read -r file; do sha256sum "$file"; done
  ) > "$out"
}
verify_source_integrity(){
  [[ -f "$ROOT/.source-manifest.sha256" ]] || return 1
  ( cd "$ROOT/source" && sha256sum -c "$ROOT/.source-manifest.sha256" >/dev/null 2>&1 )
}

known_asset(){ grep -q "^${1}," "$ROOT/source/context/assets.csv"; }
known_identity(){ grep -q "^${1}," "$ROOT/source/context/identities.csv"; }
known_hypothesis(){ grep -q "^${1}," "$ROOT/source/hypotheses/hypotheses.csv"; }
valid_timestamp(){ [[ "$1" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$ ]]; }

capability_status(){
  local source_type="$1"
  awk -F, -v t="$source_type" 'NR>1 && $2==t {print $3; found=1; exit} END{if(!found) exit 1}' "$ROOT/source/coverage/telemetry-capabilities.csv"
}
hypothesis_testable(){
  local hyp="$1" telemetry
  telemetry="$(awk -F, -v h="$hyp" 'NR>1 && $1==h {print $3; found=1; exit} END{if(!found) exit 1}' "$ROOT/source/hypotheses/hypotheses.csv")" || return 1
  [[ "$(capability_status "$telemetry" 2>/dev/null || true)" == AVAILABLE ]]
}
allowed_rule_source(){ case "$1" in windows-auth|linux-auth|network-suricata) return 0 ;; *) return 1 ;; esac; }
allowed_severity(){ case "$1" in low|medium|high|critical) return 0 ;; *) return 1 ;; esac; }
allowed_status(){ case "$1" in experimental|test|stable) return 0 ;; *) return 1 ;; esac; }

check_rule_line(){
  local line="$1"
  local rule_id version hyp source_type event_kind outcome selector_field selector_value group_by threshold window severity status synthetic
  IFS=, read -r rule_id version hyp source_type event_kind outcome selector_field selector_value group_by threshold window severity status synthetic <<< "$line"
  [[ -n "$rule_id" && "$version" =~ ^[1-9][0-9]*$ && -n "$hyp" && -n "$source_type" && -n "$event_kind" && -n "$outcome" && -n "$selector_field" && -n "$selector_value" && -n "$group_by" && "$threshold" =~ ^[1-9][0-9]*$ && "$window" =~ ^[1-9][0-9]*$ && -n "$severity" && -n "$status" ]] || return 1
  known_hypothesis "$hyp" || return 1
  hypothesis_testable "$hyp" || return 1
  allowed_rule_source "$source_type" || return 1
  allowed_severity "$severity" || return 1
  allowed_status "$status" || return 1
  [[ "$synthetic" == SYNTHETIC ]] || return 1
  case "$source_type:$selector_field:$group_by" in
    windows-auth:identity_id:identity_id|linux-auth:identity_id:identity_id) ;;
    network-suricata:signature_id:signature_id|network-suricata:signature_id:src_ip) ;;
    *) return 1 ;;
  esac
}

check_host_line(){
  local line="$1"
  local event_id timestamp source_type asset_id identity_id event_kind outcome remote_ip detail synthetic
  IFS=, read -r event_id timestamp source_type asset_id identity_id event_kind outcome remote_ip detail synthetic <<< "$line"
  [[ -n "$event_id" && -n "$timestamp" && -n "$source_type" && -n "$asset_id" && -n "$identity_id" && -n "$event_kind" && -n "$outcome" && -n "$remote_ip" && -n "$detail" ]] || return 1
  valid_timestamp "$timestamp" || return 1
  case "$source_type" in windows-auth|linux-auth) ;; *) return 1 ;; esac
  known_asset "$asset_id" || return 1
  known_identity "$identity_id" || return 1
  [[ "$synthetic" == SYNTHETIC ]]
}
known_flow(){ grep -q "^${1}," "$ROOT/source/network/flow-context.csv"; }
check_network_line(){
  local line="$1"
  local event_id timestamp src_ip dest_ip dest_port proto signature_id signature category severity flow_id event_kind outcome synthetic
  IFS=, read -r event_id timestamp src_ip dest_ip dest_port proto signature_id signature category severity flow_id event_kind outcome synthetic <<< "$line"
  [[ -n "$event_id" && -n "$timestamp" && -n "$src_ip" && -n "$dest_ip" && "$dest_port" =~ ^[0-9]+$ && -n "$proto" && "$signature_id" =~ ^[0-9]+$ && -n "$signature" && -n "$category" && "$severity" =~ ^[1-9][0-9]*$ && "$flow_id" =~ ^[0-9]+$ && -n "$event_kind" && -n "$outcome" ]] || return 1
  valid_timestamp "$timestamp" || return 1
  known_flow "$flow_id" || return 1
  [[ "$synthetic" == SYNTHETIC ]]
}
check_tuning_line(){
  local line="$1"
  local entry rule field value reason owner expires synthetic
  IFS=, read -r entry rule field value reason owner expires synthetic <<< "$line"
  [[ -n "$entry" && -n "$rule" && -n "$field" && -n "$value" && -n "$reason" && -n "$owner" && -n "$expires" ]] || return 1
  grep -q "^${rule}," "$ROOT/source/rules/rules.csv" || return 1
  awk -F, -v f="$field" 'NR>1 && $2==f && $3=="exact" && $4=="yes" {ok=1} END{exit(ok?0:1)}' "$ROOT/source/tuning/tuning-policy.csv" || return 1
  [[ "$synthetic" == SYNTHETIC ]]
}

setup_lab(){
  require_safe_root
  [[ ! -e "$ROOT" ]] || fail "root already exists: $ROOT"
  [[ -d "$FIXTURE_DIR" ]] || fail "missing fixture directory: $FIXTURE_DIR"
  [[ "$(fixture_count "$FIXTURE_DIR")" -eq 14 ]] || fail 'reference bundle must contain exactly 14 fixtures'
  mkdir -p "$ROOT/source" "$ROOT/output" "$ROOT/work"
  printf 'SORIA_IR_DETECTION_ENGINEERING\n' > "$ROOT/$MARKER"
  cp -R "$FIXTURE_DIR"/. "$ROOT/source/"
  [[ "$(fixture_count "$ROOT/source")" -eq 14 ]] || fail 'copied fixture count mismatch'
  build_manifest "$ROOT/source" "$ROOT/.source-manifest.sha256"
  verify_source_integrity || fail 'source integrity failed after setup'
  printf 'Setup OK: 14 synthetic detection-engineering fixtures copied.\n'
}
status_lab(){
  require_safe_root; require_marker
  local integrity outputs decision
  if verify_source_integrity; then integrity=OK; else integrity=FAILED; fi
  if [[ -s "$ROOT/output/raw-alerts.csv" ]]; then outputs=PRESENT; else outputs=ABSENT; fi
  if [[ -s "$ROOT/output/decision.txt" ]]; then decision="$(cat "$ROOT/output/decision.txt")"; else decision=NOT_RUN; fi
  printf 'marker=OK\nfixtures=%s\nintegrity=%s\noutputs=%s\ndecision=%s\n' "$(fixture_count "$ROOT/source")" "$integrity" "$outputs" "$decision"
}

write_hypothesis_coverage(){
  local out="$ROOT/output/hypothesis-coverage.csv"
  printf 'hypothesis_id,required_telemetry,declared_status,computed_status,reason,synthetic\n' > "$out"
  tail -n +2 "$ROOT/source/hypotheses/hypotheses.csv" | while IFS=, read -r hyp behavior telemetry asset signal declared synthetic; do
    local cap computed reason
    cap="$(capability_status "$telemetry" 2>/dev/null || true)"
    if [[ "$cap" == AVAILABLE ]]; then computed=TESTABLE; reason=telemetry-available; else computed=NOT_TESTABLE; reason=telemetry-gap; fi
    printf '%s,%s,%s,%s,%s,%s\n' "$hyp" "$telemetry" "$declared" "$computed" "$reason" "$synthetic" >> "$out"
  done
}
write_rule_quality(){
  local out="$ROOT/output/rule-quality.csv" dup
  printf 'rule_id,version,result,reason,synthetic\n' > "$out"
  dup="$(tail -n +2 "$ROOT/source/rules/rules.csv" | awk -F, '{k=$1 FS $2; c[k]++} END{for(k in c) if(c[k]>1) print k}')"
  [[ -z "$dup" ]] || fail "duplicate rule/version in positive corpus: $dup"
  tail -n +2 "$ROOT/source/rules/rules.csv" | while IFS= read -r line; do
    local rule version rest
    rule="${line%%,*}"; rest="${line#*,}"; version="${rest%%,*}"
    if check_rule_line "$line"; then printf '%s,%s,PASS,rule-contract-valid,SYNTHETIC\n' "$rule" "$version" >> "$out"; else printf '%s,%s,FAIL,rule-contract-invalid,SYNTHETIC\n' "$rule" "$version" >> "$out"; fi
  done
  if grep -q ',FAIL,' "$out"; then fail 'positive rule corpus contains invalid rule'; fi
}

evaluate_host_rule(){
  local rule_line="$1" file="$2" out="$3"
  local rule version hyp source_type event_kind outcome selector_field selector_value group_by threshold window severity status synthetic
  IFS=, read -r rule version hyp source_type event_kind outcome selector_field selector_value group_by threshold window severity status synthetic <<< "$rule_line"
  awk -F, -v r="$rule" -v v="$version" -v st="$source_type" -v ek="$event_kind" -v oc="$outcome" -v thr="$threshold" -v win="$window" -v sev="$severity" '
    function sec(t){return (substr(t,12,2)+0)*3600+(substr(t,15,2)+0)*60+(substr(t,18,2)+0)}
    NR>1 && $3==st && $6==ek && $7==oc {g=$5; now=sec($2); n[g]++; times[g,n[g]]=now; refs[g,n[g]]=$1; start=1; while(start<=n[g] && now-times[g,start]>win) start++; cnt=n[g]-start+1; if(cnt>=thr && !alerted[g]){ref=""; for(i=start;i<=n[g];i++) ref=ref (ref?"|":"") refs[g,i]; printf "ALERT-%s-%s,%s,%s,%s,%s,%s,%s,%s,threshold-match,SYNTHETIC\n",r,g,r,v,$2,g,ref,sev,st; alerted[g]=1}}' "$file" >> "$out"
}
evaluate_network_rule(){
  local rule_line="$1" file="$2" out="$3"
  local rule version hyp source_type event_kind outcome selector_field selector_value group_by threshold window severity status synthetic group_col
  IFS=, read -r rule version hyp source_type event_kind outcome selector_field selector_value group_by threshold window severity status synthetic <<< "$rule_line"
  case "$group_by" in signature_id) group_col=7 ;; src_ip) group_col=3 ;; *) fail "unsupported network group: $group_by" ;; esac
  awk -F, -v r="$rule" -v v="$version" -v ek="$event_kind" -v oc="$outcome" -v sel="$selector_value" -v gc="$group_col" -v thr="$threshold" -v win="$window" -v sev="$severity" '
    function sec(t){return (substr(t,12,2)+0)*3600+(substr(t,15,2)+0)*60+(substr(t,18,2)+0)}
    NR>1 && $12==ek && $13==oc && $7==sel {g=$gc; now=sec($2); n[g]++; times[g,n[g]]=now; refs[g,n[g]]=$1; start=1; while(start<=n[g] && now-times[g,start]>win) start++; cnt=n[g]-start+1; if(cnt>=thr && !alerted[g]){ref=""; for(i=start;i<=n[g];i++) ref=ref (ref?"|":"") refs[g,i]; printf "ALERT-%s-%s,%s,%s,%s,%s,%s,%s,network-suricata,signature=%s;flow-context-preserved,SYNTHETIC\n",r,g,r,v,$2,g,ref,sev,sel; alerted[g]=1}}' "$file" >> "$out"
}
write_detections(){
  local host="$ROOT/output/host-alerts.csv" net="$ROOT/output/network-alerts.csv" raw="$ROOT/output/raw-alerts.csv" header='alert_id,rule_id,version,timestamp,group_value,event_refs,severity,source_type,context,synthetic'
  printf '%s\n' "$header" > "$host"; printf '%s\n' "$header" > "$net"
  while IFS= read -r line; do
    [[ "$line" == rule_id,* ]] && continue
    local source; source="$(printf '%s\n' "$line" | cut -d, -f4)"
    case "$source" in windows-auth) evaluate_host_rule "$line" "$ROOT/source/host/windows-auth.csv" "$host" ;; linux-auth) evaluate_host_rule "$line" "$ROOT/source/host/linux-auth.csv" "$host" ;; network-suricata) evaluate_network_rule "$line" "$ROOT/source/network/suricata-eve-alerts.csv" "$net" ;; *) fail "unsupported rule source: $source" ;; esac
  done < "$ROOT/source/rules/rules.csv"
  printf '%s\n' "$header" > "$raw"; tail -n +2 "$host" >> "$raw"; tail -n +2 "$net" >> "$raw"
}

write_false_positive_review(){
  local raw="$ROOT/output/raw-alerts.csv" review="$ROOT/output/false-positive-review.csv" tuned="$ROOT/output/tuned-alerts.csv"
  printf 'alert_id,rule_id,group_value,severity,disposition,reason,owner,synthetic\n' > "$review"; head -n 1 "$raw" > "$tuned"
  tail -n +2 "$raw" | while IFS=, read -r alert rule version ts group refs severity source context synthetic; do
    local group_by kb disposition reason owner
    group_by="$(awk -F, -v r="$rule" 'NR>1 && $1==r {print $9; exit}' "$ROOT/source/rules/rules.csv")"
    kb="$(awk -F, -v r="$rule" -v f="$group_by" -v v="$group" 'NR>1 && $2==r && $3==f && $4==v {print $5 "," $6; exit}' "$ROOT/source/tuning/known-benign.csv")"
    if [[ -n "$kb" ]]; then reason="${kb%%,*}"; owner="${kb#*,}"; disposition=KNOWN_BENIGN; elif [[ "$severity" == high || "$severity" == critical ]]; then disposition=KEEP; reason=high-priority-review; owner=secops; else disposition=NEEDS_CONTEXT; reason=analyst-context-required; owner=secops; fi
    printf '%s,%s,%s,%s,%s,%s,%s,%s\n' "$alert" "$rule" "$group" "$severity" "$disposition" "$reason" "$owner" "$synthetic" >> "$review"
    if [[ "$disposition" != KNOWN_BENIGN ]]; then printf '%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n' "$alert" "$rule" "$version" "$ts" "$group" "$refs" "$severity" "$source" "$context" "$synthetic" >> "$tuned"; fi
  done
}
write_version_report(){
  local out="$ROOT/output/version-report.csv"
  printf 'rule_id,current_version,max_history_version,result,change_reason,synthetic\n' > "$out"
  tail -n +2 "$ROOT/source/rules/rules.csv" | while IFS=, read -r line; do
    local rule current hist max reason dup
    rule="$(printf '%s\n' "$line" | cut -d, -f1)"; current="$(printf '%s\n' "$line" | cut -d, -f2)"; hist="$ROOT/source/versions/rule-history.csv"
    max="$(awk -F, -v r="$rule" 'NR>1 && $1==r {if($2+0>m)m=$2+0; found=1} END{if(found) print m; else exit 1}' "$hist")" || fail "missing history for $rule"
    reason="$(awk -F, -v r="$rule" -v v="$current" 'NR>1 && $1==r && $2==v {print $4; found=1; exit} END{if(!found) exit 1}' "$hist")" || fail "missing current history for $rule"
    dup="$(awk -F, -v r="$rule" 'NR>1 && $1==r {c[$2]++} END{for(v in c) if(c[v]>1) print v}' "$hist")"
    [[ -z "$dup" && "$max" == "$current" && -n "$reason" ]] || fail "invalid version history for $rule"
    printf '%s,%s,%s,PASS,%s,SYNTHETIC\n' "$rule" "$current" "$max" "$reason" >> "$out"
  done
}
write_rule_tests(){
  local out="$ROOT/output/test-results.csv" raw="$ROOT/output/raw-alerts.csv"
  printf 'test_id,rule_id,fixture_case,expectation,result,synthetic\n' > "$out"
  tail -n +2 "$ROOT/source/rules/rule-tests.csv" | while IFS=, read -r test rule case_value expectation synthetic; do
    local exists=no result
    if awk -F, -v r="$rule" -v g="$case_value" 'NR>1 && $2==r && $5==g {found=1} END{exit(found?0:1)}' "$raw"; then exists=yes; fi
    if [[ "$expectation" == ALERT && "$exists" == yes ]] || [[ "$expectation" == NO_ALERT && "$exists" == no ]]; then result=PASS; else result=FAIL; fi
    printf '%s,%s,%s,%s,%s,%s\n' "$test" "$rule" "$case_value" "$expectation" "$result" "$synthetic" >> "$out"
  done
}
record_negative(){ printf '%s,%s,SYNTHETIC\n' "$1" "$2" >> "$ROOT/output/negative-results.csv"; }
run_negative_tests(){
  local out="$ROOT/output/negative-results.csv" neg="$ROOT/work/negative" line
  rm -rf "$neg"; mkdir -p "$neg"; printf 'family,status,synthetic\n' > "$out"
  line='NEG-R1,1,HYP-NOPE,windows-auth,auth_failure,FAILURE,identity_id,*,identity_id,3,300,high,test,SYNTHETIC'; if check_rule_line "$line"; then fail 'unknown hypothesis negative passed'; else record_negative unknown-hypothesis BLOCKED; fi
  line='NEG-R2,1,HYP-PROC,windows-auth,auth_failure,FAILURE,identity_id,*,identity_id,3,300,high,test,SYNTHETIC'; if check_rule_line "$line"; then fail 'not-testable hypothesis negative passed'; else record_negative not-testable-hypothesis BLOCKED; fi
  line='NEG-R3,1,HYP-AUTH-WIN,windows-auth,auth_failure,FAILURE,identity_id,*,identity_id,0,300,high,test,SYNTHETIC'; if check_rule_line "$line"; then fail 'zero threshold negative passed'; else record_negative zero-threshold BLOCKED; fi
  line='NEG-R4,1,HYP-AUTH-WIN,windows-auth,auth_failure,FAILURE,identity_id,*,identity_id,3,0,high,test,SYNTHETIC'; if check_rule_line "$line"; then fail 'zero window negative passed'; else record_negative invalid-window BLOCKED; fi
  line='NEG-R5,1,HYP-AUTH-WIN,windows-auth,auth_failure,FAILURE,identity_id,*,identity_id,3,300,urgent,test,SYNTHETIC'; if check_rule_line "$line"; then fail 'invalid severity negative passed'; else record_negative invalid-severity BLOCKED; fi
  printf '%s\n%s\n' 'DUP-1,1,HYP-AUTH-WIN,windows-auth,auth_failure,FAILURE,identity_id,*,identity_id,3,300,high,test,SYNTHETIC' 'DUP-1,1,HYP-AUTH-WIN,windows-auth,auth_failure,FAILURE,identity_id,*,identity_id,3,300,high,test,SYNTHETIC' > "$neg/duplicate-rules.csv"
  if awk -F, '{k=$1 FS $2; c[k]++} END{bad=0; for(k in c) if(c[k]>1) bad=1; exit(bad?0:1)}' "$neg/duplicate-rules.csv"; then record_negative duplicate-rule-version BLOCKED; else fail 'duplicate rule/version not detected'; fi
  line='NEG-H1,2026/08/08-10:00:00,windows-auth,AST-WIN-01,ID-ADMIN,auth_failure,FAILURE,198.51.100.10,bad-time,SYNTHETIC'; if check_host_line "$line"; then fail 'malformed host timestamp negative passed'; else record_negative malformed-host-timestamp BLOCKED; fi
  line='NEG-H2,2026-08-08T10:00:00Z,windows-auth,AST-NOPE,ID-NOPE,auth_failure,FAILURE,198.51.100.10,unknown-context,SYNTHETIC'; if check_host_line "$line"; then fail 'unknown host context negative passed'; else record_negative unknown-host-asset-identity BLOCKED; fi
  line='NEG-N1,2026-08-08T12:00:00Z,198.51.100.50,192.0.2.20,443,TCP,,SYNTHETIC-Test,Category,1,,suricata_alert,ALERT,SYNTHETIC'; if check_network_line "$line"; then fail 'missing network signature/flow negative passed'; else record_negative network-missing-signature-flow BLOCKED; fi
  line='NEG-T1,DET-AUTH-001,rule_id,DET-AUTH-001,broad-rule-suppression,,2026-12-31,SYNTHETIC'; if check_tuning_line "$line"; then fail 'broad tuning negative passed'; else record_negative broad-unowned-tuning BLOCKED; fi
}
write_proof_boundaries(){
  cat > "$ROOT/output/proof-boundaries.md" <<'BOUNDARY'
# SYNTHETIC Lot 2 proof boundaries

This lab proves deterministic detection-engineering behavior over local synthetic fixtures only.

It does not prove live Wazuh rule loading, Sigma compilation or backend conversion, Suricata packet inspection or signature effectiveness, complete live authentication logging, real precision/recall or false-positive rate, ATT&CK coverage, production tuning safety, or the existence/severity of a real incident.

Raw alerts are preserved even when a bounded synthetic benign context is excluded from the tuned view. The source SHA-256 manifest remains the integrity reference for this exercise.
BOUNDARY
}
validate_expectations(){
  local review="$ROOT/output/false-positive-review.csv"
  tail -n +2 "$ROOT/source/expectations/expected-alerts.csv" | while IFS=, read -r exp rule group disposition synthetic; do
    awk -F, -v r="$rule" -v g="$group" -v d="$disposition" 'NR>1 && $2==r && $3==g && $5==d {ok=1} END{exit(ok?0:1)}' "$review" || fail "expected alert disposition missing: $exp"
  done
}

run_lab(){
  require_safe_root; require_marker
  need awk; need sort; need sha256sum; need grep; need sed; need find; need cut; need cmp
  [[ "$(fixture_count "$ROOT/source")" -eq 14 ]] || fail 'source fixture count must remain 14'
  verify_source_integrity || fail 'source integrity mismatch before run'
  rm -rf "$ROOT/output" "$ROOT/work"; mkdir -p "$ROOT/output" "$ROOT/work"; cp "$ROOT/.source-manifest.sha256" "$ROOT/output/source-integrity.sha256"
  while IFS= read -r f; do tail -n +2 "$f" | while IFS= read -r line; do check_host_line "$line" || fail "invalid positive host event in $f"; done; done < <(printf '%s\n' "$ROOT/source/host/windows-auth.csv" "$ROOT/source/host/linux-auth.csv")
  tail -n +2 "$ROOT/source/network/suricata-eve-alerts.csv" | while IFS= read -r line; do check_network_line "$line" || fail 'invalid positive network event'; done
  tail -n +2 "$ROOT/source/tuning/known-benign.csv" | while IFS= read -r line; do check_tuning_line "$line" || fail 'invalid positive tuning entry'; done
  write_hypothesis_coverage; write_rule_quality; write_detections; write_false_positive_review; write_version_report; write_rule_tests; run_negative_tests; write_proof_boundaries; validate_expectations
  printf '%s\n' "$DECISION" > "$ROOT/output/decision.txt"
  printf 'Run OK: hypothesis coverage, four rules, detections, tuning and version evidence generated.\n'
}
validate_lab(){
  require_safe_root; require_marker
  [[ "$(fixture_count "$ROOT/source")" -eq 14 ]] || fail 'expected exactly 14 source fixtures'
  verify_source_integrity || fail 'source integrity mismatch during validate'
  local required=(hypothesis-coverage.csv rule-quality.csv host-alerts.csv network-alerts.csv raw-alerts.csv false-positive-review.csv tuned-alerts.csv version-report.csv test-results.csv negative-results.csv source-integrity.sha256 proof-boundaries.md decision.txt) f
  for f in "${required[@]}"; do [[ -s "$ROOT/output/$f" ]] || fail "missing output: $f"; done
  [[ $(( $(wc -l < "$ROOT/output/hypothesis-coverage.csv") - 1 )) -eq 5 ]] || fail 'expected five hypotheses'
  [[ "$(grep -c ',TESTABLE,telemetry-available,SYNTHETIC$' "$ROOT/output/hypothesis-coverage.csv")" -eq 4 ]] || fail 'expected four testable hypotheses'
  grep -Fq 'HYP-PROC,endpoint-edr,NOT_TESTABLE,NOT_TESTABLE,telemetry-gap,SYNTHETIC' "$ROOT/output/hypothesis-coverage.csv" || fail 'endpoint-edr gap missing'
  [[ $(( $(wc -l < "$ROOT/output/rule-quality.csv") - 1 )) -eq 4 ]] || fail 'expected four executable rules'
  [[ "$(grep -c ',PASS,rule-contract-valid,SYNTHETIC$' "$ROOT/output/rule-quality.csv")" -eq 4 ]] || fail 'all rules must pass quality'
  grep -Fq ',DET-AUTH-001,2,' "$ROOT/output/host-alerts.csv" || fail 'missing Windows authentication detection'; grep -Fq ',ID-ADMIN,' "$ROOT/output/host-alerts.csv" || fail 'missing ID-ADMIN alert'
  grep -Fq ',DET-AUTH-002,1,' "$ROOT/output/host-alerts.csv" || fail 'missing Linux authentication detection'; grep -Fq ',ID-SVC-WEB,' "$ROOT/output/host-alerts.csv" || fail 'missing service-account alert'
  grep -Fq ',DET-NET-001,1,' "$ROOT/output/network-alerts.csv" || fail 'missing high-priority network detection'; grep -Fq ',900001,' "$ROOT/output/network-alerts.csv" || fail 'missing signature 900001 alert'
  grep -Fq ',DET-NET-002,2,' "$ROOT/output/network-alerts.csv" || fail 'missing scan network detection'; grep -Fq ',198.51.100.45,' "$ROOT/output/network-alerts.csv" || fail 'missing repeated scan group'
  grep -Fq ',DET-AUTH-001,ID-MAINT,high,KNOWN_BENIGN,' "$ROOT/output/false-positive-review.csv" || fail 'bounded benign candidate missing'
  local raw_rows tuned_rows; raw_rows=$(( $(wc -l < "$ROOT/output/raw-alerts.csv") - 1 )); tuned_rows=$(( $(wc -l < "$ROOT/output/tuned-alerts.csv") - 1 )); (( raw_rows == 5 && tuned_rows == 4 )) || fail "unexpected raw/tuned alert counts: $raw_rows/$tuned_rows"
  ! grep -Fq ',ID-MAINT,' "$ROOT/output/tuned-alerts.csv" || fail 'known benign candidate not removed from tuned view'; grep -Fq ',ID-MAINT,' "$ROOT/output/raw-alerts.csv" || fail 'raw benign alert was lost'
  [[ $(( $(wc -l < "$ROOT/output/test-results.csv") - 1 )) -eq 8 ]] || fail 'expected eight rule tests'; [[ "$(grep -c ',PASS,SYNTHETIC$' "$ROOT/output/test-results.csv")" -eq 8 ]] || fail 'all rule tests must pass'
  for rule in DET-AUTH-001 DET-AUTH-002 DET-NET-001 DET-NET-002; do awk -F, -v r="$rule" 'NR>1 && $2==r && $4=="ALERT"{p=1} NR>1 && $2==r && $4=="NO_ALERT"{n=1} END{exit(p&&n?0:1)}' "$ROOT/source/rules/rule-tests.csv" || fail "positive/negative pair missing for $rule"; done
  [[ $(( $(wc -l < "$ROOT/output/version-report.csv") - 1 )) -eq 4 ]] || fail 'expected four version reports'; [[ "$(grep -c ',PASS,' "$ROOT/output/version-report.csv")" -eq 4 ]] || fail 'version report failure'
  [[ $(( $(wc -l < "$ROOT/output/negative-results.csv") - 1 )) -eq 10 ]] || fail 'expected ten negative families'; [[ "$(grep -c ',BLOCKED,SYNTHETIC$' "$ROOT/output/negative-results.csv")" -eq 10 ]] || fail 'all negatives must be BLOCKED'
  validate_expectations
  [[ "$(cat "$ROOT/output/decision.txt")" == "$DECISION" ]] || fail 'unexpected reference decision'; grep -Fq 'does not prove' "$ROOT/output/proof-boundaries.md" || fail 'proof boundary missing'; grep -Fq 'real incident' "$ROOT/output/proof-boundaries.md" || fail 'real incident boundary missing'
  printf 'Validate OK: 14 fixtures, 5 hypotheses, 4 rules, 8 rule tests, 10 blocked negatives. Decision=%s\n' "$DECISION"
}
reset_lab(){ require_safe_root; require_marker; rm -rf -- "$ROOT"; [[ ! -e "$ROOT" ]] || fail 'reset failed'; printf 'Reset OK: lab root removed.\n'; }

case "$MODE" in
  setup) setup_lab ;;
  status) status_lab ;;
  run) run_lab ;;
  validate) validate_lab ;;
  reset) reset_lab ;;
  *) fail 'usage: module2-detection-engineering.sh {setup|status|run|validate|reset} ROOT' ;;
esac
