#!/usr/bin/env bash
# Parity: the Rust port and the shell version, on the same workspace.
#
# The shell version in ../run is the specification. Every check here runs both
# and compares, because the differences that matter are the ones neither side's
# own tests can see — a path the shell makes absolute and the port does not,
# for instance, which mounts an empty directory and fails at install time.
set -uo pipefail

HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
RUST_DIR="$(cd "${HERE}/.." && pwd)"
SHELL_DIR="$(cd "${RUST_DIR}/../run" && pwd)"
RUST="${RUST_DIR}/target/release/run-stack"

PASS=0
FAIL=0

ok() { PASS=$((PASS + 1)); printf '  ok    %s\n' "$1"; }
bad() {
  FAIL=$((FAIL + 1))
  printf '  FAIL  %s\n' "$1"
  [ $# -gt 1 ] && printf '        %s\n' "$2"
}

# macOS resolves /var through a symlink to /private/var. One side canonicalises
# and the other does not; the directory is the same one either way.
normalise() { printf '%s' "${1#/private}"; }

same() {
  local label="$1" expected="$2" actual="$3"
  expected="$(normalise "${expected}")"
  actual="$(normalise "${actual}")"
  if [ "${expected}" = "${actual}" ]; then
    ok "${label}"
  else
    bad "${label}" "shell: ${expected}"
    printf '        rust:  %s\n' "${actual}"
  fi
}

[ -x "${RUST}" ] || { echo "build first: cargo build --release" >&2; exit 1; }

# ---- a workspace both can read ---------------------------------------------
WORK="$(mktemp -d)"
trap 'rm -rf "${WORK}"' EXIT
mkdir -p "${WORK}/.run" "${WORK}/backend" "${WORK}/platform/apps/web" \
         "${WORK}/platform/apps/reports" "${WORK}/platform/apps/courier"
touch "${WORK}/backend/artisan"
printf 'packages:\n' > "${WORK}/platform/pnpm-workspace.yaml"
echo '{"name":"mono","private":true}' > "${WORK}/platform/package.json"
echo '{"name":"web","dependencies":{"vite":"5"}}' > "${WORK}/platform/apps/web/package.json"
echo '{"name":"reports","dependencies":{"vite":"5"}}' > "${WORK}/platform/apps/reports/package.json"
echo '{"name":"courier","dependencies":{"expo":"51"}}' > "${WORK}/platform/apps/courier/package.json"

cat > "${WORK}/.run/run.config.json" <<'JSON'
{
  "project": { "COMPOSE_PROJECT_NAME": "parity", "PROJECT_LABEL": "Parity" },
  "repositories": { "BACKEND_DIR": "./backend", "FRONTEND_DIR": "./platform" },
  "backend": { "BACKEND_STACK": "laravel", "RUN_QUEUE": true, "RUN_SCHEDULER": false },
  "apps": {
    "WEB_APP": "web", "RUN_ADMIN": false, "RUN_LANDING": false,
    "RUN_MOBILE": false, "RUN_DESKTOP": false,
    "EXTRA_APPS": "reports courier",
    "REPORTS_CMD": "pnpm --filter reports run dev"
  },
  "infrastructure": {
    "DB_ENGINE": "postgres", "DB_DATABASE": "parity", "DB_USERNAME": "parity",
    "DB_PASSWORD": "secret", "RUN_REDIS": false, "RUN_MAILPIT": true, "RUN_MINIO": false
  },
  "ports": {
    "BACKEND_PORT": 8072, "WEB_PORT": 5156, "REPORTS_PORT": 5180,
    "COURIER_PORT": 8082, "POSTGRES_PORT": 5467, "DASHBOARD_PORT": 8066
  }
}
JSON

run_shell() { RUN_WORKSPACE_DIR="${WORK}" RUN_PACKAGE_DIR="${SHELL_DIR}" bash "${SHELL_DIR}/run.sh" "$@"; }
run_rust()  { RUN_WORKSPACE_DIR="${WORK}" RUN_PACKAGE_DIR="${SHELL_DIR}" "${RUST}" "$@"; }

# The shell writes .run/.env from the config; both read it from there. A full
# init, not --update: --update stops at "No new apps" and writes nothing.
run_shell init --yes --force >/dev/null 2>&1
[ -f "${WORK}/.run/.env" ] || { echo "the shell version wrote no .env — cannot compare" >&2; exit 1; }

echo "== parity: environment =="

# Every value the shell exports for compose, and the port's view of the same.
# The shell's own prologue: run.sh resolves the workspace before common.sh is
# of any use, and sourcing common.sh alone silently reads the package instead.
shell_value() {
  RUN_WORKSPACE_DIR="${WORK}" RUN_PACKAGE_DIR="${SHELL_DIR}" bash -c "
    export RUN_PACKAGE_DIR='${SHELL_DIR}'
    source '${SHELL_DIR}/scripts/workspace.sh'
    resolve_workspace '${WORK}'
    source '${SHELL_DIR}/scripts/common.sh'
    load_env
    printf '%s' \"\${$1:-}\"
  " 2>/dev/null
}
rust_value() { run_rust env "$1" 2>/dev/null; }

for key in FRONTEND_DIR BACKEND_DIR BACKEND_APP_DIR DB_CONNECTION DB_HOST \
           DATABASE_URL S3_ENDPOINT PROJECT_NAME; do
  same "${key}" "$(shell_value "${key}")" "$(rust_value "${key}")"
done

echo
echo "== parity: the compose command =="

# The shell's dc() argument list, without running docker.
shell_compose_args() {
  RUN_WORKSPACE_DIR="${WORK}" RUN_PACKAGE_DIR="${SHELL_DIR}" bash -c "
    export RUN_PACKAGE_DIR='${SHELL_DIR}'
    source '${SHELL_DIR}/scripts/workspace.sh'
    resolve_workspace '${WORK}'
    source '${SHELL_DIR}/scripts/common.sh'
    load_env
    docker() { printf '%s\n' \"\$@\"; }
    dc ps
  " 2>/dev/null
}

shell_args="$(shell_compose_args ps)"
rust_args="$(run_rust explain ps 2>/dev/null | sed 's/^docker //' | tr ' ' '\n')"

same "compose files" \
  "$(printf '%s\n' "${shell_args}" | grep -c 'docker-compose')" \
  "$(printf '%s\n' "${rust_args}" | grep -c 'docker-compose')"
same "profiles" \
  "$(printf '%s\n' "${shell_args}" | grep -A1 -- '--profile' | grep -v -- '--profile\|^--$' | sort | tr '\n' ' ')" \
  "$(printf '%s\n' "${rust_args}" | grep -A1 -- '--profile' | grep -v -- '--profile\|^--$' | sort | tr '\n' ' ')"

echo
echo "== parity: the config file =="
run_rust config > "${WORK}/rust.toml"
if [ -f "${WORK}/.run/run.config.toml" ]; then
  ok "config migrated to run.config.toml"
else
  bad "config migrated to run.config.toml"
fi
if python3 -c "
import sys
try:
    import tomllib
except ImportError:
    sys.exit(0)
a = tomllib.load(open('${WORK}/.run/run.config.toml', 'rb'))
b = tomllib.load(open('${WORK}/rust.toml', 'rb'))
flat = lambda d: {k: v for s in d.values() if isinstance(s, dict) for k, v in s.items() if not isinstance(v, dict)}
# Compare flat settings; essential may be added by the rust side.
af, bf = flat(a), flat(b)
sys.exit(0 if af == bf else 1)"; then
  ok "config round-trips with the same settings"
else
  bad "config round-trips with the same settings"
fi

echo
echo "== parity: the generated overlays =="

# Both write the same three files into .run/. The header names whichever wrote
# it, so compare from the second line.
mkdir -p "${WORK}/shell-overlays"
rm -f "${WORK}"/.run/docker-compose.*.yml
for gen in gen-packages gen-apps gen-resources; do
  RUN_DIR="${WORK}/.run" PACKAGE_DIR="${SHELL_DIR}" RUN_PACKAGE_DIR="${SHELL_DIR}" \
    WORKSPACE_DIR="${WORK}" ENV_FILE="${WORK}/.run/.env" \
    bash "${SHELL_DIR}/scripts/${gen}.sh" >/dev/null 2>&1
done
cp "${WORK}"/.run/docker-compose.*.yml "${WORK}/shell-overlays/" 2>/dev/null
rm -f "${WORK}"/.run/docker-compose.*.yml
run_rust generate >/dev/null 2>&1

for name in packages extra resources; do
  theirs="${WORK}/shell-overlays/docker-compose.${name}.yml"
  ours="${WORK}/.run/docker-compose.${name}.yml"
  if [ ! -f "${theirs}" ] && [ ! -f "${ours}" ]; then
    ok "${name}: neither writes one"
  elif [ ! -f "${theirs}" ] || [ ! -f "${ours}" ]; then
    bad "${name}: only one of them wrote a file"
  elif diff <(tail -n +2 "${theirs}") <(tail -n +2 "${ours}") >/dev/null; then
    ok "${name}: identical ($(wc -l < "${ours}" | tr -d ' ') lines)"
  else
    bad "${name}: differs" "$(diff <(tail -n +2 "${theirs}") <(tail -n +2 "${ours}") | head -4 | tr '\n' ' ')"
  fi
done

echo
echo "== parity: ports =="
same "ports listed" \
  "$(run_shell ports 2>/dev/null | grep -oE '[A-Z_]+_PORT' | sort -u | tr '\n' ' ')" \
  "$(run_rust ports 2>/dev/null | grep -oE '[A-Z_]+_PORT' | sort -u | tr '\n' ' ')"

echo
if [ "${FAIL}" -eq 0 ]; then
  echo "parity ok (${PASS} checks)"
  exit 0
fi
echo "parity failed: ${FAIL} of $((PASS + FAIL)) checks" >&2
exit 1
