# Common definitions for REST API test scripts, including manipulation of JSON.
#
# Copyright 2014 Serval Project Inc.
# Copyright 2018 Flinders University
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

# Setup function:
# - ensure that the curl(1) and jq(1) utilities are available
setup_rest_utilities() {
   setup_curl 7
   setup_json
}

# Setup function:
# - configure log diagnostics that are useful for debugging REST requests
# - configure some standard REST API usernames/passwords
setup_rest_config() {
   local _instance="$1"
   [ -z "$_instance" ] || push_and_set_instance $_instance || return $?
   executeOk_servald config \
      set log.console.level debug \
      set log.console.show_pid on \
      set log.console.show_time on \
      set debug.http_server on \
      set debug.httpd on \
      set api.restful.users.harry.password potter \
      set api.restful.users.ron.password weasley \
      set api.restful.users.hermione.password grainger
   [ -z "$_instance" ] || pop_instance
   return 0
}

# Setup function:
# - wait for the current instance's server to start processing REST requests
# - initialise the REST_PORT_{I} shell variable with the port number of the REST
#   server running in instance {I}
# - zero the request count for the rest_request() function
wait_until_rest_server_ready() {
   local _instance
   case $1 in
   '') _instance=$instance_name;;
   +[A-Z]) _instance=${1#+};;
   *) error "invalid instance arg: $1";;
   esac
   wait_until servald_restful_http_server_started +$_instance
   local _portvar=REST_PORT_$_instance
   get_servald_restful_http_server_port $_portvar +$_instance
   REQUEST_COUNT=0
}

# Utility function:
# - perform a REST request to the given instance (default current instance) and
#   expect a given response (default 200 OK)
rest_request() {
   local _instance=$instance_name
   case $1 in
   +[A-Z]) _instance=${1#+}; shift;;
   esac
   local _portvar=REST_PORT_$_instance
   local request_verb="${1?}"
   local path="${2?}"
   shift 2
   local response_code=200
   case $1 in
   [0-9][0-9][0-9]) response_code=$1; shift;;
   esac
   local timeout=()
   local auth=(--basic)
   local user=(--user harry:potter)
   local buffer=()
   local output="response.json"
   local dump_header="response.headers"
   local trace="response.trace"
   local output_preserve="response-$((++REQUEST_COUNT)).json"
   local trace_preserve="response-$((++REQUEST_COUNT)).trace"
   local form_parts=()
   local data=()
   local headers=()
   while [ $# -ne 0 ]; do
      case $1 in
      --timeout=*) timeout=(--timeout="${1#*=}"); shift;;
      --no-auth) auth=(); user=(); shift;;
      --no-buffer) buffer=(--no-buffer); shift;;
      --user=*) user=(--user "${1#*=}"); shift;;
      --add-header=*) headers+=(--header "${1#*=}"); shift;;
      --output=*) output="${1#*=}"; output_preserve="$output"; shift;;
      --dump-header=*) dump_header="${1#*=}"; shift;;
      --form-part=*) form_parts+=(--form "${1#*=}"); data=(); shift;;
      --data=*) data+=(--data "${1#*=}"); form_parts=(); shift;;
      *) error "unsupported option: $1";;
      esac
   done
   executeOk "${timeout[@]}" curl \
         --silent --show-error \
         --write-out '%{http_code}' \
         --output "$output" \
         --dump-header "$dump_header" \
         --trace-ascii "$trace" \
         "${auth[@]}" "${user[@]}" "${buffer[@]}" \
         --request "$request_verb" \
         "${headers[@]}" \
         "${data[@]}" "${form_parts[@]}" \
         "http://$addr_localhost:${!_portvar}$path"
   tfw_cat "$dump_header" "$output"
   [ "$output_preserve" != "$output" ] && cp "$output" "$output_preserve"
   cp "$trace" "$trace_preserve"
   tfw_preserve "$output_preserve" "$trace_preserve"
   assertStdoutIs "$response_code"
}

# Utility function:
# - ensure that a given version or later of the jq(1) utility is available
# - for use in setup (fixture) functions
setup_jq() {
   JQ=$(type -P jq) || error "jq(1) command is not present"
   local minversion="${1?}"
   local ver="$("$JQ" --version 2>&1)"
   case "$ver" in
   jq-*)
      local oIFS="$IFS"
      IFS='-'
      set -- $ver
      IFS="$oIFS"
      jqversion="$2"
      ;;
   jq\ version\ *)
      set -- $ver
      jqversion="$3"
      ;;
   *)
      error "cannot parse output of jq --version: $ver"
      ;;
   esac
   tfw_cmp_version "$jqversion" "$minversion"
   case $? in
   0|2)
      export JQ
      return 0
      ;;
   esac
   error "jq(1) version $jqversion is not adequate (need $minversion or higher)"
}

# Guard function:
JQ=
jq() {
   [ -x "$JQ" ] || error "missing call to setup_jq or setup_jsonin the fixture"
   "$JQ" "$@"
}

# Setup function:
# - any test wishing to use the JSON utilities in this file must call this in
#   its setup()
setup_json() {
   setup_jq 1.3
}

assertJq() {
   local json="$1"
   local jqscript="$2"
   assert --message="$jqscript" --dump-on-fail="$json" [ "$(jq "$jqscript" "$json")" = true ]
}

assertJqCmp() {
   local opts=()
   while [ $# -gt 0 ]; do
      case "$1" in
      --) shift; break;;
      --*) opts+=("$1"); shift;;
      *) break;;
      esac
   done
   [ $# -eq 3 ] || error "invalid arguments"
   local json="$1"
   local jqscript="$2"
   local file="$3"
   jq --raw-output "$jqscript" "$json" >"$TFWTMP/jqcmp.tmp"
   assert --dump-on-fail="$TFWTMP/jqcmp.tmp" --dump-on-fail="$file" "${opts[@]}" cmp "$TFWTMP/jqcmp.tmp" "$file"
}

assertJqIs() {
   local opts=()
   while [ $# -gt 0 ]; do
      case "$1" in
      --) shift; break;;
      --*) opts+=("$1"); shift;;
      *) break;;
      esac
   done
   [ $# -eq 3 ] || error "invalid arguments"
   local json="$1"
   local jqscript="$2"
   local text="$3"
   local jqout="$(jq --raw-output "$jqscript" "$json")"
   assert "${opts[@]}" [ "$jqout" = "$text" ]
}

assertJqGrep() {
   local opts=()
   while [ $# -gt 0 ]; do
      case "$1" in
      --) shift; break;;
      --*) opts+=("$1"); shift;;
      *) break;;
      esac
   done
   [ $# -eq 3 ] || error "invalid arguments"
   local json="$1"
   local jqscript="$2"
   local pattern="$3"
   jq "$jqscript" "$json" >"$TFWTMP/jqgrep.tmp"
   assertGrep "${opts[@]}" "$TFWTMP/jqgrep.tmp" "$pattern"
}

transform_list_json() {
   # The following jq(1) incantation transforms a JSON array in from the
   # following form (which is optimised for transmission size):
   #     {
   #        "header":[ "label1", "label2", "label3", ... ],
   #        "rows":[
   #              [  row1value1, row1value2, row1value3, ... ],
   #              [  row2value1, row2value2, row2value3, ... ],
   #                 ...
   #              [  rowNvalue1, rowNvalue2, rowNvalue3, ... ]
   #           ]
   #     }
   #
   # into an array of JSON objects:
   #     [
   #        {
   #           "label1": row1value1,
   #           "label2": row1value2,
   #           "label3": row1value3,
   #           ...
   #        },
   #        {
   #           "label1": row2value1,
   #           "label2": row2value2,
   #           "label3": row2value3,
   #           ...
   #        },
   #        ...
   #        {
   #           "label1": rowNvalue1,
   #           "label2": rowNvalue2,
   #           "label3": rowNvalue3,
   #           ...
   #        }
   #     ]
   # which is much easier to test with jq(1) expressions.
   jq '
         [
            .header as $header |
            .rows as $rows |
            $rows | keys | .[] as $index |
            [ $rows[$index] as $d | $d | keys | .[] as $i | {key:$header[$i], value:$d[$i]} ] |
            from_entries |
            .["__index"] = $index
         ]
      ' "$1" >"$2"
   assert --message="$1 contains a well-formed JSON list" [ $? -eq 0 -a -s "$2" ]
}