#!/bin/bash
# HTTP Test Assertions Library
# Provides reusable assertion functions for HTTP testing

set -euo pipefail

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

# Test counters
TESTS_RUN=0
TESTS_PASSED=0
TESTS_FAILED=0

# Log functions
log_info() {
    echo -e "${GREEN}[INFO]${NC} $*" >&2
}

log_warn() {
    echo -e "${YELLOW}[WARN]${NC} $*" >&2
}

log_error() {
    echo -e "${RED}[ERROR]${NC} $*" >&2
}

# Core assertion failure handler
fail() {
    local message="$1"
    local context="${2:-}"
    TESTS_FAILED=$((TESTS_FAILED + 1))
    log_error "ASSERTION FAILED: $message"
    if [ -n "$context" ]; then
        log_error "Context: $context"
    fi
    # Don't exit immediately, let the test continue
    return 1
}

pass() {
    local message="$1"
    TESTS_PASSED=$((TESTS_PASSED + 1))
    if [ "${VERBOSE:-false}" = "true" ]; then
        log_info "✓ $message"
    fi
}

# HTTP Status assertions
assert_status() {
    local actual="$1"
    local expected="$2"
    local context="${3:-HTTP request}"
    TESTS_RUN=$((TESTS_RUN + 1))
    
    if [ "$actual" -eq "$expected" ]; then
        pass "Status code $expected for $context"
        return 0
    else
        fail "Expected status $expected but got $actual" "$context"
        return 1
    fi
}

assert_status_range() {
    local actual="$1"
    local min="$2"
    local max="$3"
    local context="${4:-HTTP request}"
    TESTS_RUN=$((TESTS_RUN + 1))
    
    if [ "$actual" -ge "$min" ] && [ "$actual" -le "$max" ]; then
        pass "Status code $actual in range [$min-$max] for $context"
        return 0
    else
        fail "Expected status in range [$min-$max] but got $actual" "$context"
        return 1
    fi
}

# JSON assertions
assert_json_has() {
    local json="$1"
    local path="$2"
    local context="${3:-JSON response}"
    TESTS_RUN=$((TESTS_RUN + 1))
    
    if echo "$json" | jq -e "$path" >/dev/null 2>&1; then
        pass "JSON has path '$path' in $context"
        return 0
    else
        fail "JSON missing path: $path" "$context"
        return 1
    fi
}

assert_json_equals() {
    local json="$1"
    local path="$2"
    local expected="$3"
    local context="${4:-JSON response}"
    TESTS_RUN=$((TESTS_RUN + 1))
    
    local actual
    actual=$(echo "$json" | jq -r "$path" 2>/dev/null || echo "")
    
    if [ "$actual" = "$expected" ]; then
        pass "JSON path '$path' equals '$expected' in $context"
        return 0
    else
        fail "JSON path '$path' expected '$expected' but got '$actual'" "$context"
        return 1
    fi
}

assert_json_type() {
    local json="$1"
    local path="$2"
    local expected_type="$3"
    local context="${4:-JSON response}"
    TESTS_RUN=$((TESTS_RUN + 1))
    
    local actual_type
    actual_type=$(echo "$json" | jq -r "$path | type" 2>/dev/null || echo "null")
    
    if [ "$actual_type" = "$expected_type" ]; then
        pass "JSON path '$path' has type '$expected_type' in $context"
        return 0
    else
        fail "JSON path '$path' expected type '$expected_type' but got '$actual_type'" "$context"
        return 1
    fi
}

assert_json_not_empty() {
    local json="$1"
    local path="$2"
    local context="${3:-JSON response}"
    TESTS_RUN=$((TESTS_RUN + 1))
    
    local value
    value=$(echo "$json" | jq -r "$path" 2>/dev/null || echo "null")
    
    if [ "$value" != "null" ] && [ "$value" != "" ] && [ "$value" != "[]" ] && [ "$value" != "{}" ]; then
        pass "JSON path '$path' is not empty in $context"
        return 0
    else
        fail "JSON path '$path' is empty or null" "$context"
        return 1
    fi
}

# Header assertions
assert_header_exists() {
    local headers="$1"
    local header_name="$2"
    local context="${3:-HTTP response}"
    TESTS_RUN=$((TESTS_RUN + 1))
    
    if echo "$headers" | grep -qi "^$header_name:"; then
        pass "Header '$header_name' exists in $context"
        return 0
    else
        fail "Header '$header_name' missing" "$context"
        return 1
    fi
}

assert_header_contains() {
    local headers="$1"
    local header_name="$2"
    local expected_value="$3"
    local context="${4:-HTTP response}"
    TESTS_RUN=$((TESTS_RUN + 1))
    
    local header_line
    header_line=$(echo "$headers" | grep -i "^$header_name:" || echo "")
    
    if echo "$header_line" | grep -qi "$expected_value"; then
        pass "Header '$header_name' contains '$expected_value' in $context"
        return 0
    else
        fail "Header '$header_name' does not contain '$expected_value'" "$context"
        return 1
    fi
}

assert_header_equals() {
    local headers="$1"
    local header_name="$2"
    local expected_value="$3"
    local context="${4:-HTTP response}"
    TESTS_RUN=$((TESTS_RUN + 1))
    
    local actual_value
    actual_value=$(echo "$headers" | grep -i "^$header_name:" | cut -d' ' -f2- | tr -d '\r\n' || echo "")
    
    if [ "$actual_value" = "$expected_value" ]; then
        pass "Header '$header_name' equals '$expected_value' in $context"
        return 0
    else
        fail "Header '$header_name' expected '$expected_value' but got '$actual_value'" "$context"
        return 1
    fi
}

# Performance assertions
assert_latency_lt() {
    local actual_ms="$1"
    local threshold_ms="$2"
    local context="${3:-HTTP request}"
    TESTS_RUN=$((TESTS_RUN + 1))
    
    # Use bc for floating point comparison if available, otherwise awk
    if command -v bc >/dev/null 2>&1; then
        if [ "$(echo "$actual_ms < $threshold_ms" | bc)" -eq 1 ]; then
            pass "Latency ${actual_ms}ms < ${threshold_ms}ms for $context"
            return 0
        else
            fail "Latency ${actual_ms}ms >= ${threshold_ms}ms" "$context"
            return 1
        fi
    else
        if awk -v a="$actual_ms" -v b="$threshold_ms" 'BEGIN{exit !(a<b)}'; then
            pass "Latency ${actual_ms}ms < ${threshold_ms}ms for $context"
            return 0
        else
            fail "Latency ${actual_ms}ms >= ${threshold_ms}ms" "$context"
            return 1
        fi
    fi
}

# Content assertions
assert_body_contains() {
    local body="$1"
    local expected="$2"
    local context="${3:-response body}"
    TESTS_RUN=$((TESTS_RUN + 1))
    
    if echo "$body" | grep -q "$expected"; then
        pass "Body contains '$expected' in $context"
        return 0
    else
        fail "Body does not contain '$expected'" "$context"
        return 1
    fi
}

assert_body_not_contains() {
    local body="$1"
    local unexpected="$2"
    local context="${3:-response body}"
    TESTS_RUN=$((TESTS_RUN + 1))
    
    if echo "$body" | grep -q "$unexpected"; then
        fail "Body contains unexpected '$unexpected'" "$context"
        return 1
    else
        pass "Body does not contain '$unexpected' in $context"
        return 0
    fi
}

# Security assertions
assert_security_headers() {
    local headers="$1"
    local context="${2:-HTTP response}"
    local all_passed=true
    
    # Check for essential security headers
    local security_headers=(
        "X-Content-Type-Options"
        "X-Frame-Options"
        "Referrer-Policy"
        "Content-Security-Policy"
    )
    
    for header in "${security_headers[@]}"; do
        if ! assert_header_exists "$headers" "$header" "$context"; then
            all_passed=false
        fi
    done
    
    if [ "$all_passed" = true ]; then
        return 0
    else
        return 1
    fi
}

# Cookie assertions
assert_cookie_exists() {
    local headers="$1"
    local cookie_name="$2"
    local context="${3:-HTTP response}"
    TESTS_RUN=$((TESTS_RUN + 1))
    
    if echo "$headers" | grep -i "^set-cookie:" | grep -q "$cookie_name="; then
        pass "Cookie '$cookie_name' exists in $context"
        return 0
    else
        fail "Cookie '$cookie_name' not found" "$context"
        return 1
    fi
}

assert_cookie_has_flag() {
    local headers="$1"
    local cookie_name="$2"
    local flag="$3"
    local context="${4:-HTTP response}"
    TESTS_RUN=$((TESTS_RUN + 1))
    
    local cookie_line
    cookie_line=$(echo "$headers" | grep -i "^set-cookie:" | grep "$cookie_name=" || echo "")
    
    if echo "$cookie_line" | grep -qi "$flag"; then
        pass "Cookie '$cookie_name' has flag '$flag' in $context"
        return 0
    else
        fail "Cookie '$cookie_name' missing flag '$flag'" "$context"
        return 1
    fi
}

# Test summary
print_test_summary() {
    echo
    echo "=========================================="
    echo "Test Summary"
    echo "=========================================="
    echo "Total tests run: $TESTS_RUN"
    echo -e "Passed: ${GREEN}$TESTS_PASSED${NC}"
    echo -e "Failed: ${RED}$TESTS_FAILED${NC}"
    echo "=========================================="
    
    if [ "$TESTS_FAILED" -gt 0 ]; then
        return 1
    else
        return 0
    fi
}

# Utility functions
url_encode() {
    local string="$1"
    echo -n "$string" | jq -sRr @uri
}

generate_test_email() {
    local prefix="${TEST_EMAIL_PREFIX:-user+test}"
    local domain="${TEST_EMAIL_DOMAIN:-lab.veza}"
    local rand="${1:-$(date +%s)}"
    echo "${prefix}${rand}@${domain}"
}

generate_test_password() {
    local prefix="${TEST_PASSWORD_PREFIX:-V3za!lab-}"
    local rand="${1:-$(date +%s)}"
    echo "${prefix}${rand}"
}

# JSON Schema validation (if ajv available)
assert_json_schema() {
    local json="$1"
    local schema_file="$2"
    local context="${3:-JSON response}"
    TESTS_RUN=$((TESTS_RUN + 1))
    
    if ! command -v ajv >/dev/null 2>&1; then
        log_warn "ajv not installed, skipping schema validation"
        return 0
    fi
    
    if echo "$json" | ajv validate -s "$schema_file" >/dev/null 2>&1; then
        pass "JSON validates against schema in $context"
        return 0
    else
        fail "JSON schema validation failed" "$context"
        return 1
    fi
}

# Export all functions
export -f log_info log_warn log_error fail pass
export -f assert_status assert_status_range
export -f assert_json_has assert_json_equals assert_json_type assert_json_not_empty
export -f assert_header_exists assert_header_contains assert_header_equals
export -f assert_latency_lt
export -f assert_body_contains assert_body_not_contains
export -f assert_security_headers
export -f assert_cookie_exists assert_cookie_has_flag
export -f print_test_summary
export -f url_encode generate_test_email generate_test_password
export -f assert_json_schema