#!/usr/bin/env bash

#script/build

OUTPUT=$(mktemp)
RESULT=-1

SRC_GIT_DAEMON_PORT=9419
SRC_GIT_DAEMON_OUTPUT=$(mktemp)
SRC_GIT_DAEMON_PID=-1

DEST_GIT_DAEMON_PORT=9420
DEST_GIT_DAEMON_OUTPUT=$(mktemp)
DEST_GIT_DAEMON_PID=-1

DEST_API_PORT=8081
DEST_API_OUTPUT=$(mktemp)
DEST_API_PID=-1
trap "after_suite" EXIT
trap "after_suite" SIGINT

function test_version() {
  version "version shouldn't require any flags"

  echo "all version tests passed"
}

function test_pull() {
  # Pull new repo
  setup_src "org/repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142"
  setup_cache
  pull --repo-name "org/repo" "pulling new repo"
  assert_cache_sha "org/repo" "heads/main" "a5984bb887dd2fcdc2892cd906d6f004844d1142" "pulling new repo org/repo"

  echo "all pull tests passed successfully"
}

function test_push() {
  # Push with a new change to main
  setup_cache "org/repo:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52"
  setup_dest "org/repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142"

  push "pushing new commit to main"
  assert_dest_sha "org/repo" "heads/main" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "updating org/repo:heads/main to new commit"

  # Push a non-linear change
  setup_cache "org/repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142"
  setup_dest "org/repo:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52"

  push "pushing to an old commit to main"
  assert_dest_sha "org/repo" "heads/main" "a5984bb887dd2fcdc2892cd906d6f004844d1142" "updating heads/main to an old commit"

  # Push with no changes to main
  setup_cache "org/repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142"
  setup_dest "org/repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142"
  push "pushing no changes to main"
  assert_dest_sha "org/repo" "heads/main" "a5984bb887dd2fcdc2892cd906d6f004844d1142" "leaving org/repo:heads/main at existing commit"

  # Push multiple branches
  setup_cache "org/repo:heads/change:e9009d51dd6da2c363d1d14779c53dd27fcb0c52" \
    "org/repo:heads/nochange:a5984bb887dd2fcdc2892cd906d6f004844d1142"
  setup_dest "org/repo:heads/change:a5984bb887dd2fcdc2892cd906d6f004844d1142" \
    "org/repo:heads/nochange:a5984bb887dd2fcdc2892cd906d6f004844d1142"

  push "pushing multiple branches"
  assert_dest_sha "org/repo" "heads/change" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "updating org/repo:heads/change to new commit"
  assert_dest_sha "org/repo" "heads/nochange" "a5984bb887dd2fcdc2892cd906d6f004844d1142" "leaving org/repo:heads/nochange at existing commit"

  # Pushing multiple branches and tags
  setup_cache "org/repo:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52" \
    "org/repo:tags/v1:e9009d51dd6da2c363d1d14779c53dd27fcb0c52"
  setup_dest "org/repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142" \
    "org/repo:tags/v1:a5984bb887dd2fcdc2892cd906d6f004844d1142"

  push "pushing multiple branches and tags"
  assert_dest_sha "org/repo" "heads/main" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "updating org/repo:heads/main to new commit"
  assert_dest_sha "org/repo" "tags/v1" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "updating org/repo:tags/v1 to new commit"

  # Pushing multiple repositories
  setup_cache "org1/repo1:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52" \
    "org1/repo2:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52" \
    "org1/repo2:tags/v1:e9009d51dd6da2c363d1d14779c53dd27fcb0c52" \
    "org2/repo1:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52"
  setup_dest "org1/repo1:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142" \
    "org1/repo2:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142" \
    "org1/repo2:tags/v1:a5984bb887dd2fcdc2892cd906d6f004844d1142" \
    "org2/repo1:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142"

  push "pushing multiple repositories"
  assert_dest_sha "org1/repo1" "heads/main" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "updating org1/repo1:heads/main to new commit"
  assert_dest_sha "org1/repo2" "heads/main" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "updating org1/repo2:heads/main to new commit"
  assert_dest_sha "org1/repo2" "tags/v1" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "updating org1/repo2:tags/v1 to new commit"
  assert_dest_sha "org2/repo1" "heads/main" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "updating org2/repo1:tags/v1 to new commit"

  # Honor --repo-name flag, ignore other cache entries
  setup_cache "org/repo1:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52" \
    "org/repo2:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52"
  setup_dest "org/repo1:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142" \
    "org/repo2:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142"

  push2args --repo-name "org/repo1" "pushing only one of the repos in the cache"
  assert_dest_sha "org/repo1" "heads/main" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "updating org/repo1 passed in repo flag"
  assert_dest_sha "org/repo2" "heads/main" "a5984bb887dd2fcdc2892cd906d6f004844d1142" "org/repo2 not updated despite cache"

  # Push to pre-existing org
  setup_cache "org-already-exists/new-repo:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52"
  setup_dest "org-already-exists/new-repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142"

  push "pushing to existing org"
  assert_dest_sha "org-already-exists/new-repo" "heads/main" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "updating org-already-exists/new-repo"

  # Push to pre-existing repo
  setup_cache "org-already-exists/repo-already-exists:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52"
  setup_dest "org-already-exists/repo-already-exists:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142"

  push "pushing to existing repo"
  assert_dest_sha "org-already-exists/repo-already-exists" "heads/main" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "updating org-already-exists/repo-already-exists"

  # Push to repo in user's account
  setup_cache "monalisa/new-repo:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52"
  setup_dest "monalisa/new-repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142"

  push "pushing to authenticated user's account"
  assert_dest_sha "monalisa/new-repo" "heads/main" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "updating monalisa/new-repo"

  # Push to GHAE with impersonation
  setup_cache "org-already-exists/ghae-repo:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52"
  setup_dest "org-already-exists/ghae-repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142"
  push_impersonation "ghae-admin" "pushing to GHAE repo"

  # Push to GHES with impersonation
  setup_cache "org-already-exists/ghes-repo:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52"
  setup_dest "org-already-exists/ghes-repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142"
  push_impersonation "ghes-admin" "pushing to GHES repo"

  echo "all push tests passed successfully"
}

function test_sync() {
  # Sync no change without cached
  setup_src "org/repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142"
  setup_cache
  setup_dest "org/repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142"
  sync --repo-name "org/repo" "syncing a new commit in an uncached repo"
  assert_dest_sha "org/repo" "heads/main" "a5984bb887dd2fcdc2892cd906d6f004844d1142" "syncing no changes without cache"

  # Sync a new commit without cache
  setup_src "org/repo:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52"
  setup_cache
  setup_dest "org/repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142"
  sync --repo-name "org/repo" "syncing a new commit in an uncached repo"
  assert_dest_sha "org/repo" "heads/main" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "syncing a new commit without cache"

  # Sync no change with uncached
  setup_src "org/repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142"
  setup_cache "org/repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142"
  setup_dest "org/repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142"
  sync --repo-name "org/repo" "syncing a new commit in an uncached repo"
  assert_dest_sha "org/repo" "heads/main" "a5984bb887dd2fcdc2892cd906d6f004844d1142" "syncing no changes with cache"

  # Sync a new commit with cache
  setup_src "org/repo:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52"
  setup_cache "org/repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142"
  setup_dest "org/repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142"
  sync --repo-name "org/repo" "syncing a new commit in an uncached repo"
  assert_dest_sha "org/repo" "heads/main" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "syncing a new commit with cache"

  # Sync a non-linear change without cache
  setup_src "org/repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142"
  setup_cache
  setup_dest "org/repo:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52"

  sync --repo-name "org/repo" "syncing an old commit in an uncached repo"
  assert_dest_sha "org/repo" "heads/main" "a5984bb887dd2fcdc2892cd906d6f004844d1142" "syncing heads/main to an old commit without cache"

  # Sync a non-linear change with cache
  setup_src "org/repo:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142"
  setup_cache "org/repo:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52"
  setup_dest "org/repo:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52"

  sync --repo-name "org/repo" "syncing an old commit in an cached repo"
  assert_dest_sha "org/repo" "heads/main" "a5984bb887dd2fcdc2892cd906d6f004844d1142" "syncing heads/main to an old commit with cache"

  # Sync to a different repo
  setup_src "org/repo:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52"
  setup_cache
  setup_dest "org2/repo2:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142"

  sync --repo-name "org/repo:org2/repo2" "syncing org/repo aliased to org2/repo2"
  assert_dest_sha "org2/repo2" "heads/main" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "syncing org/repo aliased to org2/repo2"

  # Honor --repo-name flag, ignore other cache entries
  setup_cache "org/repo1:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52" \
    "org/repo2:heads/main:e9009d51dd6da2c363d1d14779c53dd27fcb0c52"
  setup_dest "org/repo1:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142" \
    "org/repo2:heads/main:a5984bb887dd2fcdc2892cd906d6f004844d1142"

  sync --repo-name "org/repo1" "syncing only one of the repos in the cache"
  assert_dest_sha "org/repo1" "heads/main" "e9009d51dd6da2c363d1d14779c53dd27fcb0c52" "updating org/repo1 passed in repo flag"
  assert_dest_sha "org/repo2" "heads/main" "a5984bb887dd2fcdc2892cd906d6f004844d1142" "org/repo2 not updated despite cache"

  echo "all sync tests passed successfully"
}

function before_suite() {
  set -e
  $(
    rm -rf test/tmp
    mkdir -p test/tmp/dest
    mkdir -p test/tmp/src
  ) >/dev/null

  git daemon \
    --reuseaddr \
    --verbose \
    --export-all \
    --enable=receive-pack \
    --port="$SRC_GIT_DAEMON_PORT" \
    --base-path=test/tmp/src &>$SRC_GIT_DAEMON_OUTPUT &
  SRC_GIT_DAEMON_PID=$!

  git daemon \
    --reuseaddr \
    --verbose \
    --export-all \
    --enable=receive-pack \
    --port="$DEST_GIT_DAEMON_PORT" \
    --base-path=test/tmp/dest &>$DEST_GIT_DAEMON_OUTPUT &
  DEST_GIT_DAEMON_PID=$!

  # wait for the git daemons to start before continuing

  if ! check_git_daemon $SRC_GIT_DAEMON_PORT; then
    echo "timeout waiting for src git daemon to start"
    exit 1
  fi

  if ! check_git_daemon $DEST_GIT_DAEMON_PORT; then
    echo "timeout waiting for dest git daemon to start"
    exit 1
  fi

  go run test/github.go \
    -p "$DEST_API_PORT" \
    -git-daemon-url "git://localhost:$DEST_GIT_DAEMON_PORT/" &>$DEST_API_OUTPUT &
  DEST_API_PID=$!

  script/build

  until $(curl --output /dev/null --silent --head --fail "http://localhost:$DEST_API_PORT/ping"); do
    sleep 0.1
  done

  set +e
}

function after_suite() {
  kill "$SRC_GIT_DAEMON_PID" "$DEST_GIT_DAEMON_PID" "$DEST_API_PID"
}

function setup_src() {
  rm -rf test/tmp/src
  mkdir -p test/tmp/src

  for r in "$@"; do
    local nwo=$(echo $r | cut -d':' -f1)
    local refname=$(echo $r | cut -d':' -f2)
    local sha=$(echo $r | cut -d':' -f3)

    local org=$(echo $nwo | cut -d'/' -f1)
    mkdir -p test/tmp/src/$org && cp -R test/fixtures/repo.git test/tmp/src/$nwo
    echo "$sha" >"test/tmp/src/$nwo/refs/$refname"
  done
}

function setup_cache() {
  rm -rf test/tmp/cache
  mkdir -p test/tmp/cache

  for r in "$@"; do
    local nwo=$(echo $r | cut -d':' -f1)
    local refname=$(echo $r | cut -d':' -f2)
    local sha=$(echo $r | cut -d':' -f3)
    mkdir -p test/tmp/cache/$nwo && cp -R test/fixtures/repo.git test/tmp/cache/$nwo/.git
    echo "$sha" >"test/tmp/cache/$nwo/.git/refs/$refname"
  done
}

function setup_dest() {
  rm -rf test/tmp/dest
  mkdir -p test/tmp/dest

  for r in "$@"; do
    local nwo=$(echo $r | cut -d':' -f1)
    local refname=$(echo $r | cut -d':' -f2)
    local sha=$(echo $r | cut -d':' -f3)
    mkdir -p test/tmp/dest/$nwo && cp -R test/fixtures/repo.git test/tmp/dest/$nwo/.git
    echo "$sha" >"test/tmp/dest/$nwo/.git/refs/$refname"
  done
}

function version() {
  bin/actions-sync version \
    &>$OUTPUT ||
    fail $1
}

function pull() {
  bin/actions-sync pull \
    --cache-dir "test/tmp/cache" \
    --source-url "git://localhost:$SRC_GIT_DAEMON_PORT" \
    "$1" "$2" \
    &>$OUTPUT ||
    fail $3
}

function push() {
  bin/actions-sync push \
    --cache-dir "test/tmp/cache" \
    --disable-push-git-auth \
    --destination-token "token" \
    --destination-url "http://localhost:$DEST_API_PORT" \
    &>$OUTPUT ||
    fail "$1"
}

function push2args() {
  bin/actions-sync push \
    --cache-dir "test/tmp/cache" \
    --disable-push-git-auth \
    --destination-token "token" \
    --destination-url "http://localhost:$DEST_API_PORT" \
    "$1" "$2" \
    &>$OUTPUT ||
    fail $3
}

function push_impersonation() {
  bin/actions-sync push \
    --cache-dir "test/tmp/cache" \
    --disable-push-git-auth \
    --destination-token "token" \
    --destination-url "http://localhost:$DEST_API_PORT" \
    --actions-admin-user $1 \
    &>$OUTPUT ||
    fail "$2"
}

function sync() {
  bin/actions-sync sync \
    --cache-dir "test/tmp/cache" \
    --source-url "git://localhost:$SRC_GIT_DAEMON_PORT" \
    --disable-push-git-auth \
    --destination-token "token" \
    --destination-url "http://localhost:$DEST_API_PORT" \
    "$1" "$2" \
    &>$OUTPUT ||
    fail $3
}

function assert_cache_sha() {
  nwo=$1
  ref=$2
  expected=$3
  actual=$(cat "test/tmp/cache/$nwo/.git/refs/$ref")
  [ "$actual" == "$expected" ] || fail "unexpected cache sha \`$expected != $actual\` - \`$nwo\` \`$ref\` - \`$4\`"
}

function assert_dest_sha() {
  nwo=$1
  ref=$2
  expected=$3
  actual=$(cat "test/tmp/dest/$nwo/.git/refs/$ref")
  [ "$actual" == "$expected" ] || fail "unexpected dest sha \`$expected != $actual\` - \`$nwo\` \`$ref\` - \`$4\`"
}

function fail() {
  MSG=$1
  echo "FAIL: Failed $MSG"
  echo -----output-----
  cat $OUTPUT
  echo -----/output-----

  echo -----github git daemon output-----
  cat $SRC_GIT_DAEMON_OUTPUT
  echo -----/github git daemon output-----

  echo -----ghes git daemon output-----
  cat $DEST_GIT_DAEMON_OUTPUT
  echo -----/ghes git daemon output-----

  echo -----ghes api output-----
  cat $DEST_API_OUTPUT
  echo -----/ghes api output-----
  exit 1
}

function check_git_daemon {
  port=$1
  for i in {1..10}; do
    process=$(ps aux | grep "git daemon" | grep $port)
    if [ -z "$process" ]; then
      echo "Waiting for git daemon to start on port $port..."
      sleep 1
    else
      echo "Git daemon is running on port $port"
      return 0
    fi
  done
  echo "Git daemon did not start within the expected time."
  return 1
}

test_version
before_suite
test_pull
test_push
test_sync
