#!/bin/bash
################################################################################
# Script Name: dokku-smoke-tests.sh
# Version: 1.0
# Description: Smoke test suite for Dokku PaaS — validates connectivity,
#              app deployment lifecycle, plugin health, SSL certificates,
#              and resource usage via the dokku CLI
#
# Author: Phil Connor
# Contact: contact@mylinux.work
# Website: https://mylinux.work
# License: MIT
#
# Prerequisites:
#   - bash 4+
#   - dokku binary (run on the Dokku host)
#   - Root or dokku user access
#
# Usage:
#   sudo ./dokku-smoke-tests.sh
#   sudo ./dokku-smoke-tests.sh --skip-app --skip-ssl
#   sudo ./dokku-smoke-tests.sh --format tap
#   sudo ./dokku-smoke-tests.sh --format junit --junit-file results.xml
#
################################################################################

set -euo pipefail

# --- Defaults ---
SKIP_APP="${SKIP_APP_LIFECYCLE:-false}"
SKIP_SSL="${SKIP_SSL:-false}"
OUTPUT_FORMAT="${OUTPUT_FORMAT:-text}"
JUNIT_FILE="${JUNIT_FILE:-smoke-results.xml}"
DOKKU_DOMAIN="${DOKKU_DOMAIN:-}"
VERBOSE=false
USE_COLOR=true
TEST_APP_NAME=""
PASSED=0
FAILED=0
SKIPPED=0
START_TIME=""
JUNIT_RESULTS=()
TAP_RESULTS=()
TEST_NUM=0

# --- Colors ---
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'

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

Smoke test suite for Dokku — run on the Dokku host as root or dokku user.

Options:
  --skip-app        Skip app lifecycle tests
  --skip-ssl        Skip SSL certificate checks
  --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
  -h, --help        Show this help

Environment:
  SKIP_APP_LIFECYCLE  Skip app lifecycle (same as --skip-app)
  SKIP_SSL            Skip SSL checks (same as --skip-ssl)
  OUTPUT_FORMAT       Output format (same as --format)
  JUNIT_FILE          JUnit output path
  DOKKU_DOMAIN        Dokku global domain (auto-detected if not set)
EOF
    exit 0
}

# --- Argument parsing ---
while [[ $# -gt 0 ]]; do
    case "$1" in
        --skip-app)     SKIP_APP=true; shift ;;
        --skip-ssl)     SKIP_SSL=true; shift ;;
        --format)       OUTPUT_FORMAT="$2"; shift 2 ;;
        --junit-file)   JUNIT_FILE="$2"; shift 2 ;;
        --verbose)      VERBOSE=true; shift ;;
        --no-color)     USE_COLOR=false; shift ;;
        -h|--help)      usage ;;
        *)              echo "Unknown option: $1"; usage ;;
    esac
done

if [[ "$USE_COLOR" == "false" ]]; then
    RED="" GREEN="" YELLOW="" CYAN="" BOLD="" NC=""
fi

# --- Helpers ---
debug() {
    if [[ "$VERBOSE" == "true" ]]; then
        echo -e "  ${CYAN}[debug]${NC} $*" >&2
    fi
}

pass() {
    local suite="$1" msg="$2"
    ((TEST_NUM++)) || true
    ((PASSED++)) || true
    case "$OUTPUT_FORMAT" in
        tap)  TAP_RESULTS+=("ok $TEST_NUM - [$suite] $msg") ;;
        junit) JUNIT_RESULTS+=("<testcase classname=\"$suite\" name=\"$msg\" />") ;;
        *)    echo -e "  ${GREEN}✓${NC} $msg" ;;
    esac
}

fail() {
    local suite="$1" msg="$2" detail="${3:-}"
    ((TEST_NUM++)) || true
    ((FAILED++)) || true
    case "$OUTPUT_FORMAT" in
        tap)  TAP_RESULTS+=("not ok $TEST_NUM - [$suite] $msg") ;;
        junit) JUNIT_RESULTS+=("<testcase classname=\"$suite\" name=\"$msg\"><failure message=\"$detail\">$detail</failure></testcase>") ;;
        *)    echo -e "  ${RED}✗${NC} $msg${detail:+ — $detail}" ;;
    esac
}

skip() {
    local suite="$1" msg="$2"
    ((TEST_NUM++)) || true
    ((SKIPPED++)) || true
    case "$OUTPUT_FORMAT" in
        tap)  TAP_RESULTS+=("ok $TEST_NUM - [$suite] $msg # SKIP") ;;
        junit) JUNIT_RESULTS+=("<testcase classname=\"$suite\" name=\"$msg\"><skipped /></testcase>") ;;
        *)    echo -e "  ${YELLOW}⊘${NC} $msg — skipped" ;;
    esac
}

suite_header() {
    if [[ "$OUTPUT_FORMAT" == "text" ]]; then
        echo -e "\n${BOLD}$1${NC}"
    fi
}

# --- Cleanup ---
cleanup() {
    if [[ -n "$TEST_APP_NAME" ]]; then
        debug "Cleaning up test app: $TEST_APP_NAME"
        dokku apps:destroy "$TEST_APP_NAME" --force >/dev/null 2>&1 || true
        TEST_APP_NAME=""
    fi
}
trap cleanup EXIT INT TERM

# --- Header ---
START_TIME=$(date +%s)
HOSTNAME_STR=$(hostname -f 2>/dev/null || hostname)

if [[ "$OUTPUT_FORMAT" == "text" ]]; then
    echo -e "${BOLD}Dokku Smoke Tests${NC}"
    echo "Host:   $HOSTNAME_STR"
    echo "Time:   $(date -u +%Y-%m-%dT%H:%M:%SZ)"
fi

# =====================================================
# Suite 1: Connectivity
# =====================================================
suite_header "Connectivity"

# Check dokku binary
DOKKU_BIN=$(command -v dokku 2>/dev/null || true)
if [[ -n "$DOKKU_BIN" ]]; then
    pass "Connectivity" "Dokku binary found — $DOKKU_BIN"
else
    fail "Connectivity" "Dokku binary not found" "dokku is not in PATH"
    echo -e "\n${RED}Cannot continue without dokku binary. Aborting.${NC}" >&2
    exit 1
fi

# Check Docker daemon
if docker info >/dev/null 2>&1; then
    pass "Connectivity" "Docker daemon running"
else
    fail "Connectivity" "Docker daemon not running" "docker info failed"
fi

# Check dokku version
DOKKU_VERSION=$(dokku version 2>/dev/null | grep -oP 'dokku version \K[0-9]+\.[0-9]+\.[0-9]+' || true)
if [[ -z "$DOKKU_VERSION" ]]; then
    # Try alternate format
    DOKKU_VERSION=$(dokku version 2>/dev/null | grep -oP '[0-9]+\.[0-9]+\.[0-9]+' || true)
fi

if [[ -n "$DOKKU_VERSION" ]]; then
    pass "Connectivity" "Dokku version — $DOKKU_VERSION"
else
    fail "Connectivity" "Dokku version" "Could not parse version string"
fi

# Auto-detect global domain if not set
if [[ -z "$DOKKU_DOMAIN" ]]; then
    DOKKU_DOMAIN=$(dokku domains:report --global 2>/dev/null | grep -i "global vhosts" | awk '{print $NF}' || true)
    if [[ -z "$DOKKU_DOMAIN" ]]; then
        DOKKU_DOMAIN=$(dokku domains:report --global 2>/dev/null | tail -1 | awk '{print $NF}' || true)
    fi
    debug "Auto-detected domain: $DOKKU_DOMAIN"
fi

# =====================================================
# Suite 2: App Lifecycle
# =====================================================
if [[ "$SKIP_APP" == "true" ]]; then
    suite_header "App Lifecycle"
    skip "App Lifecycle" "Create test app"
    skip "App Lifecycle" "Deploy image"
    skip "App Lifecycle" "App responding"
    skip "App Lifecycle" "Delete test app"
else
    suite_header "App Lifecycle"

    TEST_APP_NAME="dokku-smoke-$(date +%s)"
    debug "Test app name: $TEST_APP_NAME"

    # Create app
    create_output=$(dokku apps:create "$TEST_APP_NAME" 2>&1) || true
    debug "Create output: $create_output"

    if dokku apps:exists "$TEST_APP_NAME" >/dev/null 2>&1; then
        pass "App Lifecycle" "Create test app — $TEST_APP_NAME"
    else
        fail "App Lifecycle" "Create test app" "$create_output"
        skip "App Lifecycle" "Deploy image"
        skip "App Lifecycle" "App responding"
        skip "App Lifecycle" "Delete test app"
        TEST_APP_NAME=""
        SKIP_APP=true
    fi

    if [[ "$SKIP_APP" != "true" ]]; then
        # Deploy image via git:from-image
        deploy_output=$(dokku git:from-image "$TEST_APP_NAME" nginxdemos/hello 2>&1) || true
        debug "Deploy output: $deploy_output"

        # Check if app is running
        app_running=false
        for i in $(seq 1 12); do
            sleep 5
            debug "Waiting for app to start... attempt $i/12"
            ps_output=$(dokku ps:report "$TEST_APP_NAME" 2>/dev/null || true)
            if echo "$ps_output" | grep -qi "running"; then
                app_running=true
                break
            fi
            # Also check container status directly
            running_count=$(dokku ps:report "$TEST_APP_NAME" 2>/dev/null | grep -i "running" | wc -l || echo "0")
            if [[ "$running_count" -gt 0 ]]; then
                app_running=true
                break
            fi
        done

        if [[ "$app_running" == "true" ]]; then
            pass "App Lifecycle" "Deploy image — nginxdemos/hello deployed"
        else
            fail "App Lifecycle" "Deploy image" "App not running after 60s"
        fi

        # Verify HTTP response
        if [[ -n "$DOKKU_DOMAIN" ]]; then
            app_url="http://${TEST_APP_NAME}.${DOKKU_DOMAIN}"
            debug "App URL: $app_url"
            sleep 3

            app_http=$(curl -s -o /dev/null -w "%{http_code}" \
                --connect-timeout 10 --max-time 30 \
                "$app_url" 2>/dev/null || echo "000")

            if [[ "$app_http" == "200" ]]; then
                pass "App Lifecycle" "App responding — HTTP 200 at ${TEST_APP_NAME}.${DOKKU_DOMAIN}"
            else
                fail "App Lifecycle" "App responding" "HTTP $app_http at $app_url"
            fi
        else
            # No domain configured — check container port directly
            debug "No global domain — checking container directly"
            port=$(dokku proxy:ports "$TEST_APP_NAME" 2>/dev/null | grep -oP ':\K[0-9]+$' | head -1 || true)
            if [[ -n "$port" ]]; then
                app_http=$(curl -s -o /dev/null -w "%{http_code}" \
                    --connect-timeout 10 --max-time 30 \
                    "http://localhost:$port" 2>/dev/null || echo "000")
                if [[ "$app_http" == "200" ]]; then
                    pass "App Lifecycle" "App responding — HTTP 200 on port $port"
                else
                    fail "App Lifecycle" "App responding" "HTTP $app_http on port $port"
                fi
            else
                skip "App Lifecycle" "App responding"
            fi
        fi

        # Delete test app
        delete_output=$(dokku apps:destroy "$TEST_APP_NAME" --force 2>&1) || true
        debug "Delete output: $delete_output"

        if ! dokku apps:exists "$TEST_APP_NAME" >/dev/null 2>&1; then
            pass "App Lifecycle" "Delete test app — cleaned up"
            TEST_APP_NAME=""
        else
            fail "App Lifecycle" "Delete test app" "Manual cleanup may be required"
        fi
    fi
fi

# =====================================================
# Suite 3: Plugin Health
# =====================================================
suite_header "Plugins"

plugin_list=$(dokku plugin:list 2>/dev/null || true)
debug "Plugin list: $plugin_list"

if [[ -n "$plugin_list" ]]; then
    plugin_count=$(echo "$plugin_list" | grep -c "enabled" || echo "0")
    pass "Plugins" "Plugin list — $plugin_count plugins installed"
else
    fail "Plugins" "Plugin list" "dokku plugin:list failed"
fi

# Check core plugins
CORE_PLUGINS=("nginx-vhosts" "apps" "config" "ps")
for plugin in "${CORE_PLUGINS[@]}"; do
    if echo "$plugin_list" | grep -q "$plugin"; then
        pass "Plugins" "Core plugin present — $plugin"
    else
        fail "Plugins" "Core plugin present — $plugin" "Not found in plugin list"
    fi
done

# =====================================================
# Suite 4: SSL
# =====================================================
if [[ "$SKIP_SSL" == "true" ]]; then
    suite_header "SSL"
    skip "SSL" "Letsencrypt plugin installed"
    skip "SSL" "TLS certificate valid"
else
    suite_header "SSL"

    # Check if letsencrypt plugin is installed
    le_installed=false
    if echo "$plugin_list" | grep -qi "letsencrypt"; then
        le_installed=true
        pass "SSL" "Letsencrypt plugin installed"
    else
        skip "SSL" "Letsencrypt plugin installed"
    fi

    # Check global domain certificate
    if [[ "$le_installed" == "true" && -n "$DOKKU_DOMAIN" ]]; then
        # Check certificate via openssl if the domain resolves
        cert_host="$DOKKU_DOMAIN"
        cert_output=$(echo | openssl s_client -servername "$cert_host" -connect "${cert_host}:443" 2>/dev/null || true)
        cert_enddate=$(echo "$cert_output" | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2 || true)

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

            if [[ "$days_left" -gt 0 ]]; then
                pass "SSL" "TLS certificate valid — $days_left days remaining"
            else
                fail "SSL" "TLS certificate expired" "$days_left days past expiry"
            fi
        else
            skip "SSL" "TLS certificate valid"
        fi
    else
        skip "SSL" "TLS certificate valid"
    fi
fi

# =====================================================
# Suite 5: Resources
# =====================================================
suite_header "Resources"

# Disk usage
disk_line=$(df -h / 2>/dev/null | tail -1 || true)
if [[ -n "$disk_line" ]]; then
    disk_pct=$(echo "$disk_line" | awk '{print $5}' | tr -d '%')
    disk_used=$(echo "$disk_line" | awk '{print $3}')
    disk_total=$(echo "$disk_line" | awk '{print $2}')
    pass "Resources" "Disk usage — ${disk_pct}% (${disk_used} / ${disk_total})"
else
    fail "Resources" "Disk usage" "Could not read disk info"
fi

# Docker images
image_count=$(docker images -q 2>/dev/null | wc -l || echo "0")
pass "Resources" "Docker images — $image_count images"

# Docker volumes
volume_count=$(docker volume ls -q 2>/dev/null | wc -l || echo "0")
pass "Resources" "Docker volumes — $volume_count volumes"

# Docker containers
container_count=$(docker ps -q 2>/dev/null | wc -l || echo "0")
pass "Resources" "Docker containers — $container_count running"

# =====================================================
# Summary
# =====================================================
END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))

case "$OUTPUT_FORMAT" in
    tap)
        echo "TAP version 13"
        echo "1..$TEST_NUM"
        for line in "${TAP_RESULTS[@]}"; do
            echo "$line"
        done
        echo "# passed: $PASSED"
        echo "# failed: $FAILED"
        echo "# skipped: $SKIPPED"
        echo "# duration: ${DURATION}s"
        ;;
    junit)
        {
            echo '<?xml version="1.0" encoding="UTF-8"?>'
            echo "<testsuite name=\"Dokku Smoke Tests\" tests=\"$TEST_NUM\" failures=\"$FAILED\" skipped=\"$SKIPPED\" time=\"$DURATION\" timestamp=\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\">"
            echo "  <properties>"
            echo "    <property name=\"host\" value=\"$HOSTNAME_STR\" />"
            echo "  </properties>"
            for result in "${JUNIT_RESULTS[@]}"; do
                echo "  $result"
            done
            echo "</testsuite>"
        } > "$JUNIT_FILE"
        echo "JUnit results written to $JUNIT_FILE"
        ;;
    *)
        echo ""
        echo "────────────────────────────────────────"
        echo -e "Summary  ${BOLD}$HOSTNAME_STR${NC}"
        echo -e "  ${GREEN}$PASSED passed${NC}  ${RED}$FAILED failed${NC}  ${YELLOW}$SKIPPED skipped${NC}  (${DURATION}s)"
        echo "────────────────────────────────────────"
        if [[ "$FAILED" -eq 0 ]]; then
            echo -e "${GREEN}All tests passed.${NC}"
        else
            echo -e "${RED}Some tests failed.${NC}"
        fi
        ;;
esac

exit $((FAILED > 0 ? 1 : 0))
