#!/usr/bin/env bash

#########################################################################################
#### mailcow-smoke-tests.sh — Verify Mailcow instance health after upgrades          ####
#### Zero external dependencies. Requires: bash 4+, curl, openssl, nc (netcat)       ####
####                                                                                 ####
#### Author: Phil Connor                                                             ####
#### Contact: contact@mylinux.work                                                   ####
#### License: MIT                                                                    ####
#### Version 1.0                                                                     ####
####                                                                                 ####
#### Usage:                                                                          ####
####   export MAILCOW_URL="https://mail.example.com"                                 ####
####   export MAILCOW_API_KEY="your-api-key"                                         ####
####   ./mailcow-smoke-tests.sh                                                      ####
####                                                                                 ####
#### See --help for all options.                                                     ####
#########################################################################################

set -euo pipefail

# ── Defaults ──────────────────────────────────────────────────────────
MAILCOW_URL="${MAILCOW_URL:-}"
MAILCOW_API_KEY="${MAILCOW_API_KEY:-}"
MAILCOW_DIR="${MAILCOW_DIR:-/opt/mailcow-dockerized}"
CURL_TIMEOUT="${CURL_TIMEOUT:-10}"
CURL_INSECURE="${CURL_INSECURE:-false}"
SKIP_SMTP="${SKIP_SMTP:-false}"
SKIP_IMAP="${SKIP_IMAP:-false}"
SKIP_CLAMD="${SKIP_CLAMD:-false}"
OUTPUT_FORMAT="${OUTPUT_FORMAT:-text}"  # text, tap, junit
JUNIT_FILE="${JUNIT_FILE:-smoke-results.xml}"
VERBOSE="${VERBOSE:-false}"
COLOR="${COLOR:-auto}"

# ── State ─────────────────────────────────────────────────────────────
PASS=0
FAIL=0
SKIP=0
TOTAL=0
RESULTS=()
START_TIME=""

# ── Colors ────────────────────────────────────────────────────────────
setup_colors() {
    if [[ "$COLOR" == "never" ]]; then
        RED="" GREEN="" YELLOW="" BLUE="" BOLD="" RESET=""
        return
    fi
    if [[ "$COLOR" == "always" ]] || [[ -t 1 ]]; then
        RED='\033[0;31m'
        GREEN='\033[0;32m'
        YELLOW='\033[0;33m'
        BLUE='\033[0;34m'
        BOLD='\033[1m'
        RESET='\033[0m'
    else
        RED="" GREEN="" YELLOW="" BLUE="" BOLD="" RESET=""
    fi
}

# ── Logging ───────────────────────────────────────────────────────────
log()     { echo -e "${BLUE}[INFO]${RESET} $*"; }
warn()    { echo -e "${YELLOW}[WARN]${RESET} $*" >&2; }
err()     { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
verbose() { if [[ "$VERBOSE" == "true" ]]; then echo -e "${BLUE}[DEBUG]${RESET} $*"; fi; }

# ── Test Result Recording ─────────────────────────────────────────────
record_pass() {
    local name="$1"
    local detail="${2:-}"
    ((PASS++)) || true
    ((TOTAL++)) || true
    RESULTS+=("PASS|${name}|${detail}")
    if [[ "$OUTPUT_FORMAT" == "tap" ]]; then
        echo "ok ${TOTAL} - ${name}"
    else
        echo -e "  ${GREEN}✓${RESET} ${name}${detail:+ — ${detail}}"
    fi
}

record_fail() {
    local name="$1"
    local detail="${2:-}"
    ((FAIL++)) || true
    ((TOTAL++)) || true
    RESULTS+=("FAIL|${name}|${detail}")
    if [[ "$OUTPUT_FORMAT" == "tap" ]]; then
        echo "not ok ${TOTAL} - ${name}"
        [[ -n "$detail" ]] && echo "  # ${detail}"
    else
        echo -e "  ${RED}✗${RESET} ${name}${detail:+ — ${detail}}"
    fi
}

record_skip() {
    local name="$1"
    local reason="${2:-}"
    ((SKIP++)) || true
    ((TOTAL++)) || true
    RESULTS+=("SKIP|${name}|${reason}")
    if [[ "$OUTPUT_FORMAT" == "tap" ]]; then
        echo "ok ${TOTAL} - ${name} # SKIP ${reason}"
    else
        echo -e "  ${YELLOW}⊘${RESET} ${name}${reason:+ — ${reason}}"
    fi
}

# ── curl wrapper ──────────────────────────────────────────────────────
api_curl() {
    local endpoint="$1"
    shift
    local curl_opts=(-s -S --max-time "$CURL_TIMEOUT")

    [[ "$CURL_INSECURE" == "true" ]] && curl_opts+=(-k)
    curl_opts+=(-H "X-API-Key: ${MAILCOW_API_KEY}")
    curl_opts+=(-H "Content-Type: application/json")

    local url="${MAILCOW_URL}${endpoint}"
    verbose "curl GET ${url}"

    curl "${curl_opts[@]}" "$@" "$url" 2>/dev/null
}

api_curl_status() {
    local endpoint="$1"
    shift
    local curl_opts=(-s -S -o /dev/null -w "%{http_code}" --max-time "$CURL_TIMEOUT")

    [[ "$CURL_INSECURE" == "true" ]] && curl_opts+=(-k)
    curl_opts+=(-H "X-API-Key: ${MAILCOW_API_KEY}")
    curl_opts+=(-H "Content-Type: application/json")

    local url="${MAILCOW_URL}${endpoint}"
    curl "${curl_opts[@]}" "$@" "$url" 2>/dev/null
}

# ── JSON parsing (no jq required) ────────────────────────────────────
json_value() {
    local key="$1"
    local json="$2"
    echo "$json" | { grep -oP "\"${key}\"\s*:\s*\"?\K[^\",}]+" || true; } | head -1
}

json_value_string() {
    local key="$1"
    local json="$2"
    echo "$json" | { grep -oP "\"${key}\"\s*:\s*\"\K[^\"]*" || true; } | head -1
}

# ── Host extraction ───────────────────────────────────────────────────
get_mailcow_host() {
    echo "$MAILCOW_URL" | sed 's|https\?://||' | cut -d/ -f1 | cut -d: -f1
}

# ── Port check ────────────────────────────────────────────────────────
check_port() {
    local host="$1"
    local port="$2"
    nc -z -w 3 "$host" "$port" >/dev/null 2>&1
}

# ══════════════════════════════════════════════════════════════════════
#                          TEST SUITES
# ══════════════════════════════════════════════════════════════════════

# ── 1. Connectivity ──────────────────────────────────────────────────
test_connectivity() {
    echo ""
    echo -e "${BOLD}Connectivity${RESET}"

    # 1a. Mailcow UI reachable
    local curl_opts=(-s -o /dev/null -w "%{http_code}" --max-time "$CURL_TIMEOUT")
    [[ "$CURL_INSECURE" == "true" ]] && curl_opts+=(-k)

    local http_code
    http_code=$(curl "${curl_opts[@]}" "${MAILCOW_URL}/" 2>/dev/null) || http_code="000"

    if [[ "$http_code" == "200" || "$http_code" == "301" || "$http_code" == "302" ]]; then
        record_pass "Mailcow UI reachable" "HTTP ${http_code}"
    else
        record_fail "Mailcow UI reachable" "HTTP ${http_code}"
    fi

    # 1b. TLS certificate validity
    if [[ "$MAILCOW_URL" == https://* ]]; then
        local host
        host=$(get_mailcow_host)
        local port
        port=$(echo "$MAILCOW_URL" | grep -oP ':\K[0-9]+$' || echo "443")

        local expiry
        expiry=$(echo | openssl s_client -servername "$host" -connect "${host}:${port}" 2>/dev/null | \
                 openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2) || expiry=""

        if [[ -n "$expiry" ]]; then
            local expiry_epoch
            expiry_epoch=$(date -d "$expiry" +%s 2>/dev/null) || expiry_epoch=0
            local now_epoch
            now_epoch=$(date +%s)
            local days_left=$(( (expiry_epoch - now_epoch) / 86400 ))

            if [[ $days_left -gt 30 ]]; then
                record_pass "TLS certificate valid" "${days_left} days remaining"
            elif [[ $days_left -gt 0 ]]; then
                record_pass "TLS certificate valid" "${days_left} days remaining (renew soon)"
            else
                record_fail "TLS certificate valid" "expired or expiring in ${days_left} days"
            fi
        else
            record_skip "TLS certificate check" "could not retrieve certificate"
        fi
    else
        record_skip "TLS certificate check" "not using HTTPS"
    fi
}

# ── 2. API ────────────────────────────────────────────────────────────
test_api() {
    echo ""
    echo -e "${BOLD}API${RESET}"

    # 2a. API authentication — version endpoint
    local version_json
    version_json=$(api_curl "/api/v1/get/status/version" 2>/dev/null) || version_json=""

    local mc_version
    mc_version=$(json_value "version" "$version_json")

    if [[ -n "$mc_version" && "$mc_version" != "null" ]]; then
        record_pass "API authentication" "Mailcow ${mc_version}"
    elif [[ "$version_json" == *"error"* || "$version_json" == *"unauthorized"* ]]; then
        record_fail "API authentication" "API key rejected"
    else
        record_fail "API authentication" "no version returned"
    fi

    # 2b. Container status
    local containers_json
    containers_json=$(api_curl "/api/v1/get/status/containers" 2>/dev/null) || containers_json=""

    if [[ -n "$containers_json" && "$containers_json" != *"error"* ]]; then
        local running_count=0
        local stopped_count=0
        local container_names=""

        # Count running vs non-running containers
        running_count=$(echo "$containers_json" | { grep -oP '"state"\s*:\s*"running"' || true; } | wc -l)
        stopped_count=$(echo "$containers_json" | { grep -oP '"state"\s*:\s*"(?!running)[^"]*"' || true; } | wc -l)

        if [[ $running_count -gt 0 && $stopped_count -eq 0 ]]; then
            record_pass "Container status" "all ${running_count} containers running"
        elif [[ $running_count -gt 0 ]]; then
            record_fail "Container status" "${running_count} running, ${stopped_count} not running"
        else
            record_fail "Container status" "no running containers found"
        fi
    else
        record_fail "Container status" "could not query container status"
    fi

    # 2c. List domains
    local domains_status
    domains_status=$(api_curl_status "/api/v1/get/domain/all")
    if [[ "$domains_status" == "200" ]]; then
        local domains_json
        domains_json=$(api_curl "/api/v1/get/domain/all" 2>/dev/null) || domains_json=""
        local domain_count
        domain_count=$(echo "$domains_json" | { grep -oP '"domain_name"\s*:' || true; } | wc -l)
        record_pass "List domains" "${domain_count} domain(s) found"
    else
        record_fail "List domains" "HTTP ${domains_status}"
    fi

    # 2d. List mailboxes
    local mailboxes_status
    mailboxes_status=$(api_curl_status "/api/v1/get/mailbox/all")
    if [[ "$mailboxes_status" == "200" ]]; then
        local mailboxes_json
        mailboxes_json=$(api_curl "/api/v1/get/mailbox/all" 2>/dev/null) || mailboxes_json=""
        local mailbox_count
        mailbox_count=$(echo "$mailboxes_json" | { grep -oP '"username"\s*:' || true; } | wc -l)
        record_pass "List mailboxes" "${mailbox_count} mailbox(es) found"
    else
        record_fail "List mailboxes" "HTTP ${mailboxes_status}"
    fi
}

# ── 3. Mail Services ─────────────────────────────────────────────────
test_mail_services() {
    echo ""
    echo -e "${BOLD}Mail Services${RESET}"

    local host
    host=$(get_mailcow_host)

    # 3a. SMTP
    if [[ "$SKIP_SMTP" == "true" ]]; then
        record_skip "SMTP port 25" "SKIP_SMTP=true"
        record_skip "SMTP port 587" "SKIP_SMTP=true"
    else
        if check_port "$host" 25; then
            record_pass "SMTP port 25" "accepting connections"
        else
            record_fail "SMTP port 25" "not reachable"
        fi

        if check_port "$host" 587; then
            record_pass "SMTP port 587 (submission)" "accepting connections"
        else
            record_fail "SMTP port 587 (submission)" "not reachable"
        fi
    fi

    # 3b. IMAP
    if [[ "$SKIP_IMAP" == "true" ]]; then
        record_skip "IMAP port 143" "SKIP_IMAP=true"
        record_skip "IMAP port 993" "SKIP_IMAP=true"
    else
        if check_port "$host" 143; then
            record_pass "IMAP port 143" "accepting connections"
        else
            record_fail "IMAP port 143" "not reachable"
        fi

        if check_port "$host" 993; then
            record_pass "IMAP port 993 (IMAPS)" "accepting connections"
        else
            record_fail "IMAP port 993 (IMAPS)" "not reachable"
        fi
    fi

    # 3c. POP3 (optional — skip if not available)
    if check_port "$host" 110; then
        record_pass "POP3 port 110" "accepting connections"
    else
        record_skip "POP3 port 110" "not reachable (may be disabled)"
    fi

    if check_port "$host" 995; then
        record_pass "POP3 port 995 (POP3S)" "accepting connections"
    else
        record_skip "POP3 port 995 (POP3S)" "not reachable (may be disabled)"
    fi
}

# ── 4. Spam Filter ───────────────────────────────────────────────────
test_spam_filter() {
    echo ""
    echo -e "${BOLD}Spam Filter${RESET}"

    # 4a. rspamd — check container status from API
    local containers_json
    containers_json=$(api_curl "/api/v1/get/status/containers" 2>/dev/null) || containers_json=""

    if [[ -n "$containers_json" ]]; then
        local rspamd_state
        rspamd_state=$(echo "$containers_json" | { grep -oP '"rspamd-mailcow[^}]*"state"\s*:\s*"\K[^"]*' || true; } | head -1)

        if [[ -z "$rspamd_state" ]]; then
            # Try alternate pattern — look for rspamd in container names
            rspamd_state=$(echo "$containers_json" | { grep -B5 '"rspamd' || true; } | { grep -oP '"state"\s*:\s*"\K[^"]*' || true; } | head -1)
        fi

        if [[ "$rspamd_state" == "running" ]]; then
            record_pass "rspamd running" "spam filter operational"
        elif [[ -n "$rspamd_state" ]]; then
            record_fail "rspamd running" "state: ${rspamd_state}"
        else
            record_skip "rspamd status" "could not determine rspamd container state"
        fi
    else
        record_fail "rspamd status" "could not query containers"
    fi

    # 4b. ClamAV
    if [[ "$SKIP_CLAMD" == "true" ]]; then
        record_skip "ClamAV status" "SKIP_CLAMD=true"
    elif [[ -n "$containers_json" ]]; then
        local clamd_state
        clamd_state=$(echo "$containers_json" | { grep -oP '"clamd-mailcow[^}]*"state"\s*:\s*"\K[^"]*' || true; } | head -1)

        if [[ -z "$clamd_state" ]]; then
            clamd_state=$(echo "$containers_json" | { grep -B5 '"clamd' || true; } | { grep -oP '"state"\s*:\s*"\K[^"]*' || true; } | head -1)
        fi

        if [[ "$clamd_state" == "running" ]]; then
            record_pass "ClamAV running" "antivirus operational"
        elif [[ -n "$clamd_state" ]]; then
            record_fail "ClamAV running" "state: ${clamd_state}"
        else
            record_skip "ClamAV status" "ClamAV container not found (may be disabled)"
        fi
    else
        record_skip "ClamAV status" "could not query containers"
    fi
}

# ── 5. Webmail ────────────────────────────────────────────────────────
test_webmail() {
    echo ""
    echo -e "${BOLD}Webmail${RESET}"

    local curl_opts=(-s -o /dev/null -w "%{http_code}" --max-time "$CURL_TIMEOUT")
    [[ "$CURL_INSECURE" == "true" ]] && curl_opts+=(-k)

    local sogo_code
    sogo_code=$(curl "${curl_opts[@]}" "${MAILCOW_URL}/SOGo" 2>/dev/null) || sogo_code="000"

    if [[ "$sogo_code" == "200" || "$sogo_code" == "301" || "$sogo_code" == "302" ]]; then
        record_pass "SOGo reachable" "HTTP ${sogo_code}"
    elif [[ "$sogo_code" == "000" ]]; then
        record_fail "SOGo reachable" "connection failed"
    else
        record_fail "SOGo reachable" "HTTP ${sogo_code}"
    fi
}

# ── 6. Quarantine ────────────────────────────────────────────────────
test_quarantine() {
    echo ""
    echo -e "${BOLD}Quarantine${RESET}"

    local quarantine_json
    quarantine_json=$(api_curl "/api/v1/get/quarantine/all" 2>/dev/null) || quarantine_json=""

    local quarantine_status
    quarantine_status=$(api_curl_status "/api/v1/get/quarantine/all")

    if [[ "$quarantine_status" == "200" ]]; then
        local q_count
        q_count=$(echo "$quarantine_json" | { grep -oP '"id"\s*:' || true; } | wc -l)
        record_pass "Quarantine endpoint" "${q_count} item(s) in quarantine"
    else
        record_fail "Quarantine endpoint" "HTTP ${quarantine_status}"
    fi
}

# ══════════════════════════════════════════════════════════════════════
#                          OUTPUT
# ══════════════════════════════════════════════════════════════════════

print_summary() {
    local end_time
    end_time=$(date +%s)
    local duration=$(( end_time - START_TIME ))

    echo ""
    echo -e "${BOLD}────────────────────────────────────────${RESET}"
    echo -e "${BOLD}Summary${RESET}  ${MAILCOW_URL}"
    echo -e "  ${GREEN}${PASS} passed${RESET}  ${RED}${FAIL} failed${RESET}  ${YELLOW}${SKIP} skipped${RESET}  (${duration}s)"
    echo -e "${BOLD}────────────────────────────────────────${RESET}"

    if [[ $FAIL -eq 0 ]]; then
        echo -e "${GREEN}${BOLD}All tests passed.${RESET}"
    else
        echo -e "${RED}${BOLD}${FAIL} test(s) failed.${RESET}"
    fi
}

print_tap_header() {
    echo "TAP version 13"
}

print_tap_footer() {
    echo "1..${TOTAL}"
    echo "# pass ${PASS}"
    echo "# fail ${FAIL}"
    echo "# skip ${SKIP}"
}

write_junit() {
    local end_time
    end_time=$(date +%s)
    local duration=$(( end_time - START_TIME ))

    cat > "$JUNIT_FILE" <<JUNIT_EOF
<?xml version="1.0" encoding="UTF-8"?>
<testsuites tests="${TOTAL}" failures="${FAIL}" skipped="${SKIP}" time="${duration}">
  <testsuite name="mailcow-smoke-tests" tests="${TOTAL}" failures="${FAIL}" skipped="${SKIP}" time="${duration}">
JUNIT_EOF

    for result in "${RESULTS[@]}"; do
        local status name detail
        status=$(echo "$result" | cut -d'|' -f1)
        name=$(echo "$result" | cut -d'|' -f2)
        detail=$(echo "$result" | cut -d'|' -f3)

        # XML-escape the values
        name=$(echo "$name" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g')
        detail=$(echo "$detail" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g; s/"/\&quot;/g')

        case "$status" in
            PASS)
                echo "    <testcase name=\"${name}\" classname=\"smoke\">" >> "$JUNIT_FILE"
                [[ -n "$detail" ]] && echo "      <system-out>${detail}</system-out>" >> "$JUNIT_FILE"
                echo "    </testcase>" >> "$JUNIT_FILE"
                ;;
            FAIL)
                echo "    <testcase name=\"${name}\" classname=\"smoke\">" >> "$JUNIT_FILE"
                echo "      <failure message=\"${detail}\">FAILED: ${name} — ${detail}</failure>" >> "$JUNIT_FILE"
                echo "    </testcase>" >> "$JUNIT_FILE"
                ;;
            SKIP)
                echo "    <testcase name=\"${name}\" classname=\"smoke\">" >> "$JUNIT_FILE"
                echo "      <skipped message=\"${detail}\"/>" >> "$JUNIT_FILE"
                echo "    </testcase>" >> "$JUNIT_FILE"
                ;;
        esac
    done

    echo "  </testsuite>" >> "$JUNIT_FILE"
    echo "</testsuites>" >> "$JUNIT_FILE"

    log "JUnit report written to ${JUNIT_FILE}"
}

# ══════════════════════════════════════════════════════════════════════
#                          MAIN
# ══════════════════════════════════════════════════════════════════════

usage() {
    cat <<EOF
Usage: $(basename "$0") [OPTIONS]

Smoke-test a Mailcow instance. Validates API, containers, mail services, spam
filter, webmail, and quarantine. Requires: bash 4+, curl, openssl, nc.

Required environment variables:
  MAILCOW_URL         Mailcow base URL (https://mail.example.com)
  MAILCOW_API_KEY     API key (read-only is sufficient)

Optional environment variables:
  MAILCOW_DIR         Mailcow install directory (default: /opt/mailcow-dockerized)
  CURL_TIMEOUT        HTTP timeout in seconds (default: 10)
  SKIP_SMTP           Skip SMTP port checks (default: false)
  SKIP_IMAP           Skip IMAP port checks (default: false)
  SKIP_CLAMD          Skip ClamAV checks (default: false)

Options:
  --skip-smtp         Skip SMTP port tests
  --skip-imap         Skip IMAP port tests
  --insecure          Allow self-signed TLS certificates (-k)
  --timeout N         curl timeout in seconds (default: 10)
  --format FORMAT     Output: text (default), tap, junit
  --junit-file FILE   JUnit output path (default: smoke-results.xml)
  --verbose           Show debug output
  --no-color          Disable colored output
  --help              Show this help

Examples:
  # Basic run
  export MAILCOW_URL="https://mail.example.com"
  export MAILCOW_API_KEY="your-api-key"
  ./$(basename "$0")

  # Self-signed cert, JUnit output
  MAILCOW_URL=https://mail.local MAILCOW_API_KEY=xxx \\
    ./$(basename "$0") --insecure --format junit

  # Skip port checks (remote host)
  MAILCOW_URL=https://mail.local MAILCOW_API_KEY=xxx \\
    ./$(basename "$0") --skip-smtp --skip-imap

  # TAP output for CI pipeline
  MAILCOW_URL=https://mail.local MAILCOW_API_KEY=xxx \\
    ./$(basename "$0") --format tap
EOF
}

main() {
    # Parse arguments
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --skip-smtp)      SKIP_SMTP=true ;;
            --skip-imap)      SKIP_IMAP=true ;;
            --insecure)       CURL_INSECURE=true ;;
            --timeout)        CURL_TIMEOUT="$2"; shift ;;
            --format)         OUTPUT_FORMAT="$2"; shift ;;
            --junit-file)     JUNIT_FILE="$2"; shift ;;
            --verbose)        VERBOSE=true ;;
            --no-color)       COLOR=never ;;
            --help|-h)        usage; exit 0 ;;
            *)                err "Unknown option: $1"; usage; exit 1 ;;
        esac
        shift
    done

    setup_colors

    # Validate required vars
    if [[ -z "$MAILCOW_URL" ]]; then
        err "MAILCOW_URL is required"
        echo ""
        usage
        exit 1
    fi

    if [[ -z "$MAILCOW_API_KEY" ]]; then
        err "MAILCOW_API_KEY is required"
        echo ""
        usage
        exit 1
    fi

    # Strip trailing slash
    MAILCOW_URL="${MAILCOW_URL%/}"

    START_TIME=$(date +%s)

    if [[ "$OUTPUT_FORMAT" == "tap" ]]; then
        print_tap_header
    else
        echo ""
        echo -e "${BOLD}Mailcow Smoke Tests${RESET}"
        echo -e "Target: ${MAILCOW_URL}"
        echo -e "Time:   $(date -u +%Y-%m-%dT%H:%M:%SZ)"
        echo ""
    fi

    # Run test suites
    test_connectivity
    test_api
    test_mail_services
    test_spam_filter
    test_webmail
    test_quarantine

    # Output
    if [[ "$OUTPUT_FORMAT" == "tap" ]]; then
        print_tap_footer
    elif [[ "$OUTPUT_FORMAT" == "junit" ]]; then
        print_summary
        write_junit
    else
        print_summary
    fi

    # Exit code
    [[ $FAIL -eq 0 ]] && exit 0 || exit 1
}

main "$@"
