/*
 * SPDX-License-Identifier: GPL-2.0-only
 */

// End-to-end tests for Ipv4 static routing

#include "ns3/boolean.h"
#include "ns3/config.h"
#include "ns3/inet-socket-address.h"
#include "ns3/internet-stack-helper.h"
#include "ns3/ipv4-address-helper.h"
#include "ns3/ipv4-static-routing-helper.h"
#include "ns3/node-container.h"
#include "ns3/node.h"
#include "ns3/packet.h"
#include "ns3/pointer.h"
#include "ns3/simple-channel.h"
#include "ns3/simple-net-device-helper.h"
#include "ns3/simple-net-device.h"
#include "ns3/simulator.h"
#include "ns3/socket-factory.h"
#include "ns3/string.h"
#include "ns3/test.h"
#include "ns3/udp-socket-factory.h"
#include "ns3/uinteger.h"

using namespace ns3;

/**
 * @ingroup internet-test
 *
 * @brief IPv4 StaticRouting /32 Test
 */
class Ipv4StaticRoutingSlash32TestCase : public TestCase
{
  public:
    Ipv4StaticRoutingSlash32TestCase();
    ~Ipv4StaticRoutingSlash32TestCase() override;

    Ptr<Packet> m_receivedPacket; //!< Received packet

    /**
     * @brief Send data.
     * @param socket The sending socket.
     * @param to Destination address.
     */
    void DoSendData(Ptr<Socket> socket, std::string to);
    /**
     * @brief Send data.
     * @param socket The sending socket.
     * @param to Destination address.
     */
    void SendData(Ptr<Socket> socket, std::string to);

    /**
     * @brief Receive data.
     * @param socket The receiving socket.
     */
    void ReceivePkt(Ptr<Socket> socket);

  private:
    void DoRun() override;
};

// Add some help text to this case to describe what it is intended to test
Ipv4StaticRoutingSlash32TestCase::Ipv4StaticRoutingSlash32TestCase()
    : TestCase("Slash 32 static routing example")
{
}

Ipv4StaticRoutingSlash32TestCase::~Ipv4StaticRoutingSlash32TestCase()
{
}

void
Ipv4StaticRoutingSlash32TestCase::ReceivePkt(Ptr<Socket> socket)
{
    uint32_t availableData [[maybe_unused]] = socket->GetRxAvailable();
    m_receivedPacket = socket->Recv(std::numeric_limits<uint32_t>::max(), 0);
    NS_TEST_ASSERT_MSG_EQ(availableData,
                          m_receivedPacket->GetSize(),
                          "Received packet size is not equal to Rx buffer size");
}

void
Ipv4StaticRoutingSlash32TestCase::DoSendData(Ptr<Socket> socket, std::string to)
{
    Address realTo = InetSocketAddress(Ipv4Address(to.c_str()), 1234);
    NS_TEST_EXPECT_MSG_EQ(socket->SendTo(Create<Packet>(123), 0, realTo), 123, "100");
}

void
Ipv4StaticRoutingSlash32TestCase::SendData(Ptr<Socket> socket, std::string to)
{
    m_receivedPacket = Create<Packet>();
    Simulator::ScheduleWithContext(socket->GetNode()->GetId(),
                                   Seconds(60),
                                   &Ipv4StaticRoutingSlash32TestCase::DoSendData,
                                   this,
                                   socket,
                                   to);
    Simulator::Stop(Seconds(66));
    Simulator::Run();
}

// Test program for this 3-router scenario, using static routing
//
// (a.a.a.a/32)A<--x.x.x.0/30-->B<--y.y.y.0/30-->C(c.c.c.c/32)
//
void
Ipv4StaticRoutingSlash32TestCase::DoRun()
{
    Ptr<Node> nA = CreateObject<Node>();
    Ptr<Node> nB = CreateObject<Node>();
    Ptr<Node> nC = CreateObject<Node>();

    NodeContainer c = NodeContainer(nA, nB, nC);

    InternetStackHelper internet;
    internet.Install(c);

    // simple links
    NodeContainer nAnB = NodeContainer(nA, nB);
    NodeContainer nBnC = NodeContainer(nB, nC);

    SimpleNetDeviceHelper devHelper;

    Ptr<SimpleNetDevice> deviceA = CreateObject<SimpleNetDevice>();
    deviceA->SetAddress(Mac48Address::Allocate());
    nA->AddDevice(deviceA);

    NetDeviceContainer dAdB = devHelper.Install(nAnB);
    NetDeviceContainer dBdC = devHelper.Install(nBnC);

    Ptr<SimpleNetDevice> deviceC = CreateObject<SimpleNetDevice>();
    deviceC->SetAddress(Mac48Address::Allocate());
    nC->AddDevice(deviceC);

    // Later, we add IP addresses.
    Ipv4AddressHelper ipv4;
    ipv4.SetBase("10.1.1.0", "255.255.255.252");
    Ipv4InterfaceContainer iAiB = ipv4.Assign(dAdB);

    ipv4.SetBase("10.1.1.4", "255.255.255.252");
    Ipv4InterfaceContainer iBiC = ipv4.Assign(dBdC);

    Ptr<Ipv4> ipv4A = nA->GetObject<Ipv4>();
    Ptr<Ipv4> ipv4B = nB->GetObject<Ipv4>();
    Ptr<Ipv4> ipv4C = nC->GetObject<Ipv4>();

    int32_t ifIndexA = ipv4A->AddInterface(deviceA);
    int32_t ifIndexC = ipv4C->AddInterface(deviceC);

    Ipv4InterfaceAddress ifInAddrA =
        Ipv4InterfaceAddress(Ipv4Address("172.16.1.1"), Ipv4Mask("/32"));
    ipv4A->AddAddress(ifIndexA, ifInAddrA);
    ipv4A->SetMetric(ifIndexA, 1);
    ipv4A->SetUp(ifIndexA);

    Ipv4InterfaceAddress ifInAddrC =
        Ipv4InterfaceAddress(Ipv4Address("192.168.1.1"), Ipv4Mask("/32"));
    ipv4C->AddAddress(ifIndexC, ifInAddrC);
    ipv4C->SetMetric(ifIndexC, 1);
    ipv4C->SetUp(ifIndexC);

    Ipv4StaticRoutingHelper ipv4RoutingHelper;
    // Create static routes from A to C
    Ptr<Ipv4StaticRouting> staticRoutingA = ipv4RoutingHelper.GetStaticRouting(ipv4A);
    // The ifIndex for this outbound route is 1; the first p2p link added
    staticRoutingA->AddHostRouteTo(Ipv4Address("192.168.1.1"), Ipv4Address("10.1.1.2"), 1);
    Ptr<Ipv4StaticRouting> staticRoutingB = ipv4RoutingHelper.GetStaticRouting(ipv4B);
    // The ifIndex we want on node B is 2; 0 corresponds to loopback, and 1 to the first point to
    // point link
    staticRoutingB->AddHostRouteTo(Ipv4Address("192.168.1.1"), Ipv4Address("10.1.1.6"), 2);

    // Create the UDP sockets
    Ptr<SocketFactory> rxSocketFactory = nC->GetObject<UdpSocketFactory>();
    Ptr<Socket> rxSocket = rxSocketFactory->CreateSocket();
    NS_TEST_EXPECT_MSG_EQ(rxSocket->Bind(InetSocketAddress(Ipv4Address("192.168.1.1"), 1234)),
                          0,
                          "trivial");
    rxSocket->SetRecvCallback(MakeCallback(&Ipv4StaticRoutingSlash32TestCase::ReceivePkt, this));

    Ptr<SocketFactory> txSocketFactory = nA->GetObject<UdpSocketFactory>();
    Ptr<Socket> txSocket = txSocketFactory->CreateSocket();
    txSocket->SetAllowBroadcast(true);

    // ------ Now the tests ------------

    // Unicast test
    SendData(txSocket, "192.168.1.1");
    NS_TEST_EXPECT_MSG_EQ(m_receivedPacket->GetSize(),
                          123,
                          "Static routing with /32 did not deliver all packets.");

    Simulator::Destroy();
}

/**
 * @ingroup internet-test
 *
 * @brief IPv4 StaticRouting /32 TestSuite
 */
class Ipv4StaticRoutingTestSuite : public TestSuite
{
  public:
    Ipv4StaticRoutingTestSuite();
};

Ipv4StaticRoutingTestSuite::Ipv4StaticRoutingTestSuite()
    : TestSuite("ipv4-static-routing", Type::UNIT)
{
    AddTestCase(new Ipv4StaticRoutingSlash32TestCase, TestCase::Duration::QUICK);
}

static Ipv4StaticRoutingTestSuite
    ipv4StaticRoutingTestSuite; //!< Static variable for test initialization
