// Copyright (c) 2019 - 2021 by Robert Bosch GmbH. All rights reserved.
// Copyright (c) 2021 - 2022 by Apex.AI Inc. All rights reserved.
//
// 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.
//
// SPDX-License-Identifier: Apache-2.0

#include "iceoryx_hoofs/cxx/convert.hpp"
#include "iceoryx_hoofs/cxx/generic_raii.hpp"
#include "iceoryx_hoofs/internal/relocatable_pointer/base_relative_pointer.hpp"
#include "iceoryx_hoofs/posix_wrapper/posix_access_rights.hpp"
#include "iceoryx_hoofs/testing/watch_dog.hpp"
#include "iceoryx_posh/iceoryx_posh_types.hpp"
#include "iceoryx_posh/internal/capro/capro_message.hpp"
#include "iceoryx_posh/internal/popo/ports/client_port_user.hpp"
#include "iceoryx_posh/internal/popo/ports/publisher_port_user.hpp"
#include "iceoryx_posh/internal/popo/ports/server_port_user.hpp"
#include "iceoryx_posh/internal/popo/ports/subscriber_port_user.hpp"
#include "iceoryx_posh/internal/roudi/port_manager.hpp"
#include "iceoryx_posh/popo/client_options.hpp"
#include "iceoryx_posh/popo/server_options.hpp"
#include "iceoryx_posh/roudi/memory/iceoryx_roudi_memory_manager.hpp"

#include "test.hpp"

#include <cstdint>
#include <limits>

namespace iox_test_roudi_portmanager
{
using namespace ::testing;
using namespace iox;
using namespace iox::capro;
using namespace iox::cxx;
using namespace iox::popo;
using namespace iox::roudi;

using iox::runtime::PortConfigInfo;

class PortManagerTester : public PortManager
{
  public:
    PortManagerTester(IceOryxRouDiMemoryManager* roudiMemoryManager)
        : PortManager(roudiMemoryManager)
    {
    }

  private:
    FRIEND_TEST(PortManager_test, CheckDeleteOfPortsFromProcess1);
    FRIEND_TEST(PortManager_test, CheckDeleteOfPortsFromProcess2);
    FRIEND_TEST(PortManager_test, CreateServerWithNotOfferOnCreateDoesNotAddServerToServiceRegistry);
    FRIEND_TEST(PortManager_test, CreateServerWithOfferOnCreateAddsServerToServiceRegistry);
    FRIEND_TEST(PortManager_test, StopOfferRemovesServerFromServiceRegistry);
    FRIEND_TEST(PortManager_test, OfferAddsServerToServiceRegistry);
};

class PortManager_test : public Test
{
  public:
    iox::mepoo::MemoryManager* m_payloadDataSegmentMemoryManager{nullptr};
    IceOryxRouDiMemoryManager* m_roudiMemoryManager{nullptr};
    PortManagerTester* m_portManager{nullptr};

    uint16_t m_instIdCounter, m_eventIdCounter, m_sIdCounter;

    iox::RuntimeName_t m_runtimeName{"TestApp"};

    cxx::GenericRAII suppressLogging = iox::LoggerPosh().SetLogLevelForScope(iox::log::LogLevel::kOff);

    cxx::vector<iox::capro::ServiceDescription, NUMBER_OF_INTERNAL_PUBLISHERS> internalServices;
    const capro::ServiceDescription serviceRegistry{
        SERVICE_DISCOVERY_SERVICE_NAME, SERVICE_DISCOVERY_INSTANCE_NAME, SERVICE_DISCOVERY_EVENT_NAME};

    void SetUp() override
    {
        m_instIdCounter = m_sIdCounter = 1U;
        m_eventIdCounter = 0;
        // starting at {1,1,1}

        auto config = iox::RouDiConfig_t().setDefaults();
        m_roudiMemoryManager = new IceOryxRouDiMemoryManager(config);
        EXPECT_FALSE(m_roudiMemoryManager->createAndAnnounceMemory().has_error());
        m_portManager = new PortManagerTester(m_roudiMemoryManager);

        auto user = iox::posix::PosixUser::getUserOfCurrentProcess();
        auto segmentInfo =
            m_roudiMemoryManager->segmentManager().value()->getSegmentInformationWithWriteAccessForUser(user);
        ASSERT_TRUE(segmentInfo.m_memoryManager.has_value());

        m_payloadDataSegmentMemoryManager = &segmentInfo.m_memoryManager.value().get();

        // clearing the introspection, is not in d'tor -> SEGFAULT in delete sporadically
        m_portManager->stopPortIntrospection();
        m_portManager->deletePortsOfProcess(iox::roudi::IPC_CHANNEL_ROUDI_NAME);
    }

    void TearDown() override
    {
        delete m_portManager;
        delete m_roudiMemoryManager;
        iox::rp::BaseRelativePointer::unregisterAll();
    }

    void addInternalPublisherOfPortManagerToVector()
    {
        internalServices.push_back(serviceRegistry);
        internalServices.push_back(IntrospectionPortService);
        internalServices.push_back(IntrospectionPortThroughputService);
        internalServices.push_back(IntrospectionSubscriberPortChangingDataService);
    }

    iox::capro::ServiceDescription getUniqueSD()
    {
        m_eventIdCounter++;
        if (m_eventIdCounter == std::numeric_limits<uint16_t>::max())
        {
            m_eventIdCounter = 1U;
            m_instIdCounter++; // not using max (wildcard)
            if (m_instIdCounter == std::numeric_limits<uint16_t>::max())
            {
                m_instIdCounter = 1U;
                m_sIdCounter++;
                if (m_sIdCounter == std::numeric_limits<uint16_t>::max())
                {
                    // ASSERT_TRUE(false); // limits of test reached no more unique ids possible
                }
            }
        }
        return {iox::capro::IdString_t(TruncateToCapacity, convert::toString(m_sIdCounter)),
                iox::capro::IdString_t(TruncateToCapacity, convert::toString(m_eventIdCounter)),
                iox::capro::IdString_t(TruncateToCapacity, convert::toString(m_instIdCounter))};
    }

    void acquireMaxNumberOfInterfaces(
        std::string runtimeName,
        std::function<void(iox::popo::InterfacePortData*)> f = std::function<void(iox::popo::InterfacePortData*)>())
    {
        for (unsigned int i = 0; i < iox::MAX_INTERFACE_NUMBER; i++)
        {
            auto newProcessName = runtimeName + iox::cxx::convert::toString(i);
            auto interfacePort = m_portManager->acquireInterfacePortData(
                iox::capro::Interfaces::INTERNAL, iox::RuntimeName_t(iox::cxx::TruncateToCapacity, newProcessName));
            ASSERT_NE(interfacePort, nullptr);
            if (f)
            {
                f(interfacePort);
            }
        }
    }

    void acquireMaxNumberOfConditionVariables(std::string runtimeName,
                                              std::function<void(iox::popo::ConditionVariableData*)> f =
                                                  std::function<void(iox::popo::ConditionVariableData*)>())
    {
        for (unsigned int i = 0; i < iox::MAX_NUMBER_OF_CONDITION_VARIABLES; i++)
        {
            auto newProcessName = runtimeName + iox::cxx::convert::toString(i);
            auto condVar = m_portManager->acquireConditionVariableData(
                iox::RuntimeName_t(iox::cxx::TruncateToCapacity, newProcessName));
            ASSERT_FALSE(condVar.has_error());
            if (f)
            {
                f(condVar.value());
            }
        }
    }

    void
    acquireMaxNumberOfNodes(std::string nodeName,
                            std::string runtimeName,
                            std::function<void(iox::runtime::NodeData*, iox::NodeName_t, iox::RuntimeName_t)> f =
                                std::function<void(iox::runtime::NodeData*, iox::NodeName_t, iox::RuntimeName_t)>())
    {
        for (unsigned int i = 0U; i < iox::MAX_NODE_NUMBER; i++)
        {
            iox::RuntimeName_t newProcessName(iox::cxx::TruncateToCapacity,
                                              runtimeName + iox::cxx::convert::toString(i));
            iox::NodeName_t newNodeName(iox::cxx::TruncateToCapacity, nodeName + iox::cxx::convert::toString(i));
            auto node = m_portManager->acquireNodeData(newProcessName, newNodeName);
            ASSERT_FALSE(node.has_error());
            if (f)
            {
                f(node.value(), newNodeName, newProcessName);
            }
        }
    }

    void setupAndTestBlockingPublisher(const iox::RuntimeName_t& publisherRuntimeName,
                                       std::function<void()> testHook) noexcept;

    PublisherPortUser createPublisher(const PublisherOptions& options)
    {
        return PublisherPortUser(
            m_portManager
                ->acquirePublisherPortData(
                    {"1", "1", "1"}, options, "guiseppe", m_payloadDataSegmentMemoryManager, PortConfigInfo())
                .value());
    }

    SubscriberPortUser createSubscriber(const SubscriberOptions& options)
    {
        return SubscriberPortUser(
            m_portManager->acquireSubscriberPortData({"1", "1", "1"}, options, "schlomo", PortConfigInfo()).value());
    }

    ClientPortUser createClient(const ClientOptions& options)
    {
        const ServiceDescription sd{"1", "1", "1"};
        const RuntimeName_t runtimeName{"guiseppe"};
        return ClientPortUser(
            *m_portManager->acquireClientPortData(sd, options, runtimeName, m_payloadDataSegmentMemoryManager, {})
                 .value());
    }

    ServerPortUser createServer(const ServerOptions& options)
    {
        const ServiceDescription sd{"1", "1", "1"};
        const RuntimeName_t runtimeName{"schlomo"};
        return ServerPortUser(
            *m_portManager->acquireServerPortData(sd, options, runtimeName, m_payloadDataSegmentMemoryManager, {})
                 .value());
    }
};

template <typename vector>
void setDestroyFlagAndClearContainer(vector& container)
{
    for (auto& item : container)
    {
        item->m_toBeDestroyed.store(true, std::memory_order_relaxed);
    }
    container.clear();
}
} // namespace iox_test_roudi_portmanager
