#!/bin/bash
# =============================================================================
# Audit Logging Feature - Automated Test Script
# Covers test plan sections 1-7, 12-14 (backend + database)
# Sections 8-11 (frontend UI) require manual browser testing
# =============================================================================
#
# Usage:
#   ./run_audit_tests.sh [options]
#
# Options:
#   --api-base URL       Backend API base URL (default: http://localhost:3001/api)
#   --db PATH            Path to SQLite database file (default: backend/cve_database.db)
#   --admin-pass PASS    Admin password (default: admin123)
#   --help               Show this help message
#
# Prerequisites:
#   - curl, jq, sqlite3 must be installed on this machine
#   - Backend must be running with the audit feature branch deployed
#   - Admin account "admin" must exist
# =============================================================================

set -euo pipefail

# ── Defaults ──────────────────────────────────────────────────────────────────
API_BASE="http://localhost:3001/api"
DB_PATH="backend/cve_database.db"
ADMIN_PASS="admin123"

# ── Parse arguments ──────────────────────────────────────────────────────────
while [[ $# -gt 0 ]]; do
    case $1 in
        --api-base)  API_BASE="$2"; shift 2 ;;
        --db)        DB_PATH="$2"; shift 2 ;;
        --admin-pass) ADMIN_PASS="$2"; shift 2 ;;
        --help)
            sed -n '2,/^# ====.*$/p' "$0" | sed 's/^# \?//'
            exit 0
            ;;
        *) echo "Unknown option: $1"; exit 1 ;;
    esac
done

# ── Colours & counters ───────────────────────────────────────────────────────
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'

PASS_COUNT=0
FAIL_COUNT=0
SKIP_COUNT=0
TOTAL=0

TMPDIR_TEST=$(mktemp -d)
ADMIN_JAR="$TMPDIR_TEST/admin.cookies"
EDITOR_JAR="$TMPDIR_TEST/editor.cookies"
VIEWER_JAR="$TMPDIR_TEST/viewer.cookies"

cleanup() {
    rm -rf "$TMPDIR_TEST"
}
trap cleanup EXIT

# ── Helpers ──────────────────────────────────────────────────────────────────

pass() {
    PASS_COUNT=$((PASS_COUNT + 1))
    TOTAL=$((TOTAL + 1))
    echo -e "  ${GREEN}PASS${NC}  $1"
}

fail() {
    FAIL_COUNT=$((FAIL_COUNT + 1))
    TOTAL=$((TOTAL + 1))
    echo -e "  ${RED}FAIL${NC}  $1"
    if [[ -n "${2:-}" ]]; then
        echo -e "        ${RED}-> $2${NC}"
    fi
}

skip() {
    SKIP_COUNT=$((SKIP_COUNT + 1))
    TOTAL=$((TOTAL + 1))
    echo -e "  ${YELLOW}SKIP${NC}  $1 -- $2"
}

section() {
    echo ""
    echo -e "${CYAN}${BOLD}── $1 ──${NC}"
}

sql() {
    sqlite3 "$DB_PATH" "$1"
}

# Login and store session cookie in a jar file
# Usage: login_as <username> <password> <cookie_jar>
# Returns: HTTP status code
login_as() {
    local user="$1" pass="$2" jar="$3"
    curl -s -o "$TMPDIR_TEST/login_resp.json" -w "%{http_code}" \
        -c "$jar" \
        -X POST "$API_BASE/auth/login" \
        -H "Content-Type: application/json" \
        -d "{\"username\":\"$user\",\"password\":\"$pass\"}"
}

# Authenticated GET request
# Usage: api_get <path> <cookie_jar> [extra_curl_args...]
api_get() {
    local path="$1" jar="$2"
    shift 2
    curl -s -b "$jar" "$@" "$API_BASE$path"
}

# Authenticated POST request with JSON body
api_post() {
    local path="$1" jar="$2" body="$3"
    shift 3
    curl -s -b "$jar" -X POST "$API_BASE$path" \
        -H "Content-Type: application/json" \
        -d "$body" "$@"
}

# Authenticated PATCH request with JSON body
api_patch() {
    local path="$1" jar="$2" body="$3"
    shift 3
    curl -s -b "$jar" -X PATCH "$API_BASE$path" \
        -H "Content-Type: application/json" \
        -d "$body" "$@"
}

# Authenticated DELETE request
api_delete() {
    local path="$1" jar="$2"
    shift 2
    curl -s -b "$jar" -X DELETE "$API_BASE$path" "$@"
}

# Get HTTP status code for an authenticated request
api_status() {
    local method="$1" path="$2" jar="$3"
    shift 3
    curl -s -o /dev/null -w "%{http_code}" -b "$jar" -X "$method" "$API_BASE$path" "$@"
}

# Get the most recent audit log row matching a condition
latest_audit() {
    local where="$1"
    sql "SELECT id, user_id, username, action, entity_type, entity_id, details, ip_address FROM audit_logs WHERE $where ORDER BY id DESC LIMIT 1;"
}

# Count audit rows matching a condition
count_audit() {
    local where="$1"
    sql "SELECT COUNT(*) FROM audit_logs WHERE $where;"
}

# Record the current max audit log id so we can look at entries created after this point
mark_audit() {
    AUDIT_MARK=$(sql "SELECT COALESCE(MAX(id),0) FROM audit_logs;")
}

# Get audit rows created after the mark
audit_since() {
    local where="${1:-1=1}"
    sql "SELECT id, user_id, username, action, entity_type, entity_id, details, ip_address FROM audit_logs WHERE id > $AUDIT_MARK AND ($where) ORDER BY id DESC;"
}

count_audit_since() {
    local where="${1:-1=1}"
    sql "SELECT COUNT(*) FROM audit_logs WHERE id > $AUDIT_MARK AND ($where);"
}

# =============================================================================
#  PREFLIGHT CHECKS
# =============================================================================
echo ""
echo -e "${BOLD}Audit Logging Feature - Automated Test Runner${NC}"
echo "=============================================="
echo "API Base : $API_BASE"
echo "DB Path  : $DB_PATH"
echo ""

PREFLIGHT_OK=true

for cmd in curl jq sqlite3; do
    if ! command -v "$cmd" &>/dev/null; then
        echo -e "${RED}Missing dependency: $cmd${NC}"
        PREFLIGHT_OK=false
    fi
done

if [[ ! -f "$DB_PATH" ]]; then
    echo -e "${RED}Database file not found: $DB_PATH${NC}"
    PREFLIGHT_OK=false
fi

# Check backend is reachable
if ! curl -s --max-time 5 -o /dev/null "$API_BASE/../" 2>/dev/null; then
    # Try the base URL directly
    if ! curl -s --max-time 5 -o /dev/null "${API_BASE%/api}" 2>/dev/null; then
        echo -e "${RED}Backend not reachable at $API_BASE${NC}"
        PREFLIGHT_OK=false
    fi
fi

if [[ "$PREFLIGHT_OK" != "true" ]]; then
    echo ""
    echo "Fix the above issues and re-run."
    exit 1
fi

echo -e "${GREEN}Preflight checks passed.${NC}"

# =============================================================================
# SECTION 1: Database & Schema
# =============================================================================
section "1. Database & Schema"

# 1.1 audit_logs table exists with correct columns
TABLE_SQL=$(sql "SELECT sql FROM sqlite_master WHERE name='audit_logs';")
if [[ -n "$TABLE_SQL" ]]; then
    MISSING=""
    for col in id user_id username action entity_type entity_id details ip_address created_at; do
        if ! echo "$TABLE_SQL" | grep -qi "$col"; then
            MISSING="$MISSING $col"
        fi
    done
    if [[ -z "$MISSING" ]]; then
        pass "1.1 audit_logs table exists with all columns"
    else
        fail "1.1 audit_logs table missing columns:$MISSING"
    fi
else
    fail "1.1 audit_logs table does not exist" "Table not found in sqlite_master"
fi

# 1.2 Indexes created
INDEXES=$(sql "SELECT name FROM sqlite_master WHERE type='index' AND name LIKE 'idx_audit%' ORDER BY name;")
EXPECTED_INDEXES="idx_audit_action idx_audit_created_at idx_audit_entity_type idx_audit_user_id"
FOUND_INDEXES=$(echo "$INDEXES" | tr '\n' ' ' | xargs)
if [[ "$FOUND_INDEXES" == "$EXPECTED_INDEXES" ]]; then
    pass "1.2 All four audit indexes exist"
else
    fail "1.2 Audit indexes" "Expected: $EXPECTED_INDEXES | Found: $FOUND_INDEXES"
fi

# 1.3 Migration idempotency - check that migrate script exists
if [[ -f "backend/migrate-audit-log.js" ]]; then
    pass "1.3 Migration script exists (idempotency requires manual run)"
else
    skip "1.3 Migration idempotency" "migrate-audit-log.js not found at expected path"
fi

# 1.4 Migration creates backups - check script exists (actual backup test is manual)
if [[ -f "backend/migrate-audit-log.js" ]] && grep -q "backup" "backend/migrate-audit-log.js"; then
    pass "1.4 Migration script contains backup logic"
else
    skip "1.4 Migration backup" "Cannot verify without running migration"
fi

# 1.5 Setup includes audit_logs
if [[ -f "backend/setup.js" ]] && grep -q "audit_logs" "backend/setup.js"; then
    pass "1.5 setup.js references audit_logs table"
else
    skip "1.5 Setup includes audit_logs" "setup.js not found or missing reference"
fi

# =============================================================================
# Login as admin for subsequent tests
# =============================================================================
section "Setup: Authenticate test accounts"

mark_audit

ADMIN_STATUS=$(login_as "admin" "$ADMIN_PASS" "$ADMIN_JAR")
if [[ "$ADMIN_STATUS" != "200" ]]; then
    echo -e "${RED}FATAL: Cannot log in as admin (HTTP $ADMIN_STATUS). Aborting.${NC}"
    exit 1
fi
echo -e "  ${GREEN}OK${NC}    Admin login successful"

# Create editor1 and viewer1 if they don't exist
EDITOR_EXISTS=$(sql "SELECT COUNT(*) FROM users WHERE username='_test_editor1';")
if [[ "$EDITOR_EXISTS" == "0" ]]; then
    api_post "/users" "$ADMIN_JAR" '{"username":"_test_editor1","email":"editor1@test.local","password":"editor123","role":"editor"}' > /dev/null
    echo "  Created test account: _test_editor1 (editor)"
fi

VIEWER_EXISTS=$(sql "SELECT COUNT(*) FROM users WHERE username='_test_viewer1';")
if [[ "$VIEWER_EXISTS" == "0" ]]; then
    api_post "/users" "$ADMIN_JAR" '{"username":"_test_viewer1","email":"viewer1@test.local","password":"viewer123","role":"viewer"}' > /dev/null
    echo "  Created test account: _test_viewer1 (viewer)"
fi

EDITOR_STATUS=$(login_as "_test_editor1" "editor123" "$EDITOR_JAR")
if [[ "$EDITOR_STATUS" != "200" ]]; then
    echo -e "${RED}FATAL: Cannot log in as _test_editor1 (HTTP $EDITOR_STATUS). Aborting.${NC}"
    exit 1
fi
echo -e "  ${GREEN}OK${NC}    Editor login successful"

VIEWER_STATUS=$(login_as "_test_viewer1" "viewer123" "$VIEWER_JAR")
if [[ "$VIEWER_STATUS" != "200" ]]; then
    echo -e "${RED}FATAL: Cannot log in as _test_viewer1 (HTTP $VIEWER_STATUS). Aborting.${NC}"
    exit 1
fi
echo -e "  ${GREEN}OK${NC}    Viewer login successful"

# Small sleep to let fire-and-forget audit inserts complete
sleep 0.5

# =============================================================================
# SECTION 2: Authentication Audit Logging
# =============================================================================
section "2. Authentication Audit Logging"

# 2.1 Successful login logged
ROW=$(latest_audit "action='login' AND username='admin'")
if [[ -n "$ROW" ]] && echo "$ROW" | grep -q "login"; then
    # Check details contain role
    DETAILS=$(echo "$ROW" | awk -F'|' '{print $7}')
    if echo "$DETAILS" | jq -e '.role' &>/dev/null; then
        pass "2.1 Successful login logged with role in details"
    else
        fail "2.1 Login logged but details missing role" "$DETAILS"
    fi
else
    fail "2.1 No login audit entry found for admin"
fi

# 2.2 Failed login - wrong password
mark_audit
login_as "admin" "wrongpass" "$TMPDIR_TEST/throwaway.cookies" > /dev/null
sleep 0.3
ROW=$(audit_since "action='login_failed' AND username='admin'")
if [[ -n "$ROW" ]]; then
    DETAILS=$(echo "$ROW" | awk -F'|' '{print $7}')
    if echo "$DETAILS" | grep -q "invalid_password"; then
        pass "2.2 Failed login (wrong password) logged with reason"
    else
        fail "2.2 Failed login logged but reason missing" "$DETAILS"
    fi
else
    fail "2.2 No login_failed audit entry for wrong password"
fi

# 2.3 Failed login - unknown user
mark_audit
login_as "nonexistent_user_xyz" "anypass" "$TMPDIR_TEST/throwaway.cookies" > /dev/null
sleep 0.3
ROW=$(audit_since "action='login_failed' AND username='nonexistent_user_xyz'")
if [[ -n "$ROW" ]]; then
    USER_ID_VAL=$(echo "$ROW" | awk -F'|' '{print $2}')
    DETAILS=$(echo "$ROW" | awk -F'|' '{print $7}')
    if [[ -z "$USER_ID_VAL" ]] && echo "$DETAILS" | grep -q "user_not_found"; then
        pass "2.3 Failed login (unknown user) logged with null user_id"
    else
        fail "2.3 Unknown user login logged but data unexpected" "user_id=$USER_ID_VAL details=$DETAILS"
    fi
else
    fail "2.3 No login_failed audit entry for unknown user"
fi

# 2.4 Failed login - disabled account
mark_audit
# Create a temp user, disable them, try to log in
api_post "/users" "$ADMIN_JAR" '{"username":"_test_disabled","email":"disabled@test.local","password":"test123","role":"viewer"}' > /dev/null
DISABLED_ID=$(sql "SELECT id FROM users WHERE username='_test_disabled';")
if [[ -n "$DISABLED_ID" ]]; then
    api_patch "/users/$DISABLED_ID" "$ADMIN_JAR" '{"is_active":false}' > /dev/null
    sleep 0.3
    mark_audit
    login_as "_test_disabled" "test123" "$TMPDIR_TEST/throwaway.cookies" > /dev/null
    sleep 0.3
    ROW=$(audit_since "action='login_failed' AND username='_test_disabled'")
    if [[ -n "$ROW" ]]; then
        DETAILS=$(echo "$ROW" | awk -F'|' '{print $7}')
        if echo "$DETAILS" | grep -q "account_disabled"; then
            pass "2.4 Failed login (disabled account) logged with reason"
        else
            fail "2.4 Disabled login logged but reason wrong" "$DETAILS"
        fi
    else
        fail "2.4 No login_failed entry for disabled account"
    fi
    # Cleanup
    api_delete "/users/$DISABLED_ID" "$ADMIN_JAR" > /dev/null
else
    skip "2.4 Failed login - disabled account" "Could not create test user"
fi

# 2.5 Logout logged
mark_audit
# Login fresh, then logout
login_as "admin" "$ADMIN_PASS" "$TMPDIR_TEST/logout_test.cookies" > /dev/null
sleep 0.2
curl -s -b "$TMPDIR_TEST/logout_test.cookies" -X POST "$API_BASE/auth/logout" > /dev/null
sleep 0.3
ROW=$(audit_since "action='logout' AND username='admin'")
if [[ -n "$ROW" ]]; then
    pass "2.5 Logout logged"
else
    fail "2.5 No logout audit entry found"
fi

# 2.6 Fire-and-forget (tested more thoroughly in section 12)
skip "2.6 Login does not block on audit error" "Requires corrupting audit_logs table (see section 12)"

# =============================================================================
# SECTION 3: CVE Operation Audit Logging
# =============================================================================
section "3. CVE Operation Audit Logging"

TEST_CVE_ID="CVE-2025-AUDIT-$(date +%s)"

# 3.1 CVE create logged
mark_audit
api_post "/cves" "$ADMIN_JAR" "{\"cve_id\":\"$TEST_CVE_ID\",\"vendor\":\"TestVendor\",\"severity\":\"Critical\",\"description\":\"Automated test CVE\",\"published_date\":\"2025-01-01\"}" > /dev/null
sleep 0.3
ROW=$(audit_since "action='cve_create' AND entity_id='$TEST_CVE_ID'")
if [[ -n "$ROW" ]]; then
    DETAILS=$(echo "$ROW" | awk -F'|' '{print $7}')
    if echo "$DETAILS" | jq -e '.vendor' &>/dev/null && echo "$DETAILS" | jq -e '.severity' &>/dev/null; then
        pass "3.1 CVE create logged with vendor and severity in details"
    else
        fail "3.1 CVE create logged but details incomplete" "$DETAILS"
    fi
else
    fail "3.1 No cve_create audit entry found"
fi

# 3.2 CVE status update logged
mark_audit
STATUS_RESP=$(api_patch "/cves/$TEST_CVE_ID/status" "$ADMIN_JAR" '{"status":"Addressed"}')
sleep 0.3
ROW=$(audit_since "action='cve_update_status' AND entity_id='$TEST_CVE_ID'")
if [[ -n "$ROW" ]]; then
    DETAILS=$(echo "$ROW" | awk -F'|' '{print $7}')
    if echo "$DETAILS" | grep -q "Addressed"; then
        pass "3.2 CVE status update logged with status in details"
    else
        fail "3.2 CVE status update logged but status missing from details" "$DETAILS"
    fi
else
    # Check if the PATCH itself succeeded
    if echo "$STATUS_RESP" | jq -e '.error' &>/dev/null 2>&1; then
        fail "3.2 CVE status update" "PATCH failed: $(echo "$STATUS_RESP" | jq -r '.error')"
    else
        fail "3.2 No cve_update_status audit entry found"
    fi
fi

# 3.3 CVE status update bug fix (no SQL error with vendor reference)
STATUS_CODE=$(curl -s -o /dev/null -w "%{http_code}" -b "$ADMIN_JAR" -X PATCH "$API_BASE/cves/$TEST_CVE_ID/status" \
    -H "Content-Type: application/json" -d '{"status":"In Progress"}')
if [[ "$STATUS_CODE" == "200" ]]; then
    pass "3.3 CVE status update completes without SQL error (vendor bug fixed)"
else
    fail "3.3 CVE status update returned HTTP $STATUS_CODE"
fi

# 3.4 Audit captures acting user
mark_audit
EDITOR_CVE_ID="CVE-2025-EDIT-$(date +%s)"
api_post "/cves" "$EDITOR_JAR" "{\"cve_id\":\"$EDITOR_CVE_ID\",\"vendor\":\"EditorVendor\",\"severity\":\"Low\",\"description\":\"Editor test CVE\",\"published_date\":\"2025-01-01\"}" > /dev/null
sleep 0.3
ROW=$(audit_since "action='cve_create' AND entity_id='$EDITOR_CVE_ID'")
if [[ -n "$ROW" ]]; then
    ACTOR=$(echo "$ROW" | awk -F'|' '{print $3}')
    if [[ "$ACTOR" == "_test_editor1" ]]; then
        pass "3.4 Audit captures acting user (_test_editor1)"
    else
        fail "3.4 Wrong acting user on CVE create" "Expected _test_editor1, got $ACTOR"
    fi
else
    fail "3.4 No cve_create audit entry for editor CVE"
fi

# =============================================================================
# SECTION 4: Document Operation Audit Logging
# =============================================================================
section "4. Document Operation Audit Logging"

# Create a small test file for upload
echo "test document content" > "$TMPDIR_TEST/test_advisory.txt"

# 4.1 Document upload logged
mark_audit
UPLOAD_RESP=$(curl -s -b "$ADMIN_JAR" -X POST "$API_BASE/cves/$TEST_CVE_ID/documents" \
    -F "file=@$TMPDIR_TEST/test_advisory.txt" \
    -F "type=advisory" \
    -F "vendor=TestVendor" \
    -F "notes=Automated test upload")
sleep 0.3
UPLOAD_DOC_ID=$(echo "$UPLOAD_RESP" | jq -r '.id // empty')
ROW=$(audit_since "action='document_upload' AND entity_id='$TEST_CVE_ID'")
if [[ -n "$ROW" ]]; then
    pass "4.1 Document upload logged"
else
    if [[ -z "$UPLOAD_DOC_ID" ]]; then
        fail "4.1 Document upload" "Upload itself failed: $UPLOAD_RESP"
    else
        fail "4.1 No document_upload audit entry found"
    fi
fi

# 4.2 Document delete logged
if [[ -n "$UPLOAD_DOC_ID" ]]; then
    mark_audit
    api_delete "/documents/$UPLOAD_DOC_ID" "$ADMIN_JAR" > /dev/null
    sleep 0.3
    ROW=$(audit_since "action='document_delete'")
    if [[ -n "$ROW" ]]; then
        pass "4.2 Document delete logged"
    else
        fail "4.2 No document_delete audit entry found"
    fi
else
    skip "4.2 Document delete logging" "No document ID from upload step"
fi

# 4.3 Upload captures file metadata
mark_audit
UPLOAD_RESP2=$(curl -s -b "$ADMIN_JAR" -X POST "$API_BASE/cves/$TEST_CVE_ID/documents" \
    -F "file=@$TMPDIR_TEST/test_advisory.txt;filename=advisory.pdf" \
    -F "type=advisory" \
    -F "vendor=TestVendor" \
    -F "notes=metadata test")
sleep 0.3
UPLOAD_DOC_ID2=$(echo "$UPLOAD_RESP2" | jq -r '.id // empty')
ROW=$(audit_since "action='document_upload' AND entity_id='$TEST_CVE_ID'")
if [[ -n "$ROW" ]]; then
    DETAILS=$(echo "$ROW" | awk -F'|' '{print $7}')
    HAS_VENDOR=$(echo "$DETAILS" | jq -e '.vendor' 2>/dev/null)
    HAS_TYPE=$(echo "$DETAILS" | jq -e '.type' 2>/dev/null)
    HAS_FILE=$(echo "$DETAILS" | jq -e '.filename' 2>/dev/null)
    if [[ -n "$HAS_VENDOR" && -n "$HAS_TYPE" && -n "$HAS_FILE" ]]; then
        pass "4.3 Upload details contain vendor, type, and filename"
    else
        fail "4.3 Upload details incomplete" "$DETAILS"
    fi
else
    fail "4.3 No document_upload audit entry for metadata test"
fi
# Cleanup doc
if [[ -n "$UPLOAD_DOC_ID2" ]]; then
    api_delete "/documents/$UPLOAD_DOC_ID2" "$ADMIN_JAR" > /dev/null
fi

# =============================================================================
# SECTION 5: User Management Audit Logging
# =============================================================================
section "5. User Management Audit Logging"

# 5.1 User create logged
mark_audit
CREATE_RESP=$(api_post "/users" "$ADMIN_JAR" '{"username":"_test_audituser","email":"audituser@test.local","password":"pass123","role":"viewer"}')
TESTUSER_ID=$(echo "$CREATE_RESP" | jq -r '.user.id // empty')
sleep 0.3
ROW=$(audit_since "action='user_create'")
if [[ -n "$ROW" ]]; then
    DETAILS=$(echo "$ROW" | awk -F'|' '{print $7}')
    if echo "$DETAILS" | grep -q "_test_audituser" && echo "$DETAILS" | grep -q "viewer"; then
        pass "5.1 User create logged with username and role"
    else
        fail "5.1 User create logged but details incomplete" "$DETAILS"
    fi
else
    fail "5.1 No user_create audit entry found"
fi

# 5.2 User role update logged
if [[ -n "$TESTUSER_ID" ]]; then
    mark_audit
    api_patch "/users/$TESTUSER_ID" "$ADMIN_JAR" '{"role":"editor"}' > /dev/null
    sleep 0.3
    ROW=$(audit_since "action='user_update'")
    if [[ -n "$ROW" ]]; then
        DETAILS=$(echo "$ROW" | awk -F'|' '{print $7}')
        if echo "$DETAILS" | grep -q '"role":"editor"'; then
            pass "5.2 User role update logged"
        else
            fail "5.2 User update logged but role missing from details" "$DETAILS"
        fi
    else
        fail "5.2 No user_update audit entry found"
    fi
else
    skip "5.2 User role update" "No test user ID"
fi

# 5.3 User password change logged (password itself NOT logged)
if [[ -n "$TESTUSER_ID" ]]; then
    mark_audit
    api_patch "/users/$TESTUSER_ID" "$ADMIN_JAR" '{"password":"newpass456"}' > /dev/null
    sleep 0.3
    ROW=$(audit_since "action='user_update'")
    if [[ -n "$ROW" ]]; then
        DETAILS=$(echo "$ROW" | awk -F'|' '{print $7}')
        if echo "$DETAILS" | grep -q '"password_changed":true'; then
            # Ensure actual password is NOT in details
            if echo "$DETAILS" | grep -q "newpass456"; then
                fail "5.3 Password change logged but PASSWORD IS IN DETAILS (security issue)" "$DETAILS"
            else
                pass "5.3 Password change logged with password_changed:true (password not exposed)"
            fi
        else
            fail "5.3 Password change logged but password_changed flag missing" "$DETAILS"
        fi
    else
        fail "5.3 No user_update audit entry for password change"
    fi
else
    skip "5.3 Password change logging" "No test user ID"
fi

# 5.4 Multiple field update logged
if [[ -n "$TESTUSER_ID" ]]; then
    mark_audit
    api_patch "/users/$TESTUSER_ID" "$ADMIN_JAR" '{"username":"_test_audituser_renamed","role":"viewer"}' > /dev/null
    sleep 0.3
    ROW=$(audit_since "action='user_update'")
    if [[ -n "$ROW" ]]; then
        DETAILS=$(echo "$ROW" | awk -F'|' '{print $7}')
        HAS_USERNAME=$(echo "$DETAILS" | jq -e '.username' 2>/dev/null)
        HAS_ROLE=$(echo "$DETAILS" | jq -e '.role' 2>/dev/null)
        if [[ -n "$HAS_USERNAME" && -n "$HAS_ROLE" ]]; then
            pass "5.4 Multiple field update logged with all changed fields"
        else
            fail "5.4 Multi-field update logged but fields missing" "$DETAILS"
        fi
    else
        fail "5.4 No user_update audit entry for multi-field change"
    fi
    # Rename back for later tests
    api_patch "/users/$TESTUSER_ID" "$ADMIN_JAR" '{"username":"_test_audituser"}' > /dev/null
    sleep 0.2
else
    skip "5.4 Multiple field update" "No test user ID"
fi

# 5.6 User deactivation logged (testing before 5.5 delete so user still exists)
if [[ -n "$TESTUSER_ID" ]]; then
    mark_audit
    api_patch "/users/$TESTUSER_ID" "$ADMIN_JAR" '{"is_active":false}' > /dev/null
    sleep 0.3
    ROW=$(audit_since "action='user_update'")
    if [[ -n "$ROW" ]]; then
        DETAILS=$(echo "$ROW" | awk -F'|' '{print $7}')
        if echo "$DETAILS" | grep -q '"is_active":false'; then
            pass "5.6 User deactivation logged with is_active:false"
        else
            fail "5.6 Deactivation logged but is_active field missing" "$DETAILS"
        fi
    else
        fail "5.6 No user_update audit entry for deactivation"
    fi
    # Re-activate for delete test
    api_patch "/users/$TESTUSER_ID" "$ADMIN_JAR" '{"is_active":true}' > /dev/null
    sleep 0.2
else
    skip "5.6 User deactivation" "No test user ID"
fi

# 5.5 User delete logged
if [[ -n "$TESTUSER_ID" ]]; then
    mark_audit
    api_delete "/users/$TESTUSER_ID" "$ADMIN_JAR" > /dev/null
    sleep 0.3
    ROW=$(audit_since "action='user_delete'")
    if [[ -n "$ROW" ]]; then
        DETAILS=$(echo "$ROW" | awk -F'|' '{print $7}')
        if echo "$DETAILS" | grep -q "_test_audituser"; then
            pass "5.5 User delete logged with deleted_username"
        else
            fail "5.5 User delete logged but username missing from details" "$DETAILS"
        fi
    else
        fail "5.5 No user_delete audit entry found"
    fi
else
    skip "5.5 User delete" "No test user ID"
fi

# 5.7 Self-delete prevented, no log
mark_audit
ADMIN_ID=$(sql "SELECT id FROM users WHERE username='admin';")
SELF_DEL_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -b "$ADMIN_JAR" -X DELETE "$API_BASE/users/$ADMIN_ID")
sleep 0.3
SELF_DEL_LOG=$(count_audit_since "action='user_delete' AND entity_id='$ADMIN_ID'")
if [[ "$SELF_DEL_STATUS" == "400" && "$SELF_DEL_LOG" == "0" ]]; then
    pass "5.7 Self-delete prevented (400) with no audit entry"
elif [[ "$SELF_DEL_STATUS" != "400" ]]; then
    fail "5.7 Self-delete returned HTTP $SELF_DEL_STATUS (expected 400)"
else
    fail "5.7 Self-delete blocked but audit entry was still created"
fi

# =============================================================================
# SECTION 6: API Access Control
# =============================================================================
section "6. API Access Control"

# 6.1 Admin can query audit logs
RESP=$(api_get "/audit-logs" "$ADMIN_JAR")
if echo "$RESP" | jq -e '.logs' &>/dev/null && echo "$RESP" | jq -e '.pagination' &>/dev/null; then
    pass "6.1 Admin can query audit logs (200 with logs + pagination)"
else
    fail "6.1 Admin audit log query" "Response: $(echo "$RESP" | head -c 200)"
fi

# 6.2 Editor denied audit logs
EDITOR_AL_STATUS=$(api_status "GET" "/audit-logs" "$EDITOR_JAR")
if [[ "$EDITOR_AL_STATUS" == "403" ]]; then
    pass "6.2 Editor denied audit logs (403)"
else
    fail "6.2 Editor audit log access returned HTTP $EDITOR_AL_STATUS (expected 403)"
fi

# 6.3 Viewer denied audit logs
VIEWER_AL_STATUS=$(api_status "GET" "/audit-logs" "$VIEWER_JAR")
if [[ "$VIEWER_AL_STATUS" == "403" ]]; then
    pass "6.3 Viewer denied audit logs (403)"
else
    fail "6.3 Viewer audit log access returned HTTP $VIEWER_AL_STATUS (expected 403)"
fi

# 6.4 Unauthenticated denied
UNAUTH_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$API_BASE/audit-logs")
if [[ "$UNAUTH_STATUS" == "401" ]]; then
    pass "6.4 Unauthenticated denied audit logs (401)"
else
    fail "6.4 Unauthenticated audit log access returned HTTP $UNAUTH_STATUS (expected 401)"
fi

# 6.5 Admin can get actions list
ACTIONS_RESP=$(api_get "/audit-logs/actions" "$ADMIN_JAR")
if echo "$ACTIONS_RESP" | jq -e 'type == "array"' &>/dev/null; then
    pass "6.5 Admin can get actions list (array returned)"
else
    fail "6.5 Actions list" "Response: $(echo "$ACTIONS_RESP" | head -c 200)"
fi

# 6.6 Non-admin denied actions list
EDITOR_ACTIONS_STATUS=$(api_status "GET" "/audit-logs/actions" "$EDITOR_JAR")
if [[ "$EDITOR_ACTIONS_STATUS" == "403" ]]; then
    pass "6.6 Non-admin denied actions list (403)"
else
    fail "6.6 Editor actions list returned HTTP $EDITOR_ACTIONS_STATUS (expected 403)"
fi

# =============================================================================
# SECTION 7: API Filtering & Pagination
# =============================================================================
section "7. API Filtering & Pagination"

# 7.1 Default pagination
RESP=$(api_get "/audit-logs" "$ADMIN_JAR")
PAGE=$(echo "$RESP" | jq -r '.pagination.page')
LIMIT=$(echo "$RESP" | jq -r '.pagination.limit')
LOG_COUNT=$(echo "$RESP" | jq '.logs | length')
TOTAL_COUNT=$(echo "$RESP" | jq -r '.pagination.total')
if [[ "$PAGE" == "1" && "$LIMIT" == "25" && "$LOG_COUNT" -le 25 ]]; then
    pass "7.1 Default pagination (page=1, limit=25, logs<=$LIMIT)"
else
    fail "7.1 Default pagination" "page=$PAGE limit=$LIMIT log_count=$LOG_COUNT"
fi

# 7.2 Custom page size
RESP=$(api_get "/audit-logs?limit=5" "$ADMIN_JAR")
LOG_COUNT=$(echo "$RESP" | jq '.logs | length')
LIMIT=$(echo "$RESP" | jq -r '.pagination.limit')
if [[ "$LIMIT" == "5" && "$LOG_COUNT" -le 5 ]]; then
    pass "7.2 Custom page size (limit=5)"
else
    fail "7.2 Custom page size" "limit=$LIMIT log_count=$LOG_COUNT"
fi

# 7.3 Page size capped at 100
RESP=$(api_get "/audit-logs?limit=999" "$ADMIN_JAR")
LIMIT=$(echo "$RESP" | jq -r '.pagination.limit')
if [[ "$LIMIT" -le 100 ]]; then
    pass "7.3 Page size capped at 100 (got limit=$LIMIT)"
else
    fail "7.3 Page size cap" "limit=$LIMIT (expected <=100)"
fi

# 7.4 Navigate to page 2
RESP_P1=$(api_get "/audit-logs?limit=5&page=1" "$ADMIN_JAR")
RESP_P2=$(api_get "/audit-logs?limit=5&page=2" "$ADMIN_JAR")
P1_FIRST=$(echo "$RESP_P1" | jq -r '.logs[0].id // empty')
P2_FIRST=$(echo "$RESP_P2" | jq -r '.logs[0].id // empty')
P2_PAGE=$(echo "$RESP_P2" | jq -r '.pagination.page')
if [[ -n "$P2_FIRST" && "$P1_FIRST" != "$P2_FIRST" && "$P2_PAGE" == "2" ]]; then
    pass "7.4 Page 2 returns different entries"
elif [[ -z "$P2_FIRST" ]]; then
    skip "7.4 Navigate to page 2" "Not enough entries for 2 pages at limit=5"
else
    fail "7.4 Page 2 entries same as page 1"
fi

# 7.5 Filter by username
RESP=$(api_get "/audit-logs?user=admin" "$ADMIN_JAR")
NON_ADMIN=$(echo "$RESP" | jq '[.logs[] | select(.username != "admin" and (.username | test("admin") | not))] | length')
if [[ "$NON_ADMIN" == "0" ]]; then
    pass "7.5 Filter by username=admin returns only admin entries"
else
    fail "7.5 Username filter" "$NON_ADMIN non-admin entries returned"
fi

# 7.6 Partial username match
RESP=$(api_get "/audit-logs?user=adm" "$ADMIN_JAR")
HAS_ADMIN=$(echo "$RESP" | jq '[.logs[] | select(.username == "admin")] | length')
if [[ "$HAS_ADMIN" -gt 0 ]]; then
    pass "7.6 Partial username match (adm matches admin)"
else
    fail "7.6 Partial username match found no admin entries"
fi

# 7.7 Filter by action
RESP=$(api_get "/audit-logs?action=login" "$ADMIN_JAR")
NON_LOGIN=$(echo "$RESP" | jq '[.logs[] | select(.action != "login")] | length')
if [[ "$NON_LOGIN" == "0" ]]; then
    pass "7.7 Filter by action=login returns only login entries"
else
    fail "7.7 Action filter" "$NON_LOGIN non-login entries returned"
fi

# 7.8 Filter by entity type
RESP=$(api_get "/audit-logs?entityType=auth" "$ADMIN_JAR")
NON_AUTH=$(echo "$RESP" | jq '[.logs[] | select(.entity_type != "auth")] | length')
if [[ "$NON_AUTH" == "0" ]]; then
    pass "7.8 Filter by entityType=auth returns only auth entries"
else
    fail "7.8 Entity type filter" "$NON_AUTH non-auth entries returned"
fi

# 7.9 Filter by date range
TODAY=$(date +%Y-%m-%d)
RESP=$(api_get "/audit-logs?startDate=$TODAY&endDate=$TODAY" "$ADMIN_JAR")
LOG_COUNT=$(echo "$RESP" | jq '.logs | length')
if [[ "$LOG_COUNT" -gt 0 ]]; then
    pass "7.9 Date range filter returns entries for today ($LOG_COUNT entries)"
else
    fail "7.9 Date range filter returned 0 entries for today"
fi

# 7.10 Combined filters
RESP=$(api_get "/audit-logs?user=admin&action=login&entityType=auth" "$ADMIN_JAR")
ALL_MATCH=$(echo "$RESP" | jq '[.logs[] | select(.action == "login" and .entity_type == "auth" and (.username | test("admin")))] | length')
TOTAL_RETURNED=$(echo "$RESP" | jq '.logs | length')
if [[ "$TOTAL_RETURNED" -gt 0 && "$ALL_MATCH" == "$TOTAL_RETURNED" ]]; then
    pass "7.10 Combined filters return only matching entries ($TOTAL_RETURNED)"
elif [[ "$TOTAL_RETURNED" == "0" ]]; then
    fail "7.10 Combined filters returned 0 entries"
else
    fail "7.10 Combined filters" "$ALL_MATCH of $TOTAL_RETURNED matched all criteria"
fi

# 7.11 Empty result set
RESP=$(api_get "/audit-logs?user=zzz_no_such_user_zzz" "$ADMIN_JAR")
LOG_COUNT=$(echo "$RESP" | jq '.logs | length')
TOTAL_VAL=$(echo "$RESP" | jq -r '.pagination.total')
if [[ "$LOG_COUNT" == "0" && "$TOTAL_VAL" == "0" ]]; then
    pass "7.11 Empty result set returns logs:[] and total:0"
else
    fail "7.11 Empty result set" "logs=$LOG_COUNT total=$TOTAL_VAL"
fi

# 7.12 Ordering (newest first)
RESP=$(api_get "/audit-logs?limit=5" "$ADMIN_JAR")
FIRST_ID=$(echo "$RESP" | jq -r '.logs[0].id')
LAST_ID=$(echo "$RESP" | jq -r '.logs[-1].id')
if [[ "$FIRST_ID" -gt "$LAST_ID" ]]; then
    pass "7.12 Entries ordered DESC (newest first: id $FIRST_ID > $LAST_ID)"
else
    fail "7.12 Ordering" "First id=$FIRST_ID, last id=$LAST_ID (expected DESC)"
fi

# =============================================================================
# SECTION 12: Fire-and-Forget Behavior
# =============================================================================
section "12. Fire-and-Forget Behavior"

skip "12.1 Audit failure does not break login" "Requires temporarily corrupting audit_logs table"
skip "12.2 Audit failure does not break CVE create" "Requires temporarily corrupting audit_logs table"
skip "12.3 Response not delayed by audit" "Requires timing measurement under load"

# =============================================================================
# SECTION 13: Data Integrity
# =============================================================================
section "13. Data Integrity"

# 13.1 Audit survives user deletion
# Create user, perform action, delete user, check audit still has username
mark_audit
CREATE_RESP=$(api_post "/users" "$ADMIN_JAR" '{"username":"_test_ephemeral","email":"ephemeral@test.local","password":"pass123","role":"viewer"}')
EPH_ID=$(echo "$CREATE_RESP" | jq -r '.user.id // empty')
sleep 0.3
if [[ -n "$EPH_ID" ]]; then
    api_delete "/users/$EPH_ID" "$ADMIN_JAR" > /dev/null
    sleep 0.3
    SURVIVING=$(sql "SELECT COUNT(*) FROM audit_logs WHERE details LIKE '%_test_ephemeral%';")
    if [[ "$SURVIVING" -gt 0 ]]; then
        pass "13.1 Audit entries survive user deletion (denormalized username)"
    else
        fail "13.1 No audit entries found for deleted user"
    fi
else
    skip "13.1 Audit survives user deletion" "Could not create ephemeral user"
fi

# 13.2 Details stored as valid JSON
INVALID_JSON=$(sql "SELECT COUNT(*) FROM audit_logs WHERE details IS NOT NULL AND details != '' AND json_valid(details) = 0;")
if [[ "$INVALID_JSON" == "0" ]]; then
    pass "13.2 All non-null details values are valid JSON"
else
    fail "13.2 Found $INVALID_JSON rows with invalid JSON in details"
fi

# 13.3 IP address captured
HAS_IP=$(sql "SELECT COUNT(*) FROM audit_logs WHERE ip_address IS NOT NULL AND ip_address != '';")
if [[ "$HAS_IP" -gt 0 ]]; then
    pass "13.3 IP addresses captured ($HAS_IP entries with IP)"
else
    fail "13.3 No audit entries have an IP address"
fi

# 13.4 Timestamps auto-populated
NULL_TS=$(sql "SELECT COUNT(*) FROM audit_logs WHERE created_at IS NULL;")
if [[ "$NULL_TS" == "0" ]]; then
    pass "13.4 All audit entries have non-null created_at"
else
    fail "13.4 Found $NULL_TS entries with null created_at"
fi

# 13.5 Null entity_id for auth actions
AUTH_WITH_ENTITY=$(sql "SELECT COUNT(*) FROM audit_logs WHERE entity_type='auth' AND entity_id IS NOT NULL;")
if [[ "$AUTH_WITH_ENTITY" == "0" ]]; then
    pass "13.5 Auth entries have null entity_id"
else
    fail "13.5 Found $AUTH_WITH_ENTITY auth entries with non-null entity_id"
fi

# =============================================================================
# SECTION 14: End-to-End Workflow
# =============================================================================
section "14. End-to-End Workflow"

# 14.1 Full user lifecycle
mark_audit
# Step 1: admin creates user
CREATE_RESP=$(api_post "/users" "$ADMIN_JAR" '{"username":"_test_lifecycle","email":"lifecycle@test.local","password":"life123","role":"viewer"}')
LIFE_ID=$(echo "$CREATE_RESP" | jq -r '.user.id // empty')
sleep 0.2

if [[ -n "$LIFE_ID" ]]; then
    # Step 2: lifecycle user logs in
    login_as "_test_lifecycle" "life123" "$TMPDIR_TEST/lifecycle.cookies" > /dev/null
    sleep 0.2

    # Step 3: lifecycle user creates a CVE
    LIFE_CVE="CVE-2025-LIFE-$(date +%s)"
    api_post "/cves" "$TMPDIR_TEST/lifecycle.cookies" "{\"cve_id\":\"$LIFE_CVE\",\"vendor\":\"LifeVendor\",\"severity\":\"Medium\",\"description\":\"Lifecycle test\",\"published_date\":\"2025-01-01\"}" > /dev/null
    sleep 0.2

    # Step 4: admin updates role
    api_patch "/users/$LIFE_ID" "$ADMIN_JAR" '{"role":"editor"}' > /dev/null
    sleep 0.2

    # Step 5: admin deletes user
    api_delete "/users/$LIFE_ID" "$ADMIN_JAR" > /dev/null
    sleep 0.3

    # Verify all actions logged
    LIFECYCLE_ACTIONS=$(sql "SELECT action FROM audit_logs WHERE id > $AUDIT_MARK ORDER BY id;" | tr '\n' ',')
    EXPECTED_ACTIONS="user_create,login,cve_create,user_update,user_delete,"

    # Check each expected action is present in order
    ALL_FOUND=true
    for ACT in user_create login cve_create user_update user_delete; do
        if ! echo "$LIFECYCLE_ACTIONS" | grep -q "$ACT"; then
            ALL_FOUND=false
            break
        fi
    done

    if [[ "$ALL_FOUND" == "true" ]]; then
        pass "14.1 Full user lifecycle - all 5 actions logged"
    else
        fail "14.1 Full user lifecycle" "Expected: $EXPECTED_ACTIONS Got: $LIFECYCLE_ACTIONS"
    fi

    # 14.2 Filter by lifecycle user's own actions
    LIFE_OWN=$(sql "SELECT COUNT(*) FROM audit_logs WHERE id > $AUDIT_MARK AND username='_test_lifecycle';")
    if [[ "$LIFE_OWN" -ge 2 ]]; then
        pass "14.2 Lifecycle user's own actions found ($LIFE_OWN entries)"
    else
        fail "14.2 Expected at least 2 entries for _test_lifecycle, found $LIFE_OWN"
    fi
else
    skip "14.1 Full user lifecycle" "Could not create lifecycle user"
    skip "14.2 Filter by lifecycle user" "Depends on 14.1"
fi

# 14.3 Security audit trail - failed logins
mark_audit
for i in 1 2 3; do
    login_as "admin" "badpass$i" "$TMPDIR_TEST/throwaway.cookies" > /dev/null
done
sleep 0.5
FAIL_COUNT_CHECK=$(count_audit_since "action='login_failed' AND username='admin'")
if [[ "$FAIL_COUNT_CHECK" -ge 3 ]]; then
    pass "14.3 Security audit trail - $FAIL_COUNT_CHECK failed login attempts recorded"
else
    fail "14.3 Expected 3+ failed login attempts, found $FAIL_COUNT_CHECK"
fi

# =============================================================================
# CLEANUP
# =============================================================================
section "Cleanup"

# Remove test CVEs (direct SQL since there's no delete CVE API)
sql "DELETE FROM cves WHERE cve_id LIKE 'CVE-2025-AUDIT-%' OR cve_id LIKE 'CVE-2025-EDIT-%' OR cve_id LIKE 'CVE-2025-LIFE-%';" 2>/dev/null
echo "  Removed test CVEs"

# Remove test users (may already be deleted)
for u in _test_editor1 _test_viewer1 _test_disabled _test_audituser _test_ephemeral _test_lifecycle; do
    UID_VAL=$(sql "SELECT id FROM users WHERE username='$u';" 2>/dev/null)
    if [[ -n "$UID_VAL" ]]; then
        sql "DELETE FROM sessions WHERE user_id=$UID_VAL;" 2>/dev/null
        sql "DELETE FROM users WHERE id=$UID_VAL;" 2>/dev/null
    fi
done
echo "  Removed test users"

# Remove test audit entries
sql "DELETE FROM audit_logs WHERE username LIKE '_test_%' OR entity_id LIKE 'CVE-2025-AUDIT-%' OR entity_id LIKE 'CVE-2025-EDIT-%' OR entity_id LIKE 'CVE-2025-LIFE-%' OR details LIKE '%_test_%';" 2>/dev/null
# Also clean up the failed login attempts for nonexistent user
sql "DELETE FROM audit_logs WHERE username='nonexistent_user_xyz';" 2>/dev/null
echo "  Removed test audit entries"

# =============================================================================
# SUMMARY
# =============================================================================
echo ""
echo "=============================================="
echo -e "${BOLD}TEST RESULTS${NC}"
echo "=============================================="
echo -e "  ${GREEN}Passed:  $PASS_COUNT${NC}"
echo -e "  ${RED}Failed:  $FAIL_COUNT${NC}"
echo -e "  ${YELLOW}Skipped: $SKIP_COUNT${NC}"
echo "  Total:   $TOTAL"
echo ""

# Note about manual tests
MANUAL_TESTS=34
echo -e "${CYAN}Note: $MANUAL_TESTS tests in sections 8-11 (Frontend UI) require manual browser testing.${NC}"
echo "  8.  Menu Access        (5 tests)  - Role-based visibility of Audit Log menu"
echo "  9.  Modal Display     (12 tests)  - Table rendering, badges, formatting"
echo "  10. Frontend Filters  (10 tests)  - Filter dropdowns, reset, apply"
echo "  11. Pagination UI      (7 tests)  - Page navigation, boundary conditions"
echo ""

if [[ "$FAIL_COUNT" -eq 0 ]]; then
    echo -e "${GREEN}${BOLD}RESULT: ALL AUTOMATED TESTS PASSED${NC}"
    exit 0
else
    echo -e "${RED}${BOLD}RESULT: $FAIL_COUNT TEST(S) FAILED${NC}"
    exit 1
fi
