#!/usr/bin/env bash

#
# Copyright 2023 The GUAC Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

set -euf -o pipefail

SCRIPT_DIR=$(cd "$(dirname "$0")"; pwd)
GUAC_DIR=$(cd "${SCRIPT_DIR}"/../../..; pwd)

LOG_DIR="${GUAC_DIR}/internal/testing/e2e/logs"
LOG_FILE="${LOG_DIR}/logfile.log"
SCHEMA_BACKUP="${LOG_DIR}/schema_backup.sql"

export PGPASSWORD=$POSTGRES_PASSWORD

# Create the log directory if it doesn't exist
mkdir -p "$LOG_DIR"

# Create the blobstore directory if it doesn't exist
mkdir -p /tmp/blobstore

echo "Script started" | tee -a "$LOG_FILE"

guac_data_hash="254d737ebc42ca32f95033e93370f5427e3fce64"

echo @@@@ Installing gql Python package
pip install gql[all]

echo @@@@ Cloning guac-data
pushd "$GUAC_DIR"
git clone https://github.com/guacsec/guac-data.git
pushd guac-data
git checkout $guac_data_hash
popd
popd

echo @@@@ Starting up guac server in background
go run "${GUAC_DIR}/cmd/guacgql" --gql-backend=ent &

echo -n "Waiting for guac server to start"
set +e
for _ in {1..36} ; do
  sleep 5
  curl -s http://localhost:8080/query >/dev/null 2>&1
  if [ $? -ne 7 ] ; then
    break
  fi
  echo -n "."
done
set -e
echo

curl -s http://localhost:8080/query >/dev/null 2>&1 || (echo "Guac server not running in time" && exit 1)
echo "Guac server started"

duplicate_schema() {
  echo @@@@ Duplicating schema
  pg_dump -h localhost -p 5432 -U guac -d guac -s > "$SCHEMA_BACKUP"
}

restore_schema() {
  echo @@@@ Restoring schema
  psql -h localhost -p 5432 -U guac -d guac -f "$SCHEMA_BACKUP"
}

wipe_data() {
  echo @@@@ Wiping data
  local retries=5
  local count=0
  local success=false

  while [ $count -lt $retries ]; do
    psql -h localhost -p 5432 -U guac -d guac -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;" && success=true && break
    count=$((count + 1))
    echo "Retrying wipe_data ($count/$retries)..."
    sleep 5
  done

  if [ "$success" = true ]; then
    restore_schema
  else
    echo "Error: Failed to wipe data after $retries attempts" | tee -a "$LOG_FILE"
    exit 1
  fi
}

go run ${GUAC_DIR}"/cmd/guacingest" --add-vuln-on-ingest=true --add-license-on-ingest --add-eol-on-ingest &
go run ${GUAC_DIR}"/cmd/guacone" collect deps_dev -p &
go run ${GUAC_DIR}"/cmd/guaccsub" &

# Define ingestion commands
declare -a ingestion_commands=(
  "go run ${GUAC_DIR}/cmd/guacone collect files ${GUAC_DIR}/guac-data/docs/ --add-vuln-on-ingest=true --add-license-on-ingest --add-eol-on-ingest"
  "go run ${GUAC_DIR}/cmd/guaccollect files ${GUAC_DIR}/guac-data/docs/ --service-poll=false"
)

declare -A queryValues

queryValues["PkgQ1"]='.packages |= sort'
queryValues["PkgQ2"]='.packages[].namespaces |= sort'
queryValues["PkgQ3"]='.packages[].namespaces |= sort_by(.namespace) | .packages[].namespaces[].names[].versions |= sort_by(.purl) | .packages[].namespaces[].names[].versions[].qualifiers |= sort_by(.key) | del(.. | .id?)'
queryValues["PkgQ4"]='del(.. | .id?)'
queryValues["IsDependencyQ1"]='.IsDependency |= sort'
queryValues["IsDependencyQ2"]='del(.. | .id?) | del(.. | .origin?)'
queryValues["PathQ1"]='del(.. | .id?) | del(.. | .origin?)'
queryValues["OSVQ1"]='del(.. | .id?)'
queryValues["CertifyVulnQ1"]='del(.. | .id?) | del(.. | .timeScanned?)'
queryValues["ArtifactsQ1"]='.artifacts |= sort'
queryValues["PkgQ9"]='.packages[].namespaces |= sort_by(.namespace) | .packages[].namespaces[].names[].versions |= sort_by(.id) | .packages[].namespaces[].names[].versions[].qualifiers |= sort_by(.key) | del(.. | .id?)'
queryValues["CertifyLegalQ1"]='del(.. | .id?) | del(.. | .timeScanned?) | del(.. | .origin?)'
queryValues["EndOfLifeQ1"]='
  (.HasMetadata[] | select(.key == "endoflife") | .subject.namespaces[].names[].versions[].qualifiers) |= sort_by(.key)
  | del(.. | .id?)
  | del(.. | .timeScanned?)
  | del(.. | .origin?)
'

# Define an indexed array to maintain the order of the queries
queryOrder=(
  "PkgQ1"
  "PkgQ2"
  "PkgQ3"
  "PkgQ4"
  "IsDependencyQ1"
  "IsDependencyQ2"
  "PathQ1"
  "OSVQ1"
  "CertifyVulnQ1"
  "ArtifactsQ1"
  "PkgQ9"
  "CertifyLegalQ1"
  "EndOfLifeQ1"
)

queries="${GUAC_DIR}/demo/graphql/queries.gql"

# Ingestion is asynchronous and how long it takes varies, so waiting a fixed amount of
# time and hoping the data landed is what makes this job flaky. Wait for the expected
# content instead: poll the queries until they match their expect files, then let the
# normal validation below report anything still outstanding.
#
# Note this deliberately does not wait for the graph to stop changing. A polling
# deps_dev collector runs in the background for the whole job, so the data is never
# static and a stability check would either never fire or fire while a freshly wiped
# database was still empty.
#
# PathQ1 is skipped because it needs ids that are only looked up after ingestion.
wait_for_expected_data() {
  local max_wait=${INGESTION_MAX_WAIT:-900}
  local interval=15
  local elapsed=0
  local pending=0
  local scratch
  scratch=$(mktemp)

  echo -n "@@@@ Waiting for ingested data to match the expected results"
  set +e
  while [ "$elapsed" -lt "$max_wait" ]; do
    pending=0
    for query in "${queryOrder[@]}"; do
      [ "$query" == "PathQ1" ] && continue
      gql-cli http://localhost:8080/query -o "$query" < "$queries" 2>/dev/null \
        | jq "${queryValues[$query]}" > "$scratch" 2>/dev/null
      if ! diff -q "${SCRIPT_DIR}/expect${query}.json" "$scratch" >/dev/null 2>&1; then
        pending=$((pending + 1))
      fi
    done

    if [ "$pending" -eq 0 ]; then
      set -e
      rm -f "$scratch"
      echo
      echo "@@@@ Ingested data matched after ${elapsed}s" | tee -a "$LOG_FILE"
      return 0
    fi

    echo -n "."
    sleep "$interval"
    elapsed=$((elapsed + interval))
  done
  set -e
  rm -f "$scratch"
  echo

  # Out of time. Carry on so the validation below prints the actual diffs and fails
  # there, rather than timing out with no explanation of what was missing.
  echo "@@@@ ${pending} quer(y/ies) still not matching after ${max_wait}s, validating anyway" | tee -a "$LOG_FILE"
  return 0
}

duplicate_schema

# Loop through ingestion commands
for command in "${ingestion_commands[@]}"; do
  echo @@@@ Running ingestion command: "$command" | tee -a "$LOG_FILE"
  eval "$command"

  wait_for_expected_data

  echo @@@@ Running queries and validating output

  id1=$(gql-cli http://localhost:8080/query -o PkgQ5 < "$queries" | jq -r ' .packages[0].namespaces[0].names[0].id ')
  id2=$(gql-cli http://localhost:8080/query -o PkgQ6 < "$queries" | jq -r ' .packages[0].namespaces[0].names[0].id ')
  id3=$(gql-cli http://localhost:8080/query -o PkgQ7 < "$queries" | jq -r ' .packages[0].namespaces[0].names[0].id ')
  id4=$(gql-cli http://localhost:8080/query -o PkgQ8 < "$queries" | jq -r ' .packages[0].namespaces[0].names[0].versions[0].id ')

  for query in "${queryOrder[@]}"; do
    echo @@@@ Running query "$query" | tee -a "$LOG_FILE"
    if [ "$query" == "PathQ1" ]; then
      gql-cli http://localhost:8080/query -o "$query" -V subject:"${id1}" target:"${id2}" < "$queries" | jq "${queryValues[$query]}" > "${GUAC_DIR}/got${query}.json"
    else
      gql-cli http://localhost:8080/query -o "$query" < "$queries" | jq "${queryValues[$query]}" > "${GUAC_DIR}/got${query}.json"
    fi
    diff -u "${SCRIPT_DIR}/expect${query}.json" "${GUAC_DIR}/got${query}.json" | tee -a "$LOG_FILE"
  done

  # Run the additional query that doesn't follow the same format
  echo @@@@ Running query path.py | tee -a "$LOG_FILE"
  "${GUAC_DIR}/demo/graphql/path.py" "$id3" "$id4" | tail -n +2 | jq 'del(.. | .id?) | del(.. | .origin?) | del(.. | .qualifiers?)' > "${GUAC_DIR}/gotPathPy.json"
  diff -u "${SCRIPT_DIR}/expectPathPy.json" "${GUAC_DIR}/gotPathPy.json" | tee -a "$LOG_FILE"

  echo @@@@ Fully ingested command: "$command" | tee -a "$LOG_FILE"

  # Wipe data after each ingestion command
  wipe_data
done

# Note: graphql_playground is left running, CI will clean it up