#!/bin/bash
# Gate Gap API — the device report path.
#
# Checks the sealed capsule, the upsert that must not blank known values, the
# add-only attribution store, the outward forward (which must use the fixed
# analytics names, not this app's private ones), the analytics-service header
# and the account link.
#
#   php -S 127.0.0.1:8788 api/tests/mock-forward.php &
#   ./tests/signal.sh http://127.0.0.1:8787

set -uo pipefail
B="${1:-http://127.0.0.1:8787}"
DB="${GG_TEST_DB:-gategap}"
FORWARD_LOG="${TMPDIR:-/tmp}/gategap-forward.log"
PASS=0
FAIL=0

check() {
  if [ "$2" == "$3" ]; then PASS=$((PASS+1)); printf '  ok   %s\n' "$1"
  else FAIL=$((FAIL+1)); printf '  FAIL %s (expected %s, got %s)\n' "$1" "$2" "$3"; fi
}

j() {
  php -r '$d=json_decode(stream_get_contents(STDIN),true);
          foreach(explode(".",$argv[1]) as $p){
            if(!is_array($d)||!array_key_exists($p,$d)){echo ""; exit;} $d=$d[$p];
          }
          echo is_scalar($d)?var_export($d,true):json_encode($d);' "$1" | sed "s/^'//;s/'$//"
}

# Seals a JSON document with the same key the server holds, in the same layout
# the app uses: nonce, tag, ciphertext, base64.
seal() {
  php -r '
    $key = base64_decode(trim(preg_replace("/^GG_SIGNAL_KEY=/", "", shell_exec("grep ^GG_SIGNAL_KEY= .env"))), true);
    $nonce = random_bytes(12);
    $tag = "";
    $c = openssl_encrypt($argv[1], "aes-256-gcm", $key, OPENSSL_RAW_DATA, $nonce, $tag, "", 16);
    echo json_encode(["capsule" => base64_encode($nonce . $tag . $c)]);
  ' "$1"
}

post() { curl -s -X POST "$B$1" -H 'Content-Type: application/json' -d "$2"; }
posth() { curl -s -D - -o /dev/null -X POST "$B$1" -H 'Content-Type: application/json' -d "$2"; }

mysql -u root "$DB" -e "TRUNCATE rate_limits;" >/dev/null 2>&1
: > "$FORWARD_LOG"

TRACE="1786500000000-$RANDOM$RANDOM"

echo "== the capsule is required"
check "a plain body is refused when a key is set" "400" \
  "$(curl -s -o /dev/null -w '%{http_code}' -X POST $B/v1/handshake -H 'Content-Type: application/json' -d "{\"trace\":\"$TRACE\"}")"
check "a corrupt capsule is refused" "400" \
  "$(curl -s -o /dev/null -w '%{http_code}' -X POST $B/v1/handshake -H 'Content-Type: application/json' -d '{"capsule":"bm90LWEtY2Fwc3VsZQ=="}')"
check "a capsule with no trace is refused" "422" \
  "$(curl -s -o /dev/null -w '%{http_code}' -X POST $B/v1/handshake -H 'Content-Type: application/json' -d "$(seal '{"lane":"iOS"}')")"

echo "== handshake"
OPEN=$(seal "$(printf '{"trace":"%s","lane":"iOS 18.5","crate":"com.GateGap","berth":"6811234908","region":"en_GB","beacon":"1234567890"}' "$TRACE")")
R=$(post /v1/handshake "$OPEN")
check "handshake accepted" "true" "$(echo "$R" | j ack)"
check "the open was counted" "1" "$(echo "$R" | j opens)"
check "a second open increments" "2" "$(post /v1/handshake "$OPEN" | j opens)"

echo "== the row is stored under this app's own column names"
ROW=$(mysql -u root "$DB" -N -B -e "SELECT os_line, bundle, store_ref, region_tag, beacon_project FROM handsets WHERE trace_key='$TRACE';" 2>/dev/null)
check "device fields landed" "iOS 18.5	com.GateGap	6811234908	en_GB	1234567890" "$ROW"

echo "== manifest: attribution, forward, header"
FULL=$(seal "$(printf '{"trace":"%s","lane":"iOS 18.5","crate":"com.GateGap","berth":"6811234908","region":"en_GB","beacon":"1234567890","chime":"fcm-token-abc","mark":"6D92078A-8246-4BA4-AE5B-76104861E7DC","ledger":{"af_status":"Non-organic","media_source":"test_source","campaign":"spring","is_first_launch":true}}' "$TRACE")")
HEADERS=$(posth /v1/manifest "$FULL")
check "manifest answered 200" "1" "$(echo "$HEADERS" | grep -c 'HTTP/1.1 200')"
check "the analytics-service header is set" "1" "$(echo "$HEADERS" | grep -ci '^analytics-service: https://example.com/route/')"

BODY=$(post /v1/manifest "$FULL")
check "settled" "true" "$(echo "$BODY" | j settled)"
check "not authorised yet" "false" "$(echo "$BODY" | j authorized)"
check "manifest does not count as an open" "2" "$(echo "$BODY" | j opens)"

echo "== what actually left the server"
LAST=$(tail -1 "$FORWARD_LOG")
check "outward uses af_id, not trace" "$TRACE" "$(echo "$LAST" | j af_id)"
check "outward uses push_token, not chime" "fcm-token-abc" "$(echo "$LAST" | j push_token)"
check "outward uses bundle_id, not crate" "com.GateGap" "$(echo "$LAST" | j bundle_id)"
check "outward uses store_id, not berth" "6811234908" "$(echo "$LAST" | j store_id)"
check "outward uses idfa, not mark" "6D92078A-8246-4BA4-AE5B-76104861E7DC" "$(echo "$LAST" | j idfa)"
check "the conversion goes whole" "test_source" "$(echo "$LAST" | j conversion.media_source)"
check "source_ip is added by the server" "127.0.0.1" "$(echo "$LAST" | j source_ip)"
check "this app's private names never leave" "0" "$(echo "$LAST" | grep -cE '"(trace|crate|berth|chime|mark|ledger)"')"

echo "== the conversion keeps its types and its shape"
TYPED_TRACE="typed-$RANDOM"
TYPED=$(seal "$(printf '{"trace":"%s","ledger":{"af_status":"Non-organic","is_first_launch":true,"retargeting_conversion_type":"none","install_time":1786500000,"cost":1.75,"deep_link_value":{"screen":"trip","id":42},"tags":["a","b"]}}' "$TYPED_TRACE")")
post /v1/manifest "$TYPED" > /dev/null
LASTT=$(tail -1 "$FORWARD_LOG")
check "a bool stays a bool" "true" "$(echo "$LASTT" | php -r '$d=json_decode(stream_get_contents(STDIN),true); var_export($d["conversion"]["is_first_launch"] === true);')"
check "an integer stays an integer" "true" "$(echo "$LASTT" | php -r '$d=json_decode(stream_get_contents(STDIN),true); var_export(is_int($d["conversion"]["install_time"]));')"
check "a decimal stays a decimal" "true" "$(echo "$LASTT" | php -r '$d=json_decode(stream_get_contents(STDIN),true); var_export(is_float($d["conversion"]["cost"]));')"
check "a nested object keeps its shape" "42" "$(echo "$LASTT" | j conversion.deep_link_value.id)"
check "an array stays an array" '["a","b"]' "$(echo "$LASTT" | j conversion.tags)"
check "nothing was flattened to a description" "0" "$(echo "$LASTT" | grep -cE '\\[\"?[a-z_]+\"?: ')"

echo "== the attribution is kept whole and deduplicated"
check "one signal row for two identical reports" "1" \
  "$(mysql -u root "$DB" -N -B -e "SELECT COUNT(*) FROM handset_signals WHERE trace_key='$TRACE';" 2>/dev/null)"
check "the digest is on the device row" "Non-organic	test_source	spring" \
  "$(mysql -u root "$DB" -N -B -e "SELECT attr_status, attr_source, attr_campaign FROM handsets WHERE trace_key='$TRACE';" 2>/dev/null)"
check "the forward was logged" "1" \
  "$(mysql -u root "$DB" -N -B -e "SELECT accepted FROM handset_forwards WHERE trace_key='$TRACE' ORDER BY id DESC LIMIT 1;" 2>/dev/null)"

echo "== an empty value never blanks a known one"
THIN=$(seal "$(printf '{"trace":"%s","lane":"iOS 18.5"}' "$TRACE")")
post /v1/handshake "$THIN" > /dev/null
check "the push token survived a report without it" "fcm-token-abc" \
  "$(mysql -u root "$DB" -N -B -e "SELECT chime_token FROM handsets WHERE trace_key='$TRACE';" 2>/dev/null)"
check "the idfa survived too" "6D92078A-8246-4BA4-AE5B-76104861E7DC" \
  "$(mysql -u root "$DB" -N -B -e "SELECT ad_mark FROM handsets WHERE trace_key='$TRACE';" 2>/dev/null)"

echo "== a refused advertising id is not stored"
ZERO_TRACE="zero-$RANDOM"
post /v1/handshake "$(seal "$(printf '{"trace":"%s","mark":"00000000-0000-0000-0000-000000000000"}' "$ZERO_TRACE")")" > /dev/null
check "all-zero idfa dropped" "" \
  "$(mysql -u root "$DB" -N -B -e "SELECT ad_mark FROM handsets WHERE trace_key='$ZERO_TRACE';" 2>/dev/null)"

echo "== organic gets no route"
ORG_TRACE="organic-$RANDOM"
ORG=$(seal "$(printf '{"trace":"%s","ledger":{"af_status":"Organic"}}' "$ORG_TRACE")")
check "no analytics-service header for organic" "0" "$(posth /v1/manifest "$ORG" | grep -ci '^analytics-service:')"

echo "== the account link"
STAMP=$(date +%s)-$RANDOM
REG=$(post /v1/auth/register "$(printf '{"email":"signal-%s@example.com","password":"a long enough passphrase"}' "$STAMP")")
A=$(echo "$REG" | j tokens.access_token)
check "link needs a token" "401" \
  "$(curl -s -o /dev/null -w '%{http_code}' -X POST $B/v1/me/handset -H 'Content-Type: application/json' -d "$(seal "$(printf '{"trace":"%s"}' "$TRACE")")")"
LINK=$(curl -s -X POST $B/v1/me/handset -H 'Content-Type: application/json' -H "Authorization: Bearer $A" -d "$(seal "$(printf '{"trace":"%s"}' "$TRACE")")")
check "linked" "true" "$(echo "$LINK" | j linked)"
check "the manifest now reports the device as known" "true" "$(post /v1/manifest "$FULL" | j authorized)"
check "an unknown trace links to nothing, without failing" "false" \
  "$(curl -s -X POST $B/v1/me/handset -H 'Content-Type: application/json' -H "Authorization: Bearer $A" -d "$(seal '{"trace":"never-reported"}')" | j linked)"

echo
echo "passed: $PASS   failed: $FAIL"
[ "$FAIL" -eq 0 ]
