#!/usr/bin/env bash

# shellcheck source-path=SCRIPTDIR
source "$TEST_ROOT/style.sh"

# shellcheck disable=SC2034,SC2016
LOCKFILE_HEADER='# @generated - this file is auto-generated by `mise lock` https://mise.jdx.dev/dev-tools/mise-lock.html'

fail() {
  title="E2E assertion failed" err "$*"
  exit 1
}

# Portable timeout helper for e2e tests.
# Uses system timeout/gtimeout when available, otherwise falls back to a
# shell implementation that returns 124 on timeout.
run_with_timeout() {
  local seconds="$1"
  shift

  local output_file
  output_file="$(mktemp)"

  ("$@" >"$output_file" 2>&1) &
  local cmd_pid=$!
  local elapsed=0
  local timed_out=0

  while kill -0 "$cmd_pid" 2>/dev/null; do
    if ((elapsed >= seconds * 10)); then
      timed_out=1
      kill -TERM "$cmd_pid" 2>/dev/null || true
      sleep 0.2
      kill -KILL "$cmd_pid" 2>/dev/null || true
      wait "$cmd_pid" 2>/dev/null || true
      break
    fi
    sleep 0.1
    elapsed=$((elapsed + 1))
  done

  local status=0
  if ((timed_out == 1)); then
    status=124
  else
    wait "$cmd_pid" || status=$?
  fi

  cat "$output_file"
  rm -f "$output_file"
  return "$status"
}

parse_timeout_seconds() {
  local duration="$1"
  if [[ ! $duration =~ ^[0-9]+[smh]?$ ]]; then
    return 1
  fi
  case "$duration" in
    *s)
      echo "${duration%s}"
      ;;
    *m)
      echo $((10#${duration%m} * 60))
      ;;
    *h)
      echo $((10#${duration%h} * 3600))
      ;;
    *)
      echo $((10#$duration))
      ;;
  esac
}

timeout() {
  if type -P timeout >/dev/null 2>&1; then
    command timeout "$@"
    return $?
  fi
  if type -P gtimeout >/dev/null 2>&1; then
    command gtimeout "$@"
    return $?
  fi
  if [[ $# -lt 2 ]]; then
    echo "timeout fallback requires DURATION and COMMAND" >&2
    return 125
  fi
  local seconds
  seconds="$(parse_timeout_seconds "$1")" || {
    echo "unsupported timeout duration: $1" >&2
    return 125
  }
  shift
  run_with_timeout "$seconds" "$@"
}

wait_for_file() {
  local file="$1"
  local description="${2:-file}"
  local attempts="${3:-30}"
  local pid="${4:-}"

  for ((i = 0; i < attempts; i++)); do
    if [[ -f $file ]]; then
      return 0
    fi
    if [[ -n $pid ]] && ! kill -0 "$pid" 2>/dev/null; then
      fail "$description process $pid exited before writing $file"
    fi
    sleep 1
  done

  fail "$description did not appear within ${attempts}s: $file"
}

# Safeguard against running the test directly, which would execute in the actual user home
[[ -n ${TEST_NAME:-} ]] || fail "tests should be called using run_test"

quiet_assert_succeed() {
  local status=0
  debug "$ $1"
  bash -c "$1" || status=$?
  if [[ $status -ne 0 ]]; then
    fail "[$1] command failed with status $status"
  fi
}
quiet_assert_fail() {
  local status=0
  debug "$ $1"
  MISE_FRIENDLY_ERROR=1 RUST_BACKTRACE=0 bash -c "$1 2>&1" || status=$?
  if [[ $status -eq 0 ]]; then
    fail "[$1] command succeeded but was expected to fail"
  fi
}

assert_succeed() {
  if quiet_assert_succeed "$1"; then
    ok "[$1] expected success"
  fi
}

assert_fail() {
  local actual
  actual="$(quiet_assert_fail "$1")"
  if [[ -z ${2:-} ]]; then
    ok "[$1] expected failure"
  elif [[ $actual == *"$2"* ]]; then
    ok "[$1] output is equal to '$2'"
  else
    fail "[$1] expected '$2' but got '$actual'"
  fi
}

assert() {
  local actual
  actual="$(quiet_assert_succeed "$1")"
  if [[ -z ${2:-} ]]; then
    ok "[$1]"
  elif [[ $actual == "$2" ]]; then
    ok "[$1] output is equal to '$2'"
  else
    fail "[$1] expected '$2' but got '$actual'"
  fi
}

assert_json() {
  local actual
  actual="$(quiet_assert_succeed "$1")"
  if jq -e . >/dev/null <<<"$actual"; then
    ok "[$1] output is valid JSON"
  else
    fail "[$1] output is not valid JSON"
  fi

  actual_json="$(jq . <<<"$actual")"
  expected_json="$(jq . <<<"$2")"
  if [[ $actual_json == "$expected_json" ]]; then
    ok "[$1] output is equal to '$2'"
  else
    diff --side-by-side <(jq . <<<"$expected_json") <(jq . <<<"$actual_json") || true
    fail "JSON output from [$1] is different from expected"
  fi
}

assert_json_partial_array() {
  local command="$1" fields="$2" expected="$3"

  local actual
  actual="$(quiet_assert_succeed "$command")"

  local filter="map({$fields})"
  local actual_filtered expected_filtered

  actual_filtered="$(jq -S "$filter" <<<"$actual")"
  expected_filtered="$(jq -S "$filter" <<<"$expected")"

  if [[ $actual_filtered == "$expected_filtered" ]]; then
    ok "[$command] partial array match successful"
  else
    echo "Expected:"
    echo "$expected_filtered"
    echo "Got:"
    echo "$actual_filtered"
    fail "[$command] partial array match failed"
  fi
}

assert_json_partial_object() {
  local command="$1" fields="$2" expected="$3"

  local actual
  actual="$(quiet_assert_succeed "$command")"

  # shellcheck disable=SC2016
  local filter='with_entries(select(.key as $k | ($fields | split(",")) | contains([$k])))'
  local actual_filtered expected_filtered

  actual_filtered="$(jq -S --arg fields "$fields" "$filter" <<<"$actual")"
  expected_filtered="$(jq -S --arg fields "$fields" "$filter" <<<"$expected")"

  if [[ $actual_filtered == "$expected_filtered" ]]; then
    ok "[$command] partial object match successful"
  else
    echo "Expected:"
    echo "$expected_filtered"
    echo "Got:"
    echo "$actual_filtered"
    fail "[$command] partial object match failed"
  fi
}

assert_not() {
  local actual
  debug "$ $1"
  actual="$(bash -c "$1" || true)"
  if [[ $actual != "$2" ]]; then
    ok "[$1] output is different from '$2'"
  else
    fail "[$1] expected '$2' not to be in '$actual'"
  fi
}

assert_contains() {
  local actual
  actual="$(quiet_assert_succeed "$1")"
  if [[ $actual == *"$2"* ]]; then
    ok "[$1] '$2' is in output"
  else
    fail "[$1] expected '$2' to be in '$actual'"
  fi
}

assert_contains_text() {
  local actual="$1"
  if [[ $actual == *"$2"* ]]; then
    ok "[text] '$2' is in output"
  else
    fail "[text] expected '$2' to be in output"
  fi
}

assert_not_contains() {
  local actual
  actual="$(quiet_assert_succeed "$1")"
  if [[ $actual != *"$2"* ]]; then
    ok "[$1] '$2' is not in output"
  else
    fail "[$1] expected '$2' not to be in '$actual'"
  fi
}

assert_not_contains_text() {
  local actual="$1"
  if [[ $actual != *"$2"* ]]; then
    ok "[text] '$2' is not in output"
  else
    fail "[text] expected '$2' not to be in output"
  fi
}

assert_matches() {
  local actual
  actual="$(quiet_assert_succeed "$1")"
  if [[ $actual =~ $2 ]]; then
    ok "[$1] '$2' matches output"
  else
    fail "[$1] expected '$2' to match '$actual'"
  fi
}

assert_fail_contains() {
  local actual
  actual="$(quiet_assert_fail "$1")"
  if [[ $actual == *"$2"* ]]; then
    ok "[$1] '$2' is in failure output"
  else
    fail "[$1] expected '$2' to be in '$actual'"
  fi
}

assert_fail_matches() {
  local actual
  actual="$(quiet_assert_fail "$1")"
  if [[ $actual =~ $2 ]]; then
    ok "[$1] failure matches '$2'"
  else
    fail "[$1] expected failure to match '$2' but got '$actual'"
  fi
}

assert_empty() {
  local actual
  actual="$(quiet_assert_succeed "$1")"
  if [[ -z $actual ]]; then
    ok "[$1] output is empty"
  else
    fail "[$1] expected empty output but got '$actual'"
  fi
}

assert_not_empty() {
  local actual
  actual="$(quiet_assert_succeed "$1")"
  if [[ -n $actual ]]; then
    ok "[$1] output is not empty"
  else
    fail "[$1] expected non-empty output but got empty"
  fi
}

assert_directory_exists() {
  if [[ -d $1 ]]; then
    ok "[$1] directory exists"
  else
    fail "[$1] directory does not exist"
  fi
}

assert_directory_not_exists() {
  if [[ ! -d $1 ]]; then
    ok "[$1] directory does not exist"
  else
    fail "[$1] directory exists"
  fi
}

assert_directory_empty() {
  if [[ -z "$(ls -A "$1")" ]]; then
    ok "[$1] directory is empty"
  else
    fail "[$1] directory is not empty"
  fi
}

assert_directory_not_empty() {
  if [[ -n "$(ls -A "$1")" ]]; then
    ok "[$1] directory is not empty"
  else
    fail "[$1] directory is empty"
  fi
}

require_cmd() {
  if ! type -p "$1" >/dev/null; then
    title="E2E test $TEST_NAME aborted" err "'$1' is required but was not found in PATH"
    exit 2
  fi
}

# Detect platform key for lockfile tests
# Sets: MISE_PLATFORM, MISE_PLATFORM_OS, MISE_PLATFORM_ARCH
detect_platform() {
  local os arch
  os=$(uname -s)
  arch=$(uname -m)
  case "$os" in
    Darwin)
      MISE_PLATFORM_OS="macos"
      ;;
    Linux)
      MISE_PLATFORM_OS="linux"
      ;;
    *)
      MISE_PLATFORM_OS="unknown"
      ;;
  esac
  case "$arch" in
    arm64 | aarch64)
      MISE_PLATFORM_ARCH="arm64"
      ;;
    *)
      MISE_PLATFORM_ARCH="x64"
      ;;
  esac
  MISE_PLATFORM="${MISE_PLATFORM_OS}-${MISE_PLATFORM_ARCH}"
  export MISE_PLATFORM MISE_PLATFORM_OS MISE_PLATFORM_ARCH
}
