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

BASE="https://petshop-backend.nicepond-c7280126.westus2.azurecontainerapps.io/api/v1"
PASS=0 FAIL=0
CLEANUP_PRODUCT_IDS=()
CLEANUP_SALE_IDS=()
CLEANUP_COUPON_IDS=()

check() {
  local label="$1" expect="$2" method="$3" path="$4"; shift 4
  local code=$(curl -s -o /tmp/qa_body.json -w "%{http_code}" -X "$method" "$BASE$path" "$@" 2>/dev/null)
  if [ "$code" = "$expect" ]; then PASS=$((PASS+1))
  else FAIL=$((FAIL+1)); echo "FAIL: $label — expected $expect got $code"; fi
}
check_field() {
  local actual=$(jq -r "$2" /tmp/qa_body.json 2>/dev/null)
  if [ "$actual" = "$3" ]; then PASS=$((PASS+1))
  else FAIL=$((FAIL+1)); echo "FAIL: $1 — expected '$3' got '$actual'"; fi
}
check_not_500() {
  local label="$1" method="$2" path="$3"; shift 3
  local code=$(curl -s -o /tmp/qa_body.json -w "%{http_code}" -X "$method" "$BASE$path" "$@" 2>/dev/null)
  if [ "$code" != "500" ]; then PASS=$((PASS+1))
  else FAIL=$((FAIL+1)); echo "FAIL: $label — got 500 Internal Server Error"; fi
}
check_true() {
  local label="$1" expr="$2"
  local result=$(jq "$expr" /tmp/qa_body.json 2>/dev/null)
  if [ "$result" = "true" ]; then PASS=$((PASS+1))
  else FAIL=$((FAIL+1)); echo "FAIL: $label"; fi
}

echo "=== Logging in ==="
ADMIN_TOKEN=$(curl -s -X POST "$BASE/auth/login" -H 'Content-Type: application/json' -d '{"username":"admin","password":"admin123"}' | jq -r '.token')
STAFF_TOKEN=$(curl -s -X POST "$BASE/auth/login" -H 'Content-Type: application/json' -d '{"username":"staff","password":"staff123"}' | jq -r '.token')
CUST_TOKEN=$(curl -s -X POST "$BASE/auth/login" -H 'Content-Type: application/json' -d '{"username":"customer","password":"customer123"}' | jq -r '.token')

if [ "$ADMIN_TOKEN" = "null" ] || [ -z "$ADMIN_TOKEN" ]; then echo "FATAL: Admin login failed"; exit 1; fi
if [ "$STAFF_TOKEN" = "null" ] || [ -z "$STAFF_TOKEN" ]; then echo "FATAL: Staff login failed"; exit 1; fi
if [ "$CUST_TOKEN" = "null" ] || [ -z "$CUST_TOKEN" ]; then echo "FATAL: Customer login failed"; exit 1; fi
echo "All 3 logins OK"

A=(-H "Authorization: Bearer $ADMIN_TOKEN")
S=(-H "Authorization: Bearer $STAFF_TOKEN")
C=(-H "Authorization: Bearer $CUST_TOKEN")
J=(-H "Content-Type: application/json")

cleanup() {
  echo ""
  echo "=== Cleanup ==="
  for pid in "${CLEANUP_PRODUCT_IDS[@]}"; do
    curl -s -o /dev/null -X DELETE "$BASE/products/$pid" "${A[@]}" 2>/dev/null
  done
  for sid in "${CLEANUP_SALE_IDS[@]}"; do
    curl -s -o /dev/null -X DELETE "$BASE/sales/$sid" "${A[@]}" 2>/dev/null
  done
  for cid in "${CLEANUP_COUPON_IDS[@]}"; do
    curl -s -o /dev/null -X DELETE "$BASE/coupons/$cid" "${A[@]}" 2>/dev/null
  done
  echo "Cleanup done"
}
trap cleanup EXIT

###############################################################################
echo ""
echo "========== C — CONFORMANCE =========="
###############################################################################

echo "--- Response format validation ---"
check "GET /products 200" 200 GET "/products"
check_true "products .content is array" '.content | type == "array"'
check_true "products has totalElements" '.totalElements != null'
check_true "products has totalPages" '.totalPages != null'
check_true "products has number" '.number != null'
check_true "products has size" '.size != null'

check "GET /products/1 200" 200 GET "/products/1"
check_true "prodPrice is number ≤2 decimals" '(.prodPrice | tostring | test("^[0-9]+(\\.[0-9]{1,2})?$"))'

check "GET /sales admin 200" 200 GET "/sales" "${A[@]}" "${J[@]}"
check_true "saleDate has ISO T" '(.content[0].saleDate | test("T"))'

check "GET /appointments admin 200" 200 GET "/appointments" "${A[@]}" "${J[@]}"
check_true "appointmentDate YYYY-MM-DD" '(.content[0].appointmentDate | test("^[0-9]{4}-[0-9]{2}-[0-9]{2}"))'

local_code=$(curl -s -o /tmp/qa_body.json -w "%{http_code}" -D /tmp/qa_headers.txt -X GET "$BASE/auth/me" "${A[@]}" 2>/dev/null)
if grep -qi "application/json" /tmp/qa_headers.txt 2>/dev/null; then PASS=$((PASS+1))
else FAIL=$((FAIL+1)); echo "FAIL: /auth/me Content-Type not json"; fi

local_code=$(curl -s -o /tmp/qa_body.json -w "%{http_code}" -D /tmp/qa_headers.txt -X GET "$BASE/users/1/avatar/file" "${A[@]}" 2>/dev/null)
if grep -qi "image/" /tmp/qa_headers.txt 2>/dev/null; then PASS=$((PASS+1))
else FAIL=$((FAIL+1)); echo "FAIL: avatar Content-Type not image/"; fi

echo "--- Status enum validation ---"
check "GET /appointments enum" 200 GET "/appointments?size=50" "${A[@]}"
check_true "appointmentStatus valid" '[.content[].appointmentStatus] | all(. == "Scheduled" or . == "Completed" or . == "Cancelled" or . == "In Progress" or . == "No Show")'

check "GET /pets enum" 200 GET "/pets?size=100"
check_true "petStatus valid" '[.content[].petStatus] | all(. == "Available" or . == "Pending" or . == "Adopted" or . == "Owned")'

check "GET /sales enum" 200 GET "/sales?size=50" "${A[@]}"
check_true "channel valid" '[.content[].channel] | all(. == "IN_STORE" or . == "ONLINE")'

check "GET /users enum" 200 GET "/users?size=100" "${A[@]}"
check_true "role valid" '[.content[].role] | all(. == "ADMIN" or . == "STAFF" or . == "CUSTOMER")'

check "GET /coupons enum" 200 GET "/coupons" "${A[@]}"
check_true "discountType valid" '[.[].discountType] | all(. == "PERCENTAGE" or . == "FIXED")'

echo "--- Error format validation ---"
check "GET /products/999999 404" 404 GET "/products/999999"
check_true "404 has status" '.status != null'
check_true "404 has message" '.message != null'
check_true "404 has path" '.path != null'
check_true "404 has timestamp" '.timestamp != null'

echo "--- Input format enforcement ---"
check_not_500 "invalid JSON body" POST "/products" "${A[@]}" "${J[@]}" -d '{bad json}'
check_not_500 "text/plain content-type" POST "/products" "${A[@]}" -H "Content-Type: text/plain" -d 'hello'
check_not_500 "empty body" POST "/products" "${A[@]}" "${J[@]}" -d ''
check_not_500 "XML body" POST "/products" "${A[@]}" -H "Content-Type: application/xml" -d '<xml/>'

echo "--- Security conformance ---"
check_not_500 "SQL injection in search" GET "/products?q=%27%20OR%201%3D1--"
check_not_500 "XSS in search" GET '/products?q=<script>alert(1)</script>'

check_not_500 "XSS in prodName" POST "/products" "${A[@]}" "${J[@]}" -d '{"prodName":"<img src=x onerror=alert(1)>","prodPrice":9.99,"categoryId":1}'
XSS_ID=$(jq -r '.prodId // empty' /tmp/qa_body.json 2>/dev/null)
if [ -n "$XSS_ID" ]; then
  CLEANUP_PRODUCT_IDS+=("$XSS_ID")
  check "GET XSS product" 200 GET "/products/$XSS_ID"
  check_true "XSS stored literally" '(.prodName | contains("<img"))'
fi

LONG_NAME=$(printf 'A%.0s' $(seq 1 5000))
check "5000 char prodName → 400" 400 POST "/products" "${A[@]}" "${J[@]}" -d "{\"prodName\":\"$LONG_NAME\",\"prodPrice\":9.99,\"categoryId\":1}"

check_not_500 "unicode prodName" POST "/products" "${A[@]}" "${J[@]}" -d '{"prodName":"Café Résumé","prodPrice":9.99,"categoryId":1}'
UNI_ID=$(jq -r '.prodId // empty' /tmp/qa_body.json 2>/dev/null)
if [ -n "$UNI_ID" ]; then
  CLEANUP_PRODUCT_IDS+=("$UNI_ID")
  check "GET unicode product" 200 GET "/products/$UNI_ID"
  check_field "unicode preserved" '.prodName' 'Café Résumé'
fi

check_not_500 "null byte in category" POST "/categories" "${A[@]}" "${J[@]}" -d '{"categoryName":"test\u0000inject"}'

###############################################################################
echo ""
echo "========== O — ORDERING =========="
###############################################################################

check "products price asc" 200 GET "/products?sort=prodPrice,asc&size=100"
check_true "prices non-decreasing" '[.content[].prodPrice] | . as $a | [range(1;length)] | all(. as $i | $a[$i] >= $a[$i-1])'

check "products price desc" 200 GET "/products?sort=prodPrice,desc&size=100"
check_true "prices non-increasing" '[.content[].prodPrice] | . as $a | [range(1;length)] | all(. as $i | $a[$i] <= $a[$i-1])'

check "products name asc" 200 GET "/products?sort=prodName,asc&size=100"
check_true "names alphabetical" '[.content[].prodName] | . as $a | [range(1;length)] | all(. as $i | $a[$i] >= $a[$i-1])'

check "sales date desc" 200 GET "/sales?sort=saleDate,desc&size=50" "${A[@]}"
check_true "sale dates non-increasing" '[.content[].saleDate] | . as $a | [range(1;length)] | all(. as $i | $a[$i] <= $a[$i-1])'

check "users lastName asc" 200 GET "/users?sort=lastName,asc&size=100" "${A[@]}"
check_true "lastNames alphabetical" '[.content[].lastName] | . as $a | [range(1;length)] | all(. as $i | $a[$i] >= $a[$i-1])'

check_not_500 "sort by nonexistent field" GET "/products?sort=nonexistent,asc"

check "page 0 size 5" 200 GET "/products?page=0&size=5"
PAGE0_IDS=$(jq -r '[.content[].prodId] | join(",")' /tmp/qa_body.json)
check "page 1 size 5" 200 GET "/products?page=1&size=5"
PAGE1_IDS=$(jq -r '[.content[].prodId] | join(",")' /tmp/qa_body.json)
OVERLAP=0
IFS=',' read -ra P0 <<< "$PAGE0_IDS"
IFS=',' read -ra P1 <<< "$PAGE1_IDS"
for a in "${P0[@]}"; do for b in "${P1[@]}"; do [ "$a" = "$b" ] && OVERLAP=1; done; done
if [ "$OVERLAP" = "0" ]; then PASS=$((PASS+1))
else FAIL=$((FAIL+1)); echo "FAIL: page 0 and page 1 overlap"; fi

check "conversations" 200 GET "/chat/conversations" "${C[@]}"
CONV_ID=$(jq -r '.[0].conversationId // empty' /tmp/qa_body.json 2>/dev/null)
if [ -n "$CONV_ID" ]; then
  check "conv messages" 200 GET "/chat/conversations/$CONV_ID/messages" "${C[@]}"
  check_true "message timestamps ascending" '. as $a | [range(1;length)] | all(. as $i | $a[$i].timestamp >= $a[$i-1].timestamp)'
fi

check "activity logs desc" 200 GET "/activity-logs?limit=10" "${A[@]}"
check_true "log timestamps descending" '. as $a | [range(1;length)] | all(. as $i | $a[$i].logTimestamp <= $a[$i-1].logTimestamp)'

check "availability times" 200 GET "/appointments/availability?storeId=1&serviceId=1&date=2027-06-01" "${A[@]}"
check_true "times ascending" '. as $a | [range(1;length)] | all(. as $i | $a[$i] >= $a[$i-1])'

###############################################################################
echo ""
echo "========== R — RANGE =========="
###############################################################################

echo "--- Data range checks ---"
check "products range" 200 GET "/products?size=200"
check_true "all prodPrice > 0" '[.content[].prodPrice] | all(. > 0)'

check "services range" 200 GET "/services"
check_true "all servicePrice > 0" '[.[].servicePrice] | all(. > 0)'
check_true "all serviceDuration > 0" '[.[].serviceDuration] | all(. > 0)'

check "inventory range" 200 GET "/inventory" "${A[@]}"
check_true "all quantity >= 0" '[.content[].quantity] | all(. >= 0)'

check "users loyalty" 200 GET "/users?size=100" "${A[@]}"
check_true "all loyaltyPoints >= 0" '[.content[].loyaltyPoints] | all(. >= 0)'

check "coupons range" 200 GET "/coupons" "${A[@]}"
check_true "PERCENTAGE < 100 and > 0" '[.[] | select(.discountType == "PERCENTAGE") | .discountValue] | all(. > 0 and . < 100)'

check "pets age range" 200 GET "/pets?size=200"
check_true "all petAge >= 0" '[.content[].petAge | select(. != null)] | all(. >= 0)'

echo "--- Input range enforcement ---"
check "negative price → 400" 400 POST "/products" "${A[@]}" "${J[@]}" -d '{"prodName":"QA_NEG","prodPrice":-1,"categoryId":1}'
NEG_ID=$(jq -r '.prodId // empty' /tmp/qa_body.json 2>/dev/null)
[ -n "$NEG_ID" ] && CLEANUP_PRODUCT_IDS+=("$NEG_ID")

check "zero price → 400" 400 POST "/products" "${A[@]}" "${J[@]}" -d '{"prodName":"QA_ZERO","prodPrice":0,"categoryId":1}'
Z_ID=$(jq -r '.prodId // empty' /tmp/qa_body.json 2>/dev/null)
[ -n "$Z_ID" ] && CLEANUP_PRODUCT_IDS+=("$Z_ID")

check "zero duration → 400" 400 POST "/services" "${A[@]}" "${J[@]}" -d '{"serviceName":"QA_DUR0","servicePrice":10,"serviceDuration":0}'
check "neg duration → 400" 400 POST "/services" "${A[@]}" "${J[@]}" -d '{"serviceName":"QA_DURN","servicePrice":10,"serviceDuration":-5}'

check "neg cart qty → 400" 400 POST "/cart/add" "${C[@]}" "${J[@]}" -d '{"prodId":1,"storeId":1,"quantity":-1}'

check "zero coupon pct → 400" 400 POST "/coupons" "${A[@]}" "${J[@]}" -d '{"couponCode":"QA_ZERO_PCT","discountType":"PERCENTAGE","discountValue":0}'

check_not_500 "huge price" POST "/products" "${A[@]}" "${J[@]}" -d '{"prodName":"QA_HUGE","prodPrice":99999999.99,"categoryId":1}'
HUGE_ID=$(jq -r '.prodId // empty' /tmp/qa_body.json 2>/dev/null)
[ -n "$HUGE_ID" ] && CLEANUP_PRODUCT_IDS+=("$HUGE_ID")

echo "--- Extreme pagination ---"
check_not_500 "page=-1" GET "/products?page=-1"
check_not_500 "size=0" GET "/products?size=0"
check_not_500 "size=10000" GET "/products?size=10000"

echo "--- String length ---"
LONG_USER=$(printf 'u%.0s' $(seq 1 60))
check "60-char username → 400" 400 POST "/auth/register" "${J[@]}" -d "{\"username\":\"$LONG_USER\",\"password\":\"pass1234\",\"firstName\":\"Q\",\"lastName\":\"A\",\"email\":\"${LONG_USER}@test.com\"}"

LONG_PNAME=$(printf 'p%.0s' $(seq 1 200))
check "200-char prodName → 400" 400 POST "/products" "${A[@]}" "${J[@]}" -d "{\"prodName\":\"$LONG_PNAME\",\"prodPrice\":9.99,\"categoryId\":1}"

###############################################################################
echo ""
echo "========== R — REFERENCE =========="
###############################################################################

echo "--- FK integrity: sales ---"
check "sales sample" 200 GET "/sales?size=3" "${A[@]}"
for i in 0 1 2; do
  SID=$(jq -r ".content[$i].storeId // empty" /tmp/qa_body.json)
  EID=$(jq -r ".content[$i].employeeId // empty" /tmp/qa_body.json)
  [ -n "$SID" ] && check "sale[$i] store $SID exists" 200 GET "/stores/$SID"
  [ -n "$EID" ] && check "sale[$i] employee $EID exists" 200 GET "/users/$EID" "${A[@]}"
done

echo "--- FK integrity: appointments ---"
check "appts sample" 200 GET "/appointments?size=3" "${A[@]}"
cp /tmp/qa_body.json /tmp/qa_appts.json
for i in 0 1 2; do
  SVID=$(jq -r ".content[$i].serviceId // empty" /tmp/qa_appts.json)
  STID=$(jq -r ".content[$i].storeId // empty" /tmp/qa_appts.json)
  CID=$(jq -r ".content[$i].customerId // empty" /tmp/qa_appts.json)
  EMID=$(jq -r ".content[$i].employeeId // empty" /tmp/qa_appts.json)
  [ -n "$SVID" ] && check "appt[$i] service $SVID" 200 GET "/services/$SVID"
  [ -n "$STID" ] && check "appt[$i] store $STID" 200 GET "/stores/$STID"
  [ -n "$CID" ] && check "appt[$i] customer $CID" 200 GET "/users/$CID" "${A[@]}"
  [ -n "$EMID" ] && check "appt[$i] employee $EMID" 200 GET "/users/$EMID" "${A[@]}"
done

echo "--- FK integrity: inventory ---"
check "inv sample" 200 GET "/inventory?size=3" "${A[@]}"
cp /tmp/qa_body.json /tmp/qa_inv.json
for i in 0 1 2; do
  ISTID=$(jq -r ".content[$i].storeId // empty" /tmp/qa_inv.json)
  IPID=$(jq -r ".content[$i].prodId // empty" /tmp/qa_inv.json)
  [ -n "$ISTID" ] && check "inv[$i] store $ISTID" 200 GET "/stores/$ISTID"
  [ -n "$IPID" ] && check "inv[$i] product $IPID" 200 GET "/products/$IPID"
done

echo "--- FK integrity: products ---"
check "products sample" 200 GET "/products?size=3"
cp /tmp/qa_body.json /tmp/qa_prods.json
for i in 0 1 2; do
  CATID=$(jq -r ".content[$i].categoryId // empty" /tmp/qa_prods.json)
  [ -n "$CATID" ] && check "prod[$i] category $CATID" 200 GET "/categories/$CATID"
done

echo "--- Cross-ref violations ---"
check "sale bad storeId" 404 POST "/sales" "${S[@]}" "${J[@]}" -d '{"storeId":999,"paymentMethod":"Cash","channel":"IN_STORE","items":[{"prodId":1,"quantity":1}]}'
check "appt bad serviceId" 404 POST "/appointments" "${A[@]}" "${J[@]}" -d '{"serviceId":999,"petId":53,"customerId":32,"storeId":2,"employeeId":7,"appointmentDate":"2027-09-01","appointmentTime":"10:00","appointmentStatus":"Scheduled"}'
check "appt bad customerId" 404 POST "/appointments" "${A[@]}" "${J[@]}" -d '{"serviceId":1,"petId":53,"customerId":999,"storeId":2,"employeeId":7,"appointmentDate":"2027-09-01","appointmentTime":"10:00","appointmentStatus":"Scheduled"}'
check "appt bad employeeId" 404 POST "/appointments" "${A[@]}" "${J[@]}" -d '{"serviceId":1,"petId":53,"customerId":32,"storeId":2,"employeeId":999,"appointmentDate":"2027-09-01","appointmentTime":"10:00","appointmentStatus":"Scheduled"}'

echo "--- Dangling reference (FK on delete) ---"
DEL_CAT_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE "$BASE/categories/1" "${A[@]}" 2>/dev/null)
if [ "$DEL_CAT_CODE" != "204" ] && [ "$DEL_CAT_CODE" != "200" ]; then PASS=$((PASS+1))
else FAIL=$((FAIL+1)); echo "FAIL: DELETE /categories/1 should fail (FK) — got $DEL_CAT_CODE"; fi

DEL_STORE_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE "$BASE/stores/1" "${A[@]}" 2>/dev/null)
if [ "$DEL_STORE_CODE" != "204" ] && [ "$DEL_STORE_CODE" != "200" ]; then PASS=$((PASS+1))
else FAIL=$((FAIL+1)); echo "FAIL: DELETE /stores/1 should fail (FK) — got $DEL_STORE_CODE"; fi

echo "--- Avatar file references ---"
check "admin avatar file" 200 GET "/users/1/avatar/file" "${A[@]}"
check "user 16 avatar file" 200 GET "/users/16/avatar/file" "${A[@]}"
check "product 1 image" 200 GET "/products/1/image"

###############################################################################
echo ""
echo "========== E — EXISTENCE =========="
###############################################################################

echo "--- Required fields never null ---"
check "products fields" 200 GET "/products?size=100"
check_true "products non-null fields" '.content | all(.prodId != null and .prodName != null and .prodPrice != null and .categoryId != null)'

check "users fields" 200 GET "/users?size=100" "${A[@]}"
check_true "users non-null fields" '.content | all(.id != null and .firstName != null and .lastName != null and .role != null)'

check "sales fields" 200 GET "/sales?size=50" "${A[@]}"
check_true "sales non-null fields" '.content | all(.saleId != null and .saleDate != null and .totalAmount != null and .storeId != null and .employeeId != null)'

check "appts fields" 200 GET "/appointments?size=50" "${A[@]}"
check_true "appts non-null fields" '.content | all(.appointmentId != null and .serviceId != null and .customerId != null and .storeId != null and .appointmentDate != null and .appointmentTime != null and .appointmentStatus != null)'

check "coupons fields" 200 GET "/coupons" "${A[@]}"
check_true "coupons non-null fields" '[.[] | .couponId != null and .couponCode != null and .discountType != null and .discountValue != null] | all'

echo "--- Avatar existence for all roles ---"
check "admin me avatar" 200 GET "/auth/me/avatar/file" "${A[@]}"
check "staff me avatar" 200 GET "/auth/me/avatar/file" "${S[@]}"
check "customer me avatar" 200 GET "/auth/me/avatar/file" "${C[@]}"

echo "--- Empty search returns valid structure ---"
check "empty product search" 200 GET "/products?q=zzzznothing"
check_true "empty search .content is array" '.content | type == "array"'
check_true "empty search totalElements 0" '.totalElements == 0'

check "empty pet species" 200 GET "/pets?species=Unicorn"
check_true "unicorn totalElements 0" '.totalElements == 0'

check "future appts" 200 GET "/appointments?date=2099-12-31" "${A[@]}"
check_true "far future totalElements 0" '.totalElements == 0'

check "nonexistent activity log" 200 GET "/activity-logs?search=xyznonexistent" "${A[@]}"
check_true "activity log empty array" 'length == 0'

echo "--- Missing parameter handling ---"
check_not_500 "cart no storeId" GET "/cart" "${C[@]}"
check_not_500 "availability no params" GET "/appointments/availability"
check_not_500 "checkout empty body" POST "/cart/checkout" "${C[@]}" "${J[@]}" -d '{}'

###############################################################################
echo ""
echo "========== C — CARDINALITY =========="
###############################################################################

echo "--- Unique constraints ---"
check "dup username admin" 409 POST "/auth/register" "${J[@]}" -d '{"username":"admin","password":"pass1234","firstName":"Q","lastName":"A","email":"qadup1@test.com"}'
DUP_CODE=$(cat /tmp/qa_body.json | jq -r '.status // empty' 2>/dev/null)
if [ "$?" != "0" ] || [ "$(curl -s -o /dev/null -w "%{http_code}" /dev/null 2>/dev/null)" = "" ]; then true; fi

check "dup email admin" 409 POST "/auth/register" "${J[@]}" -d '{"username":"qauniq99","password":"pass1234","firstName":"Q","lastName":"A","email":"admin@petshop.com"}'

check "dup coupon code" 400 POST "/coupons" "${A[@]}" "${J[@]}" -d '{"couponCode":"WELCOME10","discountType":"PERCENTAGE","discountValue":10}'

echo "--- One-to-many counts ---"
check "sale 1 items" 200 GET "/sales/1" "${A[@]}"
check_true "sale has >= 1 item" '(.items | length) >= 1'

check "service 1 species" 200 GET "/services/1"
check_true "service has >= 1 species" '(.species | length) >= 1'

check "products page size 5" 200 GET "/products?size=5"
check_true "content length <= 5" '(.content | length) <= 5'
check_true "content length = min(5, total)" '(.content | length) == (if .totalElements < 5 then .totalElements else 5 end)'

echo "--- Pagination math ---"
check "pagination check" 200 GET "/products?page=0&size=10"
check_field "page number is 0" '.number' '0'
check_field "page size is 10" '.size' '10'

check "products default page" 200 GET "/products"
TOTAL=$(jq -r '.totalElements' /tmp/qa_body.json)
SIZE=$(jq -r '.size' /tmp/qa_body.json)
PAGES=$(jq -r '.totalPages' /tmp/qa_body.json)
EXPECTED_PAGES=$(( (TOTAL + SIZE - 1) / SIZE ))
if [ "$PAGES" = "$EXPECTED_PAGES" ]; then PASS=$((PASS+1))
else FAIL=$((FAIL+1)); echo "FAIL: totalPages=$PAGES expected=$EXPECTED_PAGES (total=$TOTAL size=$SIZE)"; fi

check "activity logs limit 3" 200 GET "/activity-logs?limit=3" "${A[@]}"
check_true "logs length <= 3" 'length <= 3'

echo "--- Cart cardinality ---"
curl -s -o /dev/null -X DELETE "$BASE/cart" "${C[@]}" 2>/dev/null
curl -s -o /dev/null -X POST "$BASE/cart/add" "${C[@]}" "${J[@]}" -d '{"prodId":1,"storeId":1,"quantity":1}' 2>/dev/null
check "cart after add 1" 200 GET "/cart?storeId=1" "${C[@]}"
CART_LEN1=$(jq '.items | length' /tmp/qa_body.json 2>/dev/null)

curl -s -o /dev/null -X POST "$BASE/cart/add" "${C[@]}" "${J[@]}" -d '{"prodId":2,"storeId":1,"quantity":1}' 2>/dev/null
check "cart after add 2" 200 GET "/cart?storeId=1" "${C[@]}"
CART_LEN2=$(jq '.items | length' /tmp/qa_body.json 2>/dev/null)

curl -s -o /dev/null -X POST "$BASE/cart/add" "${C[@]}" "${J[@]}" -d '{"prodId":1,"storeId":1,"quantity":1}' 2>/dev/null
check "cart after re-add 1" 200 GET "/cart?storeId=1" "${C[@]}"
CART_LEN3=$(jq '.items | length' /tmp/qa_body.json 2>/dev/null)

if [ "$CART_LEN2" = "2" ]; then PASS=$((PASS+1))
else FAIL=$((FAIL+1)); echo "FAIL: cart should have 2 items after adding 2 products, got $CART_LEN2"; fi
if [ "$CART_LEN3" = "2" ]; then PASS=$((PASS+1))
else FAIL=$((FAIL+1)); echo "FAIL: cart should still have 2 items (qty merged), got $CART_LEN3"; fi

curl -s -o /dev/null -X DELETE "$BASE/cart" "${C[@]}" 2>/dev/null

echo "--- Availability cardinality ---"
check "avail no dups" 200 GET "/appointments/availability?storeId=1&serviceId=1&date=2027-09-01" "${A[@]}"
check_true "no duplicate times" '[. | group_by(.) | all(length == 1)]'

###############################################################################
echo ""
echo "========== T — TIME =========="
###############################################################################

echo "--- Timestamp freshness ---"
NOW_EPOCH=$(date -u +%s)
check "create product" 201 POST "/products" "${A[@]}" "${J[@]}" -d '{"prodName":"QA_TIME_TEST","prodPrice":5.55,"categoryId":1}'
TPROD_ID=$(jq -r '.prodId // empty' /tmp/qa_body.json)
[ -n "$TPROD_ID" ] && CLEANUP_PRODUCT_IDS+=("$TPROD_ID")

if [ -n "$TPROD_ID" ]; then
  check "get created product" 200 GET "/products/$TPROD_ID"
  CREATED_AT=$(jq -r '.createdAt // empty' /tmp/qa_body.json)
  if [ -n "$CREATED_AT" ]; then
    CREATED_EPOCH=$(date -u -d "$CREATED_AT" +%s 2>/dev/null || echo "0")
    DIFF=$((NOW_EPOCH - CREATED_EPOCH))
    if [ "$DIFF" -ge "-10" ] && [ "$DIFF" -le "120" ]; then PASS=$((PASS+1))
    else FAIL=$((FAIL+1)); echo "FAIL: createdAt not recent (diff=${DIFF}s)"; fi
  else
    FAIL=$((FAIL+1)); echo "FAIL: createdAt is null"
  fi

  check "update product" 200 PUT "/products/$TPROD_ID" "${A[@]}" "${J[@]}" -d "{\"prodName\":\"QA_TIME_UPD\",\"prodPrice\":6.66,\"categoryId\":1}"
  check "get updated product" 200 GET "/products/$TPROD_ID"
  check_true "updatedAt >= createdAt" '(.updatedAt // .createdAt) >= .createdAt'
fi

check "create sale" 201 POST "/sales" "${S[@]}" "${J[@]}" -d '{"storeId":1,"paymentMethod":"Cash","channel":"IN_STORE","items":[{"prodId":2,"quantity":1}]}'
TSALE_ID=$(jq -r '.saleId // empty' /tmp/qa_body.json)
[ -n "$TSALE_ID" ] && CLEANUP_SALE_IDS+=("$TSALE_ID")
if [ -n "$TSALE_ID" ]; then
  check "get sale" 200 GET "/sales/$TSALE_ID" "${A[@]}"
  check_true "saleDate has T (recent)" '.saleDate | test("T")'
fi

check "conversations for chat" 200 GET "/chat/conversations" "${C[@]}"
CHAT_CONV=$(jq -r '.[0].conversationId // empty' /tmp/qa_body.json)
if [ -n "$CHAT_CONV" ]; then
  curl -s -o /dev/null -X POST "$BASE/chat/conversations/$CHAT_CONV/messages" "${C[@]}" "${J[@]}" -d '{"content":"QA time test message"}' 2>/dev/null
  check "get chat messages" 200 GET "/chat/conversations/$CHAT_CONV/messages" "${C[@]}"
  LAST_TS=$(jq -r '.[-1].timestamp // empty' /tmp/qa_body.json)
  if [ -n "$LAST_TS" ]; then
    MSG_EPOCH=$(date -u -d "$LAST_TS" +%s 2>/dev/null || echo "0")
    MDIFF=$((NOW_EPOCH - MSG_EPOCH))
    if [ "$MDIFF" -ge "-10" ] && [ "$MDIFF" -le "300" ]; then PASS=$((PASS+1))
    else FAIL=$((FAIL+1)); echo "FAIL: chat message timestamp not recent (diff=${MDIFF}s)"; fi
  fi
fi

echo "--- Temporal constraints ---"
TODAY=$(date -u +%Y-%m-%d)
check "scheduled appts" 200 GET "/appointments?status=Scheduled&size=50" "${A[@]}"
check_true "scheduled dates >= today" "[.content[].appointmentDate] | all(. >= \"$TODAY\")"

check "coupons temporal" 200 GET "/coupons" "${A[@]}"
NOW_ISO=$(date -u +%Y-%m-%dT%H:%M:%S)
check_true "active coupons endsAt valid" "[.[] | select(.endsAt != null) | .endsAt] | all(. >= \"$NOW_ISO\" or . == null)"

echo "--- Sequencing ---"
if [ -n "${CHAT_CONV:-}" ]; then
  check "chat sequence" 200 GET "/chat/conversations/$CHAT_CONV/messages" "${C[@]}"
  check_true "chat timestamps non-decreasing" '. as $a | [range(1;length)] | all(. as $i | $a[$i].timestamp >= $a[$i-1].timestamp)'
fi

check "activity log sequence" 200 GET "/activity-logs?limit=20" "${A[@]}"
check_true "logs non-increasing" '. as $a | [range(1;length)] | all(. as $i | $a[$i].logTimestamp <= $a[$i-1].logTimestamp)'

echo "--- Token timing ---"
FRESH_TOKEN=$(curl -s -X POST "$BASE/auth/login" -H 'Content-Type: application/json' -d '{"username":"admin","password":"admin123"}' | jq -r '.token')
check "fresh token works" 200 GET "/auth/me" -H "Authorization: Bearer $FRESH_TOKEN"

curl -s -o /dev/null -X POST "$BASE/auth/logout" -H "Authorization: Bearer $FRESH_TOKEN" 2>/dev/null
LOGOUT_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X GET "$BASE/auth/me" -H "Authorization: Bearer $FRESH_TOKEN" 2>/dev/null)
if [ "$LOGOUT_CODE" = "401" ]; then PASS=$((PASS+1))
else FAIL=$((FAIL+1)); echo "FAIL: old token after logout — expected 401 got $LOGOUT_CODE"; fi

ADMIN_TOKEN=$(curl -s -X POST "$BASE/auth/login" -H 'Content-Type: application/json' -d '{"username":"admin","password":"admin123"}' | jq -r '.token')
A=(-H "Authorization: Bearer $ADMIN_TOKEN")

echo ""
echo "========================================="
echo "CORRECT RESULTS: $PASS passed, $FAIL failed"
echo "========================================="
