#!/bin/bash
#
#  Copyright (c) 2018, The OpenThread Authors.
#  All rights reserved.
#
#  Redistribution and use in source and binary forms, with or without
#  modification, are permitted provided that the following conditions are met:
#  1. Redistributions of source code must retain the above copyright
#     notice, this list of conditions and the following disclaimer.
#  2. Redistributions in binary form must reproduce the above copyright
#     notice, this list of conditions and the following disclaimer in the
#     documentation and/or other materials provided with the distribution.
#  3. Neither the name of the copyright holder nor the
#     names of its contributors may be used to endorse or promote products
#     derived from this software without specific prior written permission.
#
#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
#  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
#  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
#  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
#  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
#  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
#  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
#  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
#  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
#  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
#  POSSIBILITY OF SUCH DAMAGE.
#

set -e
set -x

die()
{
    echo " *** ERROR: " "$*"
    exit 1
}

at_exit()
{
    EXIT_CODE=$?

    sudo killall expect || true
    sudo killall ot-ctl || true
    sudo killall ot-daemon || true
    sudo killall ot-cli || true
    sudo killall ot-rcp || true
    sudo killall socat || true

    # The daemon's output no longer streams through tee; show it when the check failed.
    if [[ ${EXIT_CODE} != 0 && -f ${OT_DAEMON_OUTPUT:-} ]]; then
        cat "${OT_DAEMON_OUTPUT}"
    fi

    exit $EXIT_CODE
}

wait_for_leader()
{
    if grep -q leader "$OT_OUTPUT"; then
        return 0
    else
        echo 'Still waiting for leader'
    fi
    return 1
}

timeout_run()
{
    local count="$1"
    local exit_code
    shift 1

    while [[ $count != 0 && $exit_code != 0 ]]; do
        count=$((count - 1))
        "$@" && return 0 || exit_code=$?
        sleep 1
    done

    return $exit_code
}

do_build()
{
    ./script/cmake-build simulation
    # OMR prefixes are installed as kernel routes (which needs border routing for otNetDataContainsOmrPrefix), so that
    # the daemon's route handling is exercised.
    ./script/cmake-build posix -DOT_PLATFORM_NETIF=1 -DOT_PLATFORM_UDP=1 -DOT_UDP_FORWARD=0 -DOT_POSIX_MAX_POWER_TABLE=1 -DOT_POSIX_INSTALL_OMR_ROUTES=ON -DOT_BORDER_ROUTING=ON -DOT_DAEMON="${OT_DAEMON}" -DOT_READLINE="${OT_READLINE}"
}

# The netif joins ff02::1 on the Thread interface when it comes up and leaves it when it
# goes down; both must reach the kernel. On macOS the join used to fail with EINVAL on a
# never-used utun (IPv6 is attached to a tun by its first address, which the stack adds
# after this join) and the failure was tolerated with a warning. The ot-daemon log is checked
# rather than a kernel-side group listing, which could not tell the daemon's own socket
# membership apart from one the kernel holds on its own.
check_daemon_multicast_group()
{
    local action="$1"

    [[ ${OT_DAEMON} == 'on' ]] || return 0

    grep -q -E "${action} multicast address ff02::1([^0-9a-f:]|$)" "${OT_DAEMON_OUTPUT}" || die "ff02::1 not ${action}"
    if grep -q -E 'IPV6_(JOIN|LEAVE)_GROUP failure' "${OT_DAEMON_OUTPUT}"; then
        die 'multicast group failure in the ot-daemon log'
    fi
}

# Taking the Thread interface down makes OpenThread leave its multicast groups, which
# ot-daemon treats as fatal when it fails. Verify that ot-daemon survives it.
check_daemon_netif_down()
{
    local status

    [[ ${OT_DAEMON} == 'on' ]] || return 0

    "${OT_CTL[@]}" -I "${NETIF_NAME}" ifconfig down || true
    sleep 1

    # OT_DAEMON_JOB is sudo (or the sudo function of script/_maybe_isolate), which
    # lives as long as ot-daemon and exits with its status.
    if ! ps -p "${OT_DAEMON_JOB}" >/dev/null; then
        wait "${OT_DAEMON_JOB}" && status=0 || status=$?
        die "ot-daemon exited with status ${status} on ifconfig down"
    fi

    "${OT_CTL[@]}" -I "${NETIF_NAME}" ifconfig | grep 'down' || die 'ot-daemon does not respond after ifconfig down'

    check_daemon_multicast_group Removed
}

# Lists a route to the prefix through the Thread interface. On Linux only the daemon's (proto boot) counts, since the
# kernel adds one of its own (proto kernel) for the interface's address once the address is deprecated.
kernel_route_known()
{
    if [[ ${OSTYPE} == "linux-gnu"* ]]; then
        ip -6 route show "$1" dev "${NETIF_NAME}" proto boot | grep .
    else
        netstat -rn -f inet6 | grep -F "$1" | grep -w "${NETIF_NAME}"
    fi
}

# Runs an ot-ctl command on the daemon and succeeds when it answers Done. The output is collected first, so that a
# grep -q cannot end ot-ctl before it has read the daemon's whole reply.
ot_ctl_done()
{
    local output

    output=$("${OT_CTL[@]}" -I "${NETIF_NAME}" "$@") || return 1
    grep -q Done <<<"${output}"
}

# Passes when the route listing fails too, so it must follow a kernel_route_known that passed.
kernel_route_gone()
{
    ! kernel_route_known "$1"
}

check_daemon_omr_route()
{
    local omr_prefix=fd00:dead:beef::/64
    # OpenThread prints every group within the prefix length, zero groups included.
    local logged_prefix=fd00:dead:beef:0::/64

    [[ ${OT_DAEMON} == 'on' ]] || return 0

    # An OMR prefix in the network data becomes a kernel route through the Thread interface, and goes with the prefix.
    # The kernel installs a prefix route of its own for the address the daemon configures from the prefix (XNU cannot
    # be told not to), so the table alone does not prove the daemon's work: its log must show the route. The daemon
    # logs critical lines only until told otherwise, and the route is reported at the info level.
    ot_ctl_done log level 4 || die 'log level failed'
    ot_ctl_done prefix add "${omr_prefix}" paos || die 'prefix add failed'
    ot_ctl_done netdata register || die 'netdata register failed'
    timeout_run 10 kernel_route_known "${omr_prefix}" || die 'the OMR prefix was not installed as a route through the Thread interface'
    timeout_run 10 grep -qE "(Successfully added|Failed to add) an OMR route ${logged_prefix} in kernel" "${OT_DAEMON_OUTPUT}" \
        || die 'the daemon did not report the route of the OMR prefix'
    grep -qF "Successfully added an OMR route ${logged_prefix} in kernel" "${OT_DAEMON_OUTPUT}" \
        || die 'the daemon failed to install the route of the OMR prefix'

    ot_ctl_done prefix remove "${omr_prefix}" || die 'prefix remove failed'
    ot_ctl_done netdata register || die 'netdata register failed'
    timeout_run 10 kernel_route_gone "${omr_prefix}" || die 'the route of the removed OMR prefix is still installed'
    timeout_run 10 grep -qE "(Successfully deleted|Failed to delete) an OMR route ${logged_prefix} in kernel" "${OT_DAEMON_OUTPUT}" \
        || die 'the daemon did not report the removal of the route of the OMR prefix'
    grep -qF "Successfully deleted an OMR route ${logged_prefix} in kernel" "${OT_DAEMON_OUTPUT}" \
        || die 'the daemon failed to delete the route of the OMR prefix'
}

set_host_address()
{
    if [[ ${OSTYPE} == darwin* ]]; then
        sudo ifconfig "${NETIF_NAME}" inet6 "$2" prefixlen 64 "$([[ $1 == add ]] && echo alias || echo -alias)"
    else
        sudo ip -6 addr "$1" "$2/64" dev "${NETIF_NAME}"
    fi
}

check_daemon_address_events()
{
    local address='fd00:1234:5678::1'
    # ot-ctl prints every address as eight groups, without "::"; `ipaddr -v` adds the origin, the prefix
    # length taken from the trimmed netmask and the flags the address was added with.
    local listed='fd00:1234:5678:0:0:0:0:1'
    local listed_verbose="${listed} origin:manual plen:64 preferred:1 valid:1"

    [[ ${OT_DAEMON} == 'on' ]] || return 0

    # The routing socket (netlink on Linux) reports every address and multicast membership of the
    # interface to the daemon, its own included, and the daemon logs each report at info level.
    grep -q 'ADD \[U\] ' "${OT_DAEMON_OUTPUT}" || die "the reports of OpenThread's own addresses did not reach ot-daemon"
    grep -q 'ADD \[M\] ' "${OT_DAEMON_OUTPUT}" || die "the reports of the interface's multicast memberships did not reach ot-daemon"

    # An address the host adds becomes an address of OpenThread and goes with its removal.
    set_host_address add "${address}"
    timeout_run 10 host_address_known "${listed_verbose}" || die 'an address added on the host did not reach OpenThread'

    set_host_address del "${address}"
    timeout_run 10 host_address_gone "${listed}" || die 'an address removed on the host is still known to OpenThread'
}

# The CLI ends its lines with CR LF; the CR goes before the whole-line match.
host_address_known()
{
    local addresses

    addresses=$("${OT_CTL[@]}" -I "${NETIF_NAME}" ipaddr -v) || return 1
    grep -Fx "$1" <<<"${addresses//$'\r'/}" >/dev/null
}

host_address_gone()
{
    local addresses

    addresses=$("${OT_CTL[@]}" -I "${NETIF_NAME}" ipaddr) || return 1
    ! grep -Fx "$1" <<<"${addresses//$'\r'/}" >/dev/null
}

do_check()
{
    trap at_exit INT TERM EXIT

    sudo rm -rf tmp
    mkdir tmp

    SOCAT_PTY="$PWD/tmp/ot-rcp-pty"
    OT_OUTPUT="$PWD/tmp/ot-output"
    OT_DAEMON_OUTPUT="$PWD/tmp/ot-daemon-output"
    RADIO_NCP_PATH="$PWD/build/simulation/examples/apps/ncp/ot-rcp"

    socat pty,raw,echo=0,link="${SOCAT_PTY}" exec:"${RADIO_NCP_PATH} 1" &
    timeout_run 10 test -c "${SOCAT_PTY}"
    echo 'SOCAT_PTY' "${SOCAT_PTY}"

    # Cover setting a valid network interface name. On macOS only a utun number can be chosen.
    if [[ ${OSTYPE} == darwin* ]]; then
        VALID_NETIF_NAME="utun$((200 + 10#$(date +%S)))"
    else
        VALID_NETIF_NAME="wan$(date +%H%M%S)"
    fi
    readonly VALID_NETIF_NAME

    RADIO_URL="spinel+hdlc+uart://${SOCAT_PTY}?region=US&max-power-table-mbm=1100,1200,1300,1400,1500,1600,1700,1800,1900,2000,2100,2200,2300,2400,2500,2600"

    if [[ ${OT_DAEMON} == 'on' ]]; then
        # The log file is in our own tmp directory, so the redirect does not need sudo.
        # shellcheck disable=SC2024
        sudo -E "$PWD/build/posix/src/posix/ot-daemon" -d5 -v -I "${VALID_NETIF_NAME}" "${RADIO_URL}" >"${OT_DAEMON_OUTPUT}" 2>&1 &
        OT_DAEMON_JOB=$!
        sleep 3
        NETIF_NAME=$(grep -o 'Thread interface: .\+' "${OT_DAEMON_OUTPUT}" | cut -d: -f2 | tr -d ' \r\n')
        [[ ${NETIF_NAME} == "${VALID_NETIF_NAME}" ]] || die "ot-daemon created ${NETIF_NAME}, not the requested ${VALID_NETIF_NAME}"
        OT_CTL_PATH="$PWD/build/posix/src/posix/ot-ctl"
        if [[ ${OT_DAEMON_ALLOW_ALL} == 1 ]] || [[ -n ${OT_ISOLATED_REEXEC:-} ]]; then
            OT_CTL=("${OT_CTL_PATH}")
        else
            OT_CTL=(sudo "${OT_CTL_PATH}")
        fi
        # -d5 asks for debug logging; the daemon's output must carry OpenThread's own log lines, not only the
        # syslog() lines of its startup.
        timeout_run 10 grep -qE '\[(D|I)\] ' "${OT_DAEMON_OUTPUT}" || die 'the daemon does not log at the level -d asked for'
        "${OT_CTL[@]}" -I "${NETIF_NAME}" panid 0xface | grep 'Done' || die 'failed to set panid with ot-ctl'

        # verify supports options in OpenThread commands without separator --
        "${OT_CTL[@]}" -I "${NETIF_NAME}" pskc -p 123456 | grep 'Done' || die 'unable to set pskc'

        # verify this reset and factoryreset end immediately
        "${OT_CTL[@]}" -I "${NETIF_NAME}" reset
        # sleep a while for daemon ready
        sleep 2
        "${OT_CTL[@]}" -I "${NETIF_NAME}" factoryreset
        # sleep a while for daemon ready
        sleep 2

        OPENTHREAD_CONFIG_CLI_MAX_LINE_LENGTH=640
        readonly OPENTHREAD_CONFIG_CLI_MAX_LINE_LENGTH
        local -r kMaxStringLength="$((OPENTHREAD_CONFIG_CLI_MAX_LINE_LENGTH - 1))"

        # verify success if command length doesn't exceed the limit
        for len in $(seq 1 ${kMaxStringLength}); do
            "${OT_CTL[@]}" -I "${NETIF_NAME}" "$(printf '1%.0s' $(seq 1 "${len}"))"
        done

        # verify failure if command length exceeds the limit
        len=${OPENTHREAD_CONFIG_CLI_MAX_LINE_LENGTH}
        if "${OT_CTL[@]}" -I "${NETIF_NAME}" "$(printf '1%.0s' $(seq 1 "${len}"))"; then
            die
        fi

        # check_daemon_multicast_group reads INFO lines of the netif; -d5 above gives them along
        # with everything at DEBG, so settle on INFO for the rest of the run.
        "${OT_CTL[@]}" -I "${NETIF_NAME}" log level 4 | grep 'Done' || die 'failed to set log level'
        OT_CLI_CMD="${OT_CTL[*]} -I ${NETIF_NAME}"
    else
        OT_CLI="$PWD/build/posix/src/posix/ot-cli"
        sudo "${OT_CLI}" -I "${VALID_NETIF_NAME}" -n "${RADIO_URL}"

        # Cover setting a too long(max is 15 characters) network interface name.
        # Expect exit code to be 2(OT_EXIT_INVALID_ARGUMENTS).
        INVALID_NETIF_NAME="wan0123456789123"
        readonly INVALID_NETIF_NAME
        sudo "${OT_CLI}" -I "${INVALID_NETIF_NAME}" -n "${RADIO_URL}" || test $? = 2

        if [[ ${OSTYPE} == darwin* ]]; then
            # A utun name needs a number. The check runs when the interface is created, after a dry run ends,
            # so no -n here; the CLI exits with 2 (OT_EXIT_INVALID_ARGUMENTS) before it would block.
            sudo "${OT_CLI}" -I utunabc "${RADIO_URL}" || test $? = 2
        fi

        OT_CLI_CMD="$PWD/build/posix/src/posix/ot-cli ${RADIO_URL}"
    fi

    sudo expect <<EOF | tee "${OT_OUTPUT}" &
spawn ${OT_CLI_CMD}
expect_after {
    timeout { error }
}
send "region\r\n"
expect "US"
expect "Done"
send "dataset init new\r\n"
expect "Done"
send "dataset commit active\r\n"
expect "Done"
send "routerselectionjitter 1\r\n"
expect "Done"
send "ifconfig up\r\n"
expect "Done"
send "thread start\r\n"
expect "Done"
sleep 10
send "state\r\n"
expect "leader"
expect "Done"
send "extaddr\r\n"
expect "Done"
send "dataset active\r\n"
expect "Done"
send "ipaddr\r\n"
expect "Done"
send "coex\r\n"
expect "Done"
send "coap start\r\n"
expect "Done"
send "coap resource TestResource\r\n"
expect "Done"
send "coap set TestContent\r\n"
expect "Done"
set timeout -1
expect eof
EOF

    sleep 5

    # wait until the node becomes leader
    timeout_run 10 wait_for_leader

    # wait coap service start
    sleep 5

    sudo lsof -iUDP:5683 -nP || die 'Application CoAP port is not available!'

    extaddr=$(grep -ao -A +1 'extaddr' "$OT_OUTPUT" | tail -n1 | tr -d '\r\n\0')
    echo "Extended address is: ${extaddr}"

    prefix=$(grep -ao 'Mesh Local Prefix: [0-9a-f:]\+' "$OT_OUTPUT" | cut -d: -f2- | tr -d ' \r\n')
    LEADER_ALOC="${prefix}ff:fe00:fc00"

    if [[ ${OT_DAEMON} == 'on' ]]; then
        # ot-daemon serves one session at a time and the interactive ot-ctl of the expect
        # session reconnects whenever it is displaced, so end it before using ot-ctl below.
        sudo killall -9 expect || true
        sudo killall -9 ot-ctl || true
    fi

    check_daemon_multicast_group Added
    check_daemon_address_events

    # A group an application joins on the host becomes a membership of OpenThread and goes with the leave
    # (netlink on Linux, RTM_NEWMADDR/RTM_DELMADDR on BSD).
    if [[ ${OT_DAEMON} == 'on' ]]; then
        NETIF_INDEX=$(python3 -c 'import socket, sys; print(socket.if_nametoindex(sys.argv[1]))' "${NETIF_NAME}")
        sudo env PATH="$(dirname "${OT_CTL_PATH}"):${PATH}" \
            python3 "$PWD/tests/scripts/misc/test_multicast_join.py" "${NETIF_INDEX}" "${NETIF_NAME}" \
            || die 'multicast group join failed'
    fi

    # skip testing CoAP for https://github.com/openthread/openthread/issues/6363
    if [[ $OSTYPE != "linux-gnu"* ]]; then
        check_daemon_omr_route
        check_daemon_netif_down
        return 0
    fi

    # Retrievie test resource through application CoAP
    coap_response=$(coap-client -B 5 -m GET "coap://[${LEADER_ALOC}]:5683/TestResource")
    echo "CoAP response is: ${coap_response}"

    # Verify CoAP response contains the test content
    if [[ ${coap_response} == *TestContent* ]]; then
        echo 'Success'
    else
        die 'Failed to access application CoAP'
    fi

    # Retrievie extended address through network diagnostic get
    coap_response=$(echo -n '120100' | xxd -r -p | coap-client -B 5 -m POST "coap://[${LEADER_ALOC}]:61631/d/dg" -f-)

    # Verify Tmf CoAP is blocked
    if [[ -z ${coap_response} ]]; then
        die 'Tmf is not blocked'
    fi

    check_daemon_omr_route
    check_daemon_netif_down
}

main()
{
    if [[ $# == 0 ]]; then
        do_build
        do_check
        return 0
    fi

    while [[ $# != 0 ]]; do
        case $1 in
            build)
                do_build
                ;;
            check)
                # shellcheck source=script/_maybe_isolate
                source script/_maybe_isolate
                do_check
                ;;
            *)
                echo "Unknown action: $1"
                return 1
                ;;
        esac
        shift
    done
}

main "$@"
