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

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
COMPOSE_FILE="${ROOT_DIR}/examples/production-adapters/docker-compose.external.yml"
KIND_CLUSTER="${RS_ZERO_KIND_CLUSTER:-rs-zero-external}"
KIND_CONTEXT="kind-${KIND_CLUSTER}"
KUBE_TEST_CONTEXT=""
COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-rs-zero-external}"
RS_ZERO_EXTERNAL_ETCD_PORT="${RS_ZERO_EXTERNAL_ETCD_PORT:-12379}"
RS_ZERO_EXTERNAL_REDIS_PORT="${RS_ZERO_EXTERNAL_REDIS_PORT:-16379}"
RS_ZERO_EXTERNAL_OTLP_GRPC_PORT="${RS_ZERO_EXTERNAL_OTLP_GRPC_PORT:-14317}"
RS_ZERO_EXTERNAL_OTLP_HTTP_PORT="${RS_ZERO_EXTERNAL_OTLP_HTTP_PORT:-14318}"
RS_ZERO_EXTERNAL_PYROSCOPE_PORT="${RS_ZERO_EXTERNAL_PYROSCOPE_PORT:-14040}"
RS_ZERO_EXTERNAL_MYSQL_PORT="${RS_ZERO_EXTERNAL_MYSQL_PORT:-13306}"
RS_ZERO_EXTERNAL_POSTGRES_PORT="${RS_ZERO_EXTERNAL_POSTGRES_PORT:-15432}"
export COMPOSE_PROJECT_NAME
export RS_ZERO_EXTERNAL_ETCD_PORT RS_ZERO_EXTERNAL_REDIS_PORT RS_ZERO_EXTERNAL_OTLP_GRPC_PORT
export RS_ZERO_EXTERNAL_OTLP_HTTP_PORT RS_ZERO_EXTERNAL_PYROSCOPE_PORT RS_ZERO_EXTERNAL_MYSQL_PORT RS_ZERO_EXTERNAL_POSTGRES_PORT

DEFAULT_TARGETS=(redis redis-fault redis-lock etcd kubernetes otlp pyroscope mysql postgres)
ALL_TARGETS=("${DEFAULT_TARGETS[@]}" redis-recovery redis-cluster linux-cpu)
STARTED_COMPOSE=0
STARTED_KIND=0

usage() {
  cat <<'USAGE'
Usage: scripts/external-integration.sh [all|redis|redis-fault|redis-lock|redis-recovery|redis-cluster|linux-cpu|etcd|kubernetes|otlp|pyroscope|mysql|postgres ...]

Runs rs-zero ignored external integration tests against local containers.
The default target is all.

Environment overrides:
  COMPOSE_PROJECT_NAME        Docker Compose project name, default rs-zero-external
  RS_ZERO_KIND_CLUSTER        KinD cluster name, default rs-zero-external
  RS_ZERO_KUBE_CONTEXT        Existing Kubernetes context for local runs
  RS_ZERO_SKIP_CLEANUP=1      Keep containers and KinD cluster after the run
  RS_ZERO_EXTERNAL_*_PORT     Override local compose ports when needed
  RS_ZERO_TEST_REDIS_CLUSTER_URL
                              Existing Redis Cluster startup nodes for redis-cluster target
  RS_ZERO_CPU_PRESSURE_*      Linux CPU pressure duration, task count and shedder settings
USAGE
}

log() {
  printf '[external] %s\n' "$*"
}

section() {
  printf '\n[external] === %s ===\n' "$*"
}

fail() {
  printf '[external] error: %s\n' "$*" >&2
  exit 1
}

compose() {
  docker compose -f "${COMPOSE_FILE}" "$@"
}

cleanup() {
  local exit_code=$?
  if [[ "${RS_ZERO_SKIP_CLEANUP:-}" == "1" ]]; then
    log "RS_ZERO_SKIP_CLEANUP=1, keeping external resources"
    return "${exit_code}"
  fi
  if [[ "${STARTED_KIND}" == "1" ]] && command -v kind >/dev/null 2>&1; then
    kind delete cluster --name "${KIND_CLUSTER}" >/dev/null 2>&1 || true
  fi
  if [[ "${STARTED_COMPOSE}" == "1" ]]; then
    compose down -v --remove-orphans >/dev/null 2>&1 || true
  fi
  return "${exit_code}"
}
trap cleanup EXIT

require_command() {
  command -v "$1" >/dev/null 2>&1 || fail "missing required command: $1"
}

contains_target() {
  local needle="$1"
  shift
  local target
  for target in "$@"; do
    [[ "${target}" == "${needle}" ]] && return 0
  done
  return 1
}

validate_targets() {
  local target known allowed
  for target in "$@"; do
    known=0
    for allowed in "${ALL_TARGETS[@]}"; do
      if [[ "${target}" == "${allowed}" ]]; then
        known=1
        break
      fi
    done
    [[ "${known}" == "1" ]] || fail "unknown target: ${target}"
  done
}

expand_targets() {
  if [[ "$#" -eq 0 ]]; then
    printf '%s\n' "${DEFAULT_TARGETS[@]}"
    return
  fi
  local expanded=()
  local target
  for target in "$@"; do
    if [[ "${target}" == "all" ]]; then
      expanded+=("${DEFAULT_TARGETS[@]}")
    else
      expanded+=("${target}")
    fi
  done
  validate_targets "${expanded[@]}"
  printf '%s\n' "${expanded[@]}" | awk '!seen[$0]++'
}

compose_services_for_targets() {
  local services=()
  contains_target redis "$@" && services+=(redis)
  contains_target redis-recovery "$@" && services+=(redis)
  contains_target redis-lock "$@" && services+=(redis)
  contains_target etcd "$@" && services+=(etcd)
  contains_target otlp "$@" && services+=(otel-collector)
  contains_target pyroscope "$@" && services+=(pyroscope)
  contains_target mysql "$@" && services+=(mysql)
  contains_target postgres "$@" && services+=(postgres)
  if [[ "${#services[@]}" -gt 0 ]]; then
    printf '%s\n' "${services[@]}"
  fi
}

container_id_for_service() {
  compose ps -q "$1" | tail -n 1
}

container_health() {
  local container_id="$1"
  docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "${container_id}" 2>/dev/null || true
}

wait_for_compose_services() {
  local services=("$@")
  [[ "${#services[@]}" -eq 0 ]] && return
  section "starting compose services: ${services[*]}"
  compose up -d "${services[@]}"
  STARTED_COMPOSE=1

  local service container_id status
  for service in "${services[@]}"; do
    section "waiting for ${service} health"
    container_id="$(container_id_for_service "${service}")"
    [[ -n "${container_id}" ]] || fail "service ${service} did not create a container"
    for _ in $(seq 1 90); do
      status="$(container_health "${container_id}")"
      if [[ "${status}" == "healthy" || "${status}" == "running" ]]; then
        log "${service} is ${status}"
        break
      fi
      sleep 2
    done
    status="$(container_health "${container_id}")"
    if [[ "${status}" != "healthy" && "${status}" != "running" ]]; then
      compose ps "${service}" >&2 || true
      compose logs --tail=80 "${service}" >&2 || true
      fail "service ${service} health check did not pass, status=${status:-unknown}"
    fi
    compose ps "${service}"
  done
}

ensure_kubernetes_context() {
  require_command kubectl
  if [[ -n "${RS_ZERO_KUBE_CONTEXT:-}" ]]; then
    KUBE_TEST_CONTEXT="${RS_ZERO_KUBE_CONTEXT}"
    log "using Kubernetes context ${KUBE_TEST_CONTEXT}"
    kubectl cluster-info --request-timeout=20s --context "${KUBE_TEST_CONTEXT}"
    return
  fi

  if command -v kind >/dev/null 2>&1; then
    require_command docker
    if kind get clusters | grep -Fxq "${KIND_CLUSTER}"; then
      log "reusing KinD cluster ${KIND_CLUSTER}"
    else
      section "creating KinD cluster ${KIND_CLUSTER}"
      kind create cluster --name "${KIND_CLUSTER}" --wait 120s
      STARTED_KIND=1
    fi
    kubectl config use-context "${KIND_CONTEXT}"
    KUBE_TEST_CONTEXT="${KIND_CONTEXT}"
    kubectl cluster-info --request-timeout=20s --context "${KUBE_TEST_CONTEXT}"
    return
  fi

  KUBE_TEST_CONTEXT="$(select_existing_kubernetes_context)"
  [[ -n "${KUBE_TEST_CONTEXT}" ]] || fail "missing kind and no reachable Kubernetes context; install kind or set RS_ZERO_KUBE_CONTEXT"
  log "kind not found, using reachable Kubernetes context ${KUBE_TEST_CONTEXT}"
  kubectl cluster-info --request-timeout=20s --context "${KUBE_TEST_CONTEXT}"
}

select_existing_kubernetes_context() {
  local current candidate
  local candidates=()
  current="$(kubectl config current-context 2>/dev/null || true)"
  [[ -n "${current}" ]] && candidates+=("${current}")
  candidates+=(docker-desktop minikube "${KIND_CONTEXT}")

  local seen=""
  for candidate in "${candidates[@]}"; do
    [[ -n "${candidate}" ]] || continue
    [[ " ${seen} " == *" ${candidate} "* ]] && continue
    seen="${seen} ${candidate}"
    if kubectl config get-contexts "${candidate}" >/dev/null 2>&1 \
      && kubectl cluster-info --request-timeout=5s --context "${candidate}" >/dev/null 2>&1; then
      printf '%s\n' "${candidate}"
      return
    fi
    printf '[external] Kubernetes context %s is not reachable\n' "${candidate}" >&2
  done
}

run_redis() {
  section "redis external test"
  RS_ZERO_TEST_REDIS_URL="${RS_ZERO_TEST_REDIS_URL:-redis://127.0.0.1:${RS_ZERO_EXTERNAL_REDIS_PORT}}" \
    cargo test --features cache-redis --test cache_integration redis_store_talks_to_external_redis -- --ignored
  RS_ZERO_TEST_REDIS_URL="${RS_ZERO_TEST_REDIS_URL:-redis://127.0.0.1:${RS_ZERO_EXTERNAL_REDIS_PORT}}" \
    cargo test --features cache-redis --test cache_redis_external redis_single_node_cache_roundtrip_and_degradation_config -- --ignored
}

run_redis_recovery() {
  section "redis limiter recovery external test"
  local container_id
  container_id="$(container_id_for_service redis)"
  [[ -n "${container_id}" ]] || fail "redis container is required for redis-recovery target"
  RS_ZERO_TEST_REDIS_URL="${RS_ZERO_TEST_REDIS_URL:-redis://127.0.0.1:${RS_ZERO_EXTERNAL_REDIS_PORT}}" \
  RS_ZERO_TEST_REDIS_CONTAINER="${container_id}" \
    cargo test --features cache-redis,resil --test redis_limiter_recovery_external -- --ignored --test-threads=1
}

run_redis_lock() {
  section "redis distributed lock external test"
  RS_ZERO_TEST_REDIS_URL="${RS_ZERO_TEST_REDIS_URL:-redis://127.0.0.1:${RS_ZERO_EXTERNAL_REDIS_PORT}}" \
    cargo test --features cache-redis,observability --test redis_lock_external -- --ignored --test-threads=1
}


run_linux_cpu() {
  section "linux cpu pressure test"
  if [[ "$(uname -s)" != "Linux" ]]; then
    log "linux-cpu target requires Linux for real CPU pressure; running non-Linux skip check only"
  fi
  cargo test --features resil --test resilience_cpu_linux_external -- --ignored --test-threads=1
}

run_redis_fault() {
  section "redis fault injection and breaker test"
  cargo test --features cache-redis,observability --test cache_redis_degradation
}

run_redis_cluster() {
  section "redis cluster external test"
  [[ -n "${RS_ZERO_TEST_REDIS_CLUSTER_URL:-}" ]] \
    || fail "RS_ZERO_TEST_REDIS_CLUSTER_URL is required for redis-cluster target"
  cargo test --features cache-redis --test cache_redis_cluster
  RS_ZERO_TEST_REDIS_CLUSTER_URL="${RS_ZERO_TEST_REDIS_CLUSTER_URL}" \
    cargo test --features cache-redis --test cache_redis_external redis_cluster_env_is_available_for_manual_moved_ask_validation -- --ignored
}

run_etcd() {
  section "etcd external test"
  RS_ZERO_ETCD_ENDPOINT="${RS_ZERO_ETCD_ENDPOINT:-http://127.0.0.1:${RS_ZERO_EXTERNAL_ETCD_PORT}}" \
    cargo test --no-default-features --features discovery-etcd --test discovery_etcd_external -- --ignored
}

run_kubernetes() {
  section "kubernetes external test"
  ensure_kubernetes_context
  RS_ZERO_KUBE_EXTERNAL=1 RS_ZERO_KUBE_CONTEXT="${KUBE_TEST_CONTEXT}" \
    cargo test --no-default-features --features discovery-kube --test discovery_kube_external -- --ignored
}

run_otlp() {
  section "otlp external test"
  RS_ZERO_OTLP_ENDPOINT="${RS_ZERO_OTLP_ENDPOINT:-http://127.0.0.1:${RS_ZERO_EXTERNAL_OTLP_HTTP_PORT}}" \
    cargo test --no-default-features --features observability,otlp --test observability_otlp_external -- --ignored
}

run_pyroscope() {
  section "pyroscope external test"
  RS_ZERO_PYROSCOPE_ENDPOINT="${RS_ZERO_PYROSCOPE_ENDPOINT:-http://127.0.0.1:${RS_ZERO_EXTERNAL_PYROSCOPE_PORT}}" \
    cargo test --no-default-features --features profiling --test profiling_pyroscope_external -- --ignored
}

run_mysql() {
  section "mysql external test"
  RS_ZERO_MYSQL_URL="${RS_ZERO_MYSQL_URL:-mysql://rs_zero:rs_zero@127.0.0.1:${RS_ZERO_EXTERNAL_MYSQL_PORT}/rs_zero}" \
    cargo test --no-default-features --features db-mysql --test db_external mysql_external_pool_lifecycle -- --ignored
}

run_postgres() {
  section "postgres external test"
  RS_ZERO_POSTGRES_URL="${RS_ZERO_POSTGRES_URL:-postgres://rs_zero:rs_zero@127.0.0.1:${RS_ZERO_EXTERNAL_POSTGRES_PORT}/rs_zero}" \
    cargo test --no-default-features --features db-postgres --test db_external postgres_external_pool_lifecycle -- --ignored
}

main() {
  cd "${ROOT_DIR}"
  if [[ "$#" -eq 1 && ( "$1" == "-h" || "$1" == "--help" ) ]]; then
    usage
    exit 0
  fi
  local targets=()
  local services=()
  local item
  while IFS= read -r item; do
    targets+=("${item}")
  done < <(expand_targets "$@")
  if [[ "${#targets[@]}" -eq 0 ]]; then
    exit 0
  fi
  while IFS= read -r item; do
    [[ -n "${item}" ]] && services+=("${item}")
  done < <(compose_services_for_targets "${targets[@]}")

  if [[ "${#services[@]}" -gt 0 ]] || contains_target kubernetes "${targets[@]}"; then
    require_command docker
  fi

  if [[ "${#services[@]}" -gt 0 ]]; then
    wait_for_compose_services "${services[@]}"
  fi

  local target
  for target in "${targets[@]}"; do
    local function_name="run_${target//-/_}"
    "${function_name}"
  done
}

main "$@"
