#!/bin/bash
# End-to-end deployment smoke test for NVR Docker stack with ScaleWatcher POS.
#
# Exercises the fresh-install happy path:
#   1. Set product key (features: LPR,POS — uppercase)
#   2. Start testcam container (2 simulated RTSP streams)
#   3. Add Device + enable Camera 1 (overlay) and Camera 2 (capture only)
#   4. Create PosType 19 (ScaleWatcher) with Ticket_Received start/finish and
#      bTcpPort so ThreadTicketReader opens a listener
#   5. Restart connector; wait for TCP listener to come up
#   6. Send a <ticketentry> XML block over the TCP port — this fires a
#      Ticket_Received event that ThreadTruckTransaction consumes and
#      generates a PDF (plus per-camera JPGs) in the tickets folder
#   7. Verify the resulting PDF/JPG artifacts land in /videostore/vs1/dividia/tickets
#
# Usage:
#   cd docker/
#   ./test-deploy.sh                    # run against the current compose stack
#   ./test-deploy.sh --clean            # teardown testcam + restore DB state
#
# Assumes the NVR stack is already up (`docker compose up -d`) and healthy.
# Picks up MYSQL_ROOT_PASSWORD and COMPOSE_FILE from $SCRIPT_DIR/.env if present.

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"

# Source DB password from the install's .env (fresh installs randomize it).
# Fallback to lynn1094 covers older installs / dev stacks without .env.
if [[ -f "$SCRIPT_DIR/.env" ]]; then
    ENV_MYSQL=$(grep '^MYSQL_ROOT_PASSWORD=' "$SCRIPT_DIR/.env" 2>/dev/null | cut -d= -f2- | tr -d '\r\n')
    [[ -n "$ENV_MYSQL" ]] && MYSQL_PASS="$ENV_MYSQL"
fi
MYSQL_PASS="${MYSQL_ROOT_PASSWORD:-${MYSQL_PASS:-lynn1094}}"

# If .env has a COMPOSE_FILE=... entry, respect it so dev/prod/windows all work.
# Otherwise default to the dev overlay (explicit-file mode).
if [[ -f "$SCRIPT_DIR/.env" ]] && grep -q '^COMPOSE_FILE=' "$SCRIPT_DIR/.env"; then
    COMPOSE_FILES=()   # docker compose reads COMPOSE_FILE from .env automatically
else
    COMPOSE_OVERLAY="${COMPOSE_OVERLAY:-dev}"
    COMPOSE_FILES=(-f docker-compose.yml -f "docker-compose.${COMPOSE_OVERLAY}.yml")
fi
ENGINE_VERSION="${ENGINE_VERSION:-6.2}"
TESTCAM_IMAGE="${TESTCAM_IMAGE:-dividia/nvr-testcam:latest}"
TEST_TICKET_NUM="${TEST_TICKET_NUM:-99001}"
TEST_TICKET_UNIQUE="${TEST_TICKET_UNIQUE:-99001001}"
TEST_TICKET_PORT="${TEST_TICKET_PORT:-43219}"   # Unused port for ThreadTicketReader listener

# --- Helpers ---

log()    { printf "\n\033[1;34m==>\033[0m %s\n" "$1"; }
ok()     { printf "  \033[1;32m[OK]\033[0m %s\n" "$1"; }
warn()   { printf "  \033[1;33m[WARN]\033[0m %s\n" "$1"; }
fail()   { printf "  \033[1;31m[FAIL]\033[0m %s\n" "$1"; exit 1; }

mysql_exec() {
    docker compose ${COMPOSE_FILES[@]+"${COMPOSE_FILES[@]}"} exec -T db \
        mariadb -u root -p"$MYSQL_PASS" dtech "$@"
}

# --- Teardown (only when --clean passed) ---

if [[ "${1:-}" == "--clean" ]]; then
    log "Cleaning up test state"
    docker rm -f testcam 2>/dev/null || true
    mysql_exec <<'SQL'
DELETE FROM ApexLog WHERE bTicket LIKE '99%';
DELETE FROM PosType19 WHERE bID IN (SELECT bID FROM Pos WHERE sName='Test ScaleWatcher');
DELETE FROM Pos WHERE sName='Test ScaleWatcher';
UPDATE Camera SET fEnable=0, fPos=0, fLPR=0 WHERE bID IN (1,2);
DELETE FROM Device WHERE sName='Testcam';
SQL
    ok "Test state removed"
    exit 0
fi

# --- 1. Set product key (pos feature) ---

log "[1/7] Setting product key with 'pos' feature"

SEED=$(docker compose ${COMPOSE_FILES[@]+"${COMPOSE_FILES[@]}"} exec -T backend \
    nvr-check-key -S | tr -d '\r\n')
[[ ${#SEED} -eq 12 ]] || fail "Invalid seed: '$SEED' (len=${#SEED})"
ok "Seed: $SEED"

# Key is generated by running make-key on an internal host (e.g. develop), NOT
# checked in or publicly distributed. Sources (in priority order):
#   1. KEY=... env var
#   2. existing KEY= line in data/config/dvs.conf (pre-seeded manually)
#   3. MAKE_KEY=/path/to/make-key pointing at an internal build
if [[ -z "${KEY:-}" && -f ./data/config/dvs.conf ]]; then
    KEY=$(sed -n 's/^KEY=//p' ./data/config/dvs.conf | tr -d '\r\n' | head -1)
fi
if [[ -n "${KEY:-}" ]]; then
    :  # honor pre-computed key
elif [[ -x "${MAKE_KEY:-/does/not/exist}" ]]; then
    # Features and POS types MUST be uppercase for nvr-check-key to validate.
    # -n (numcams), -p (pos max) must be set alongside -L (lprcams).
    KEY=$("$MAKE_KEY" -N -S 4000 -V "$ENGINE_VERSION" -F LPR,POS -P JWS_APEX -n 4 -p 4 -L 4 -s "$SEED" | tr -d '\r\n')
else
    fail "Need KEY=... env var (precomputed on internal host for seed $SEED), or pre-seeded KEY= line in data/config/dvs.conf, or MAKE_KEY=/path/to/make-key"
fi
[[ -n "$KEY" ]] || fail "make-key produced empty key"
ok "Key generated"

# Write key into dvs.conf (host-side config mount) and restart backend.
if [[ -f ./data/config/dvs.conf ]]; then
    perl -pi -e "s/^KEY=.*/KEY=${KEY}/" ./data/config/dvs.conf
    grep -q "^KEY=" ./data/config/dvs.conf || echo "KEY=${KEY}" >> ./data/config/dvs.conf
else
    mkdir -p ./data/config
    echo "KEY=${KEY}" > ./data/config/dvs.conf
fi

docker compose ${COMPOSE_FILES[@]+"${COMPOSE_FILES[@]}"} restart backend >/dev/null

# Wait for backend to come back (up to 60s) then validate
CHECK=""
for i in $(seq 1 30); do
    sleep 2
    CHECK=$(docker compose ${COMPOSE_FILES[@]+"${COMPOSE_FILES[@]}"} exec -T backend nvr-check-key -V "$KEY" 2>/dev/null | tr -d '\r\n' || true)
    if [[ "$CHECK" == "pass" ]]; then break; fi
done
[[ "$CHECK" == "pass" ]] || fail "Key validation failed after 60s: '$CHECK'"
ok "Key validated: pass"

# --- 2. Start testcam (2 streams) ---

log "[2/7] Starting testcam container (2 RTSP streams)"

docker rm -f testcam 2>/dev/null || true

# If the dividia/nvr-testcam:latest pull is denied (restricted DockerHub repo),
# build it locally from the public-only context hosted on files.dividia.net.
if ! docker image inspect "$TESTCAM_IMAGE" >/dev/null 2>&1; then
    if ! docker pull "$TESTCAM_IMAGE" >/dev/null 2>&1; then
        warn "Cannot pull $TESTCAM_IMAGE — building from public context"
        BUILD_DIR=$(mktemp -d)
        curl -fsSL http://files.dividia.net/software/nvr-docker/testcam-context.tar.gz | tar xz -C "$BUILD_DIR"
        # --network host works around Docker bridge MTU issues on Vagrant/NAT
        # setups where apk fetches time out on the default bridge network.
        docker build --network host -t "$TESTCAM_IMAGE" "$BUILD_DIR/testcam" >/dev/null \
            || fail "testcam build failed"
        rm -rf "$BUILD_DIR"
        ok "Built $TESTCAM_IMAGE locally"
    fi
fi

docker run -d --network host --name testcam \
    -e NUM_STREAMS=2 -e RTSP_PORT=8554 \
    "$TESTCAM_IMAGE" >/dev/null
sleep 3
docker ps --filter name=testcam --format '{{.Names}} {{.Status}}' | grep -q 'Up' \
    || fail "testcam container did not start"
ok "testcam up on rtsp://127.0.0.1:8554/cam1 + /cam2"

# --- 3. Add Device + enable Cameras 1 and 2 ---

log "[3/7] Configuring Device and Cameras"

# Register a Testcam DeviceType (bID=200 to avoid collision with shipped types).
# URL template: rtsp://<IP>[:port]/cam<DEVICE_SUB_ID> — mpengine-conf-sync appends
# the port automatically via @@IP@@ substitution (bRtspPort if bType!=0, else bPort).
# bType=1 selects the RTSP branch, so bRtspPort=8554 wins.
# Schema (src/schema/dtech.sql): bID, sName, fLocal, bNumChannel, bNumDevice,
# bNumInput, bNumAudio, bType, sPath, sResolution, bPort, bRtspPort, fFisheye.
mysql_exec <<'SQL'
INSERT INTO DeviceType
  (bID, sName, fLocal, bNumChannel, bNumDevice, bNumInput, bNumAudio, bType,
   sPath, sResolution, bPort, bRtspPort, fFisheye)
VALUES
  (200, 'Testcam', 0, 1, 1, 4, 0, 1,
   'rtsp://@@IP@@/cam@@DEVICE@@', '1920x1080', 80, 8554, 0)
ON DUPLICATE KEY UPDATE sName=VALUES(sName), bType=1, sPath=VALUES(sPath), bRtspPort=8554;

INSERT INTO Device (bID, sName, bType, sIP, bPort, bRtspPort, fInternal)
VALUES (1, 'Testcam', 200, '127.0.0.1', 80, 8554, 1)
ON DUPLICATE KEY UPDATE bType=200, sIP='127.0.0.1', bPort=80, bRtspPort=8554;

UPDATE Camera
   SET fEnable=1, sName='Testcam 1',
       bDeviceID=1, bDeviceSubID=1, bDeviceLiveSubID=1,
       sRecordType='motion', fPos=1, fLPR=1
 WHERE bID=1;

UPDATE Camera
   SET fEnable=1, sName='Testcam 2',
       bDeviceID=1, bDeviceSubID=2, bDeviceLiveSubID=2,
       sRecordType='motion', fPos=1, fLPR=0
 WHERE bID=2;

-- VideoStore (harmless if already present)
INSERT IGNORE INTO VideoStore (bID, sName, sMountPoint) VALUES (1, 'vs1', '/videostore/vs1');
SQL
ok "Device + Camera 1 (overlay) + Camera 2 (capture) configured"

# --- 4. Create PosType 19 (ScaleWatcher) ---

log "[4/7] Creating PosType 19 (ScaleWatcher) entry"

# sImages format: <cam>-<event>-<overlay>, entries separated by ~.
# event must match a scalewatcher.event class name (Ticket_Received, Scale_TruckEntering, etc.).
# overlay is 1 (overlay mpengine) or 0 (plain vcengine snap).
mysql_exec <<SQL
-- Wipe any prior test POS so re-runs are idempotent.
DELETE FROM PosType19 WHERE bID IN (SELECT bID FROM Pos WHERE sName='Test ScaleWatcher');
DELETE FROM Pos WHERE sName='Test ScaleWatcher';

INSERT INTO Pos (sName, bType) VALUES ('Test ScaleWatcher', 19);
SET @pos_id = LAST_INSERT_ID();

INSERT INTO PosType19
  (bID, bTcpPort, bKeep, sFileFormat, fShare, sSharePW, bTicketThreshold,
   sStartTransaction, sFinishTransaction,
   sStartTransactionInbound, sFinishTransactionInbound,
   sImages, sImagesInbound, bTimeoutInbound, sMatchInbound,
   sFileFormatInbound, sCombineFileType,
   bTicketPrintDelay, bMinTransDuration, bHungTransTimeout,
   fTripwireEnabled, bTripwireCamera, bTripwireObjWindow,
   bTripwireTimeout, sTripwireMode)
VALUES
  (@pos_id, $TEST_TICKET_PORT, 10000, 'Tk.%k.%u.D%02r', 0, '', 0,
   'Ticket_Received', 'Ticket_Received',
   'Ticket_Received', 'Ticket_Received',
   '1-Ticket_Received-1~2-Ticket_Received-0',
   '1-Ticket_Received-1~2-Ticket_Received-0',
   60, 'bTicket',
   'Inbound.Alert.%u.%02i', 'pdf',
   0, 0, 60,
   0, 0, 0,
   3, '');
SQL
ok "PosType 19 created (bTcpPort=$TEST_TICKET_PORT, Ticket_Received outbound+inbound, 2 cams)"

# --- 5. Signal connector to reload (restart connector container) ---
#     Also restart engine so mpengine-conf-sync picks up the new Device/Camera rows.

log "[5/7] Reloading connector + engine to pick up new config"
# --force-recreate (not restart) so /var/run/recorder.pid doesn't persist.
# recorder.alreadyRunning() checks /proc/<pidfile_pid>/cmdline — in a container
# restart, the old PID often resolves to the new recorder's own fork, causing
# the new process to see itself as "already running" and abort in a loop.
docker compose ${COMPOSE_FILES[@]+"${COMPOSE_FILES[@]}"} up -d --force-recreate connector engine >/dev/null

# Wait for ThreadTicketReader to bind to the TCP port (up to 60s).
PORT_UP=""
for i in $(seq 1 30); do
    sleep 2
    if docker compose ${COMPOSE_FILES[@]+"${COMPOSE_FILES[@]}"} exec -T connector \
         python3 -c "import socket; s=socket.socket(); s.settimeout(1); s.connect(('127.0.0.1',$TEST_TICKET_PORT)); s.close()" 2>/dev/null; then
        PORT_UP=yes; break
    fi
done
[[ -n "$PORT_UP" ]] || fail "connector did not open TCP $TEST_TICKET_PORT within 60s"

# Wait for mpengine to serve a 200 on cam1.jpg — it takes ~30s to spin up the
# RTSP capture thread and have its first frame ready. Without this, the ticket
# event fires before the engine can hand out snapshots and step 7 finds no files.
ENGINE_UP=""
CODE=""
for i in $(seq 1 30); do
    sleep 2
    # `|| true` prevents `set -e` from aborting on the expected early failures
    # while the engine's mpengine-conf-sync + RTSP capture thread warm up.
    CODE=$(docker compose ${COMPOSE_FILES[@]+"${COMPOSE_FILES[@]}"} exec -T connector \
        python3 -c "import urllib.request; print(urllib.request.urlopen('http://127.0.0.1:43209/cam1.jpg?sess=dtech&overlay=0', timeout=2).status)" 2>/dev/null || true)
    if [[ "$CODE" == "200" ]]; then
        ENGINE_UP=yes; break
    fi
done
[[ -n "$ENGINE_UP" ]] || warn "engine cam1.jpg not 200 after 60s — snapshots may fail"
ok "Connector + engine reloaded; listener on 127.0.0.1:$TEST_TICKET_PORT, cam1.jpg=${CODE:-?}"

# --- 6. Send fake Ticket_Received XML over TCP ---

log "[6/7] Sending fake <ticketentry> XML to 127.0.0.1:$TEST_TICKET_PORT"

POS_ID=$(mysql_exec -sN -e "SELECT bID FROM Pos WHERE sName='Test ScaleWatcher'")
[[ -n "$POS_ID" ]] || fail "Could not resolve POS bID"

# ThreadTicketReader expects a <ticketentry> block — ticket + uniqueid are required.
# Reader closes the socket on seeing </ticketentry>, so no further framing needed.
TICKET_XML=$(cat <<XML
<ticketentry>
<ticket>$TEST_TICKET_NUM</ticket>
<uniqueid>$TEST_TICKET_UNIQUE</uniqueid>
<autoid>TESTCAM</autoid>
<scale>1</scale>
<gross>5000 lb</gross>
<tare>1000 lb</tare>
<net>4000 lb</net>
</ticketentry>
XML
)

docker compose ${COMPOSE_FILES[@]+"${COMPOSE_FILES[@]}"} exec -T connector \
    python3 -c "
import socket, sys
xml = sys.stdin.read()
s = socket.socket(); s.settimeout(5)
s.connect(('127.0.0.1', $TEST_TICKET_PORT))
s.sendall(xml.encode('utf-8'))
s.shutdown(socket.SHUT_WR)
try: s.recv(1024)
except Exception: pass
s.close()
" <<< "$TICKET_XML" || fail "Failed to send ticket XML"

ok "Ticket XML sent (bTicket=$TEST_TICKET_NUM, uniqueid=$TEST_TICKET_UNIQUE)"

# --- 7. Verify artifacts in tickets folder ---

log "[7/7] Verifying ticket artifacts in VideoStore"

# Tickets dir: /videostore/vs1/dividia/tickets on Linux (recorder.globals.GB['ticketsdir']).
# recorder wraps PDF generation around jpg snapshots, so we expect both a .pdf
# and one .jpg per camera. Poll up to 30s — vcengine snapshot + jpg2pdf can be slow.
TICKETS_DIR="/videostore/vs1/dividia/tickets"
FOUND=""
for i in $(seq 1 15); do
    sleep 2
    FOUND=$(docker compose ${COMPOSE_FILES[@]+"${COMPOSE_FILES[@]}"} exec -T connector \
        find "$TICKETS_DIR" -type f \( -name "*${TEST_TICKET_NUM}*" -o -name "*.pdf" -o -name "*.jpg" \) -mmin -5 2>/dev/null | head -20 || true)
    if [[ -n "$FOUND" ]]; then
        break
    fi
done

if [[ -z "$FOUND" ]]; then
    warn "No ticket artifacts in $TICKETS_DIR after 30s"
    warn "Debug: connector logs, tickets dir listing"
    docker compose ${COMPOSE_FILES[@]+"${COMPOSE_FILES[@]}"} logs --tail=80 connector 2>&1 | tail -80 | sed 's/^/    /' || true
    docker compose ${COMPOSE_FILES[@]+"${COMPOSE_FILES[@]}"} exec -T connector ls -la "$TICKETS_DIR" 2>/dev/null || true
    fail "Verification failed — no files found"
fi

echo "$FOUND" | sed 's/^/    /'
ok "Ticket artifacts present"

log "=== test-deploy.sh PASSED ==="
echo
echo "Cleanup when done:  ./test-deploy.sh --clean"
