/*
 * Copyright (C) 2019,2020 by Sukchan Lee <acetcom@gmail.com>
 *
 * This file is part of Open5GS.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 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, see <https://www.gnu.org/licenses/>.
 */

#include "test-common.h"
#include "ipfw/ipfw2.h"

ogs_socknode_t *test_gtpu_server(int index, int family)
{
    int rv;
    ogs_sockaddr_t *addr = NULL;
    ogs_socknode_t *node = NULL;
    ogs_sock_t *sock = NULL;

    if (index == 1) {
        if (family == AF_INET6)
            ogs_assert(OGS_OK ==
                ogs_copyaddrinfo(&addr, test_self()->gnb1_addr6));
        else
            ogs_assert(OGS_OK ==
                ogs_copyaddrinfo(&addr, test_self()->gnb1_addr));
    } else if (index == 2) {
        if (family == AF_INET6)
            ogs_assert(OGS_OK ==
                ogs_copyaddrinfo(&addr, test_self()->gnb2_addr6));
        else
            ogs_assert(OGS_OK ==
                ogs_copyaddrinfo(&addr, test_self()->gnb2_addr));
    } else
        ogs_assert_if_reached();

    node = ogs_socknode_new(addr);
    ogs_assert(node);

    sock = ogs_udp_server(node);
    ogs_assert(sock);

    return node;
}

ogs_pkbuf_t *test_gtpu_read(ogs_socknode_t *node)
{
    int rc = 0;
    ogs_sockaddr_t from;
    ogs_pkbuf_t *recvbuf = ogs_pkbuf_alloc(NULL, OGS_MAX_SDU_LEN);
    ogs_assert(recvbuf);
    ogs_pkbuf_put(recvbuf, OGS_MAX_SDU_LEN);

    ogs_assert(node);
    ogs_assert(node->sock);

    while (1) {
        rc = ogs_recvfrom(
                node->sock->fd, recvbuf->data, recvbuf->len, 0, &from);
        if (rc <= 0) {
            ogs_log_message(OGS_LOG_ERROR, ogs_socket_errno,
                    "ogs_recvfrom() failed");
        }
        break;
    }
    recvbuf->len = rc;

    return recvbuf;
}

void test_gtpu_close(ogs_socknode_t *node)
{
    ogs_socknode_free(node);
}

#include "upf/upf-config.h"

#if HAVE_NETINET_IP_H
#include <netinet/ip.h>
#endif

#if HAVE_NETINET_IP6_H
#include <netinet/ip6.h>
#endif

#if HAVE_NETINET_IP_ICMP_H
#include <netinet/ip_icmp.h>
#endif

#if HAVE_NETINET_ICMP6_H
#include <netinet/icmp6.h>
#endif

void testgtpu_recv(test_ue_t *test_ue, ogs_pkbuf_t *pkbuf)
{
    test_sess_t *sess = NULL;
    test_bearer_t *bearer = NULL;

    ogs_gtp_header_t *gtp_h = NULL;
    struct ip6_hdr *ip6_h =  NULL;
    struct nd_router_advert *advert_h = NULL;
    struct nd_opt_prefix_info *prefix = NULL;

    uint32_t teid;
    uint8_t mask[OGS_IPV6_LEN];

    ogs_assert(test_ue);
    ogs_assert(pkbuf);

    gtp_h = (ogs_gtp_header_t *)pkbuf->data;
    ogs_assert(gtp_h);

    ogs_assert(gtp_h->version == OGS_GTP_VERSION_1);
    ogs_assert(gtp_h->type == OGS_GTPU_MSGTYPE_GPDU);

    teid = be32toh(gtp_h->teid);

    if (test_ue->mme_ue_s1ap_id) {
        /* EPC */
        ogs_list_for_each(&test_ue->sess_list, sess) {
            ogs_list_for_each(&sess->bearer_list, bearer) {
                if (teid == bearer->enb_s1u_teid) goto found;
            }
            ogs_assert(bearer);
        }
        ogs_assert(sess);
    } else if (test_ue->amf_ue_ngap_id) {
        /* 5GC */
        ogs_list_for_each(&test_ue->sess_list, sess) {
            if (sess->gnb_n3_teid == teid) goto found;
        }
        ogs_assert(sess);
    } else {
        ogs_assert_if_reached();
    }

found:
    ogs_assert(sess);

    ip6_h = pkbuf->data + OGS_GTPV1U_HEADER_LEN;
    ogs_assert(ip6_h);
    if (ip6_h->ip6_nxt == IPPROTO_ICMPV6) {
        struct nd_router_advert *advert_h = (struct nd_router_advert *)
            ((unsigned char*)ip6_h + sizeof(struct ip6_hdr));
        ogs_assert(advert_h);
        if (advert_h->nd_ra_hdr.icmp6_type == ND_ROUTER_ADVERT) {
            int i;
            struct nd_opt_prefix_info *prefix = (struct nd_opt_prefix_info *)
                ((unsigned char*)advert_h + sizeof(struct nd_router_advert));
            ogs_assert(prefix);
            n2mask(mask, prefix->nd_opt_pi_prefix_len);
            for (i = 0; i < OGS_IPV6_LEN; i++) {
                sess->ue_ip.addr6[i] |=
                    (mask[i] & prefix->nd_opt_pi_prefix.s6_addr[i]);
            }
        }
    }
    ogs_pkbuf_free(pkbuf);
}

int test_gtpu_send(
        ogs_socknode_t *node, test_bearer_t *bearer,
        ogs_gtp_header_t *gtp_hdesc, ogs_gtp_extension_header_t *ext_hdesc,
        ogs_pkbuf_t *pkbuf)
{
    ogs_gtp_node_t gnode;
    test_sess_t *sess = NULL;

    ogs_assert(gtp_hdesc);
    ogs_assert(ext_hdesc);
    ogs_assert(pkbuf);

    ogs_assert(bearer);
    sess = bearer->sess;
    ogs_assert(sess);

    memset(&gnode, 0, sizeof(ogs_gtp_node_t));

    gnode.addr.ogs_sin_port = htobe16(OGS_GTPV1_U_UDP_PORT);
    gnode.sock = node->sock;

    if (bearer->qfi) {
        if (sess->upf_n3_ip.ipv4) {
            gnode.addr.ogs_sa_family = AF_INET;
            gnode.addr.sin.sin_addr.s_addr = sess->upf_n3_ip.addr;
        } else {
            ogs_fatal("Not implemented");
            ogs_assert_if_reached();
        }

    } else if (bearer->ebi) {
        if (bearer->sgw_s1u_ip.ipv4) {
            gnode.addr.ogs_sa_family = AF_INET;
            gnode.addr.sin.sin_addr.s_addr = bearer->sgw_s1u_ip.addr;
        } else {
            ogs_fatal("Not implemented");
            ogs_assert_if_reached();
        }
    } else {
        ogs_fatal("No QFI[%d] and EBI[%d]", bearer->qfi, bearer->ebi);
        ogs_assert_if_reached();
    }

    ext_hdesc->pdu_type =
        OGS_GTP_EXTENSION_HEADER_PDU_TYPE_UL_PDU_SESSION_INFORMATION;

    return ogs_gtp_send_user_plane(&gnode, gtp_hdesc, ext_hdesc, pkbuf);
}

int test_gtpu_send_ping(
        ogs_socknode_t *node, test_bearer_t *bearer, const char *dst_ip)
{
    int rv;
    test_sess_t *sess = NULL;

    ogs_gtp_header_t gtp_hdesc;
    ogs_gtp_extension_header_t ext_hdesc;

    ogs_pkbuf_t *pkbuf = NULL;
    ogs_ipsubnet_t dst_ipsub;

    ogs_assert(bearer);
    sess = bearer->sess;
    ogs_assert(sess);
    ogs_assert(dst_ip);

    rv = ogs_ipsubnet(&dst_ipsub, dst_ip, NULL);
    ogs_assert(rv == OGS_OK);

    pkbuf = ogs_pkbuf_alloc(
            NULL, 200 /* enough for ICMP; use smaller buffer */);
    ogs_assert(pkbuf);
    ogs_pkbuf_reserve(pkbuf, OGS_GTPV1U_5GC_HEADER_LEN);
    ogs_pkbuf_put(pkbuf, 200-OGS_GTPV1U_5GC_HEADER_LEN);
    memset(pkbuf->data, 0, pkbuf->len);

    if (dst_ipsub.family == AF_INET) {
        struct ip *ip_h = NULL;
        struct icmp *icmp_h = NULL;

        ogs_pkbuf_trim(pkbuf, sizeof *ip_h + ICMP_MINLEN);

        ip_h = (struct ip *)pkbuf->data;
        icmp_h = (struct icmp *)((uint8_t *)ip_h + sizeof *ip_h);

        ip_h->ip_v = 4;
        ip_h->ip_hl = 5;
        ip_h->ip_tos = 0;
        ip_h->ip_id = rand();
        ip_h->ip_off = 0;
        ip_h->ip_ttl = 255;
        ip_h->ip_p = IPPROTO_ICMP;
        ip_h->ip_len = htobe16(sizeof *ip_h + ICMP_MINLEN);
        ip_h->ip_src.s_addr = sess->ue_ip.addr;
        ip_h->ip_dst.s_addr = dst_ipsub.sub[0];
        ip_h->ip_sum = ogs_in_cksum((uint16_t *)ip_h, sizeof *ip_h);

        icmp_h->icmp_type = 8;
        icmp_h->icmp_seq = rand();
        icmp_h->icmp_id = rand();
        icmp_h->icmp_cksum = ogs_in_cksum((uint16_t *)icmp_h, ICMP_MINLEN);

    } else if (dst_ipsub.family == AF_INET6) {
        struct ip6_hdr *ip6_h = NULL;
        struct icmp6_hdr *icmp6_h = NULL;
        uint16_t plen = 0;
        uint8_t nxt = 0;
        uint8_t *p = NULL;

        ogs_pkbuf_trim(pkbuf, sizeof *ip6_h + sizeof *icmp6_h);

        p = (uint8_t *)pkbuf->data;
        plen =  htobe16(sizeof *icmp6_h);
        nxt = IPPROTO_ICMPV6;

        ip6_h = (struct ip6_hdr *)p;
        icmp6_h = (struct icmp6_hdr *)((uint8_t *)ip6_h + sizeof *ip6_h);

        memcpy(p, sess->ue_ip.addr6, sizeof sess->ue_ip.addr6);
        p += sizeof sess->ue_ip.addr6;
        memcpy(p, dst_ipsub.sub, sizeof dst_ipsub.sub);
        p += sizeof dst_ipsub.sub;
        p += 2; memcpy(p, &plen, 2); p += 2;
        p += 3; *p = nxt; p += 1;

        icmp6_h->icmp6_type = ICMP6_ECHO_REQUEST;
        icmp6_h->icmp6_seq = rand();
        icmp6_h->icmp6_id = rand();

        icmp6_h->icmp6_cksum = ogs_in_cksum(
                (uint16_t *)ip6_h, sizeof *ip6_h + sizeof *icmp6_h);

        ip6_h->ip6_flow = htobe32(0x60000001);
        ip6_h->ip6_plen = plen;
        ip6_h->ip6_nxt = nxt;;
        ip6_h->ip6_hlim = 0xff;
        memcpy(ip6_h->ip6_src.s6_addr,
                sess->ue_ip.addr6, sizeof sess->ue_ip.addr6);
        memcpy(ip6_h->ip6_dst.s6_addr, dst_ipsub.sub, sizeof dst_ipsub.sub);
    } else {
        ogs_fatal("Invalid family[%d]", dst_ipsub.family);
        ogs_assert_if_reached();
    }

    memset(&gtp_hdesc, 0, sizeof(gtp_hdesc));
    memset(&ext_hdesc, 0, sizeof(ext_hdesc));

    gtp_hdesc.type = OGS_GTPU_MSGTYPE_GPDU;

    if (bearer->qfi) {
        gtp_hdesc.teid = sess->upf_n3_teid;
        ext_hdesc.qos_flow_identifier = bearer->qfi;

    } else if (bearer->ebi) {
        gtp_hdesc.teid = bearer->sgw_s1u_teid;

    } else {
        ogs_fatal("No QFI[%d] and EBI[%d]", bearer->qfi, bearer->ebi);
        ogs_assert_if_reached();
    }

    return test_gtpu_send(node, bearer, &gtp_hdesc, &ext_hdesc, pkbuf);
}

int test_gtpu_send_slacc_rs(ogs_socknode_t *node, test_bearer_t *bearer)
{
    test_sess_t *sess = NULL;

    ogs_gtp_header_t gtp_hdesc;
    ogs_gtp_extension_header_t ext_hdesc;

    ogs_pkbuf_t *pkbuf = NULL;

    const char *payload =
        "6000000000083aff fe80000000000000 0000000000000002"
        "ff02000000000000 0000000000000002 85007d3500000000";
    unsigned char tmp[OGS_MAX_SDU_LEN];
    int payload_len = 48;

    ogs_assert(bearer);
    sess = bearer->sess;
    ogs_assert(sess);

    pkbuf = ogs_pkbuf_alloc(
            NULL, 200 /* enough for ICMP; use smaller buffer */);
    ogs_assert(pkbuf);
    ogs_pkbuf_reserve(pkbuf, OGS_GTPV1U_5GC_HEADER_LEN);
    ogs_pkbuf_put(pkbuf, 200-OGS_GTPV1U_5GC_HEADER_LEN);
    memset(pkbuf->data, 0, pkbuf->len);

    OGS_HEX(payload, strlen(payload), tmp);
    memcpy(pkbuf->data, tmp, payload_len);

    ogs_pkbuf_trim(pkbuf, payload_len);

    memset(&gtp_hdesc, 0, sizeof(gtp_hdesc));
    memset(&ext_hdesc, 0, sizeof(ext_hdesc));

    gtp_hdesc.type = OGS_GTPU_MSGTYPE_GPDU;
    gtp_hdesc.flags = OGS_GTPU_FLAGS_S;

    if (bearer->qfi) {
        gtp_hdesc.teid = sess->upf_n3_teid;

/* CHECK: I guess that Router Soliciation does not include QFI in 5G Core */
#if 0
        ext_hdesc.qos_flow_identifier = bearer->qfi;
#endif

    } else if (bearer->ebi) {
        gtp_hdesc.teid = bearer->sgw_s1u_teid;

    } else {
        ogs_fatal("No QFI[%d] and EBI[%d]", bearer->qfi, bearer->ebi);
        ogs_assert_if_reached();
    }

    return test_gtpu_send(node, bearer, &gtp_hdesc, &ext_hdesc, pkbuf);
}

int test_gtpu_send_error_indication(
        ogs_socknode_t *node, test_bearer_t *bearer)
{
    test_sess_t *sess = NULL;
    uint32_t teid = 0;

    ogs_gtp_header_t gtp_hdesc;
    ogs_gtp_extension_header_t ext_hdesc;

    ogs_pkbuf_t *pkbuf = NULL;

    ogs_assert(bearer);
    sess = bearer->sess;
    ogs_assert(sess);

    if (bearer->qfi) {
        /* 5GC */
        teid = sess->gnb_n3_teid;

    } else if (bearer->ebi) {
        /* EPC */
        teid = bearer->enb_s1u_teid;

    } else {
        ogs_fatal("No QFI[%d] and EBI[%d]", bearer->qfi, bearer->ebi);
        ogs_assert_if_reached();
    }

    pkbuf = ogs_gtp_build_error_indication(teid, node->addr);
    ogs_assert(pkbuf);

    memset(&gtp_hdesc, 0, sizeof(gtp_hdesc));
    memset(&ext_hdesc, 0, sizeof(ext_hdesc));

    gtp_hdesc.type = OGS_GTPU_MSGTYPE_ERR_IND;
    gtp_hdesc.flags = OGS_GTPU_FLAGS_S|OGS_GTPU_FLAGS_E;
    ext_hdesc.type = OGS_GTP_EXTENSION_HEADER_TYPE_UDP_PORT;

    return test_gtpu_send(node, bearer, &gtp_hdesc, &ext_hdesc, pkbuf);
}

int test_gtpu_send_indirect_data_forwarding(
        ogs_socknode_t *node, test_bearer_t *bearer, ogs_pkbuf_t *pkbuf)
{
    test_sess_t *sess = NULL;

    ogs_gtp_header_t gtp_hdesc;
    ogs_gtp_extension_header_t ext_hdesc;

    ogs_assert(bearer);
    sess = bearer->sess;
    ogs_assert(sess);
    ogs_assert(pkbuf);

    memset(&gtp_hdesc, 0, sizeof(gtp_hdesc));
    memset(&ext_hdesc, 0, sizeof(ext_hdesc));

    gtp_hdesc.type = OGS_GTPU_MSGTYPE_GPDU;

    if (bearer->qfi) {
        gtp_hdesc.teid = sess->handover.upf_dl_teid;
        ext_hdesc.qos_flow_identifier = bearer->qfi;

    } else if (bearer->ebi) {

        ogs_fatal("Not implmented EPC Indirect Tunnel");
        ogs_assert_if_reached();

    } else {
        ogs_fatal("No QFI[%d] and EBI[%d]", bearer->qfi, bearer->ebi);
        ogs_assert_if_reached();
    }

    return test_gtpu_send(node, bearer, &gtp_hdesc, &ext_hdesc, pkbuf);
}
