// Copyright (c) 2020 Metrolab Technology S.A., Geneva, Switzerland (www.metrolab.com)
// See the included file LICENSE.txt for the licensing conditions.

//////////////////////////////////////////////////////////////////////////
/// \file
/// \brief Test IEEE488 Instrument: Open, Close, IsOpen methods.

#pragma once

#include <gtest/gtest.h>
#include <regex>
#include <future>

#include "IEEE488InstrumentTest.h"
#include "Exception.h"
#include "Helpers.h"

using namespace testing;

class IEEE488InstrumentOpenCloseLockTest : public ::testing::Test
{
protected:
    static IEEE4888_TEST_RESOURCE_MANAGER_CLASS *	pResourceManager;

    static void SetUpTestCase()
    {
        pResourceManager = new IEEE4888_TEST_RESOURCE_MANAGER_CLASS;
        ASSERT_NE(nullptr, pResourceManager);
        ASSERT_EQ(true, pResourceManager->Initialize());
    }

    static void TearDownTestCase()
    {
        delete pResourceManager;
        pResourceManager = nullptr;
    }
};

static const U32 IEEE488_TEST_DURATION = 5; // s
IEEE4888_TEST_RESOURCE_MANAGER_CLASS *  IEEE488InstrumentOpenCloseLockTest::pResourceManager    = nullptr;

/// \brief Utility function to be run in a separate thread:
/// open an instrument, do an *IDN?, wait a while and then close.
static void l_OpenLoopQueryClose(IEEE4888_TEST_RESOURCE_MANAGER_CLASS * pResourceManager,
                                 std::string                            InstrumentName,
                                 U32                                    NSeconds,
                                 std::promise<bool> &                   rSucceeded,
                                 std::promise<U32> &                    rNLoopsPerformed)
{
    bool l_Status = true;
    U32 l_LoopCount = 0;
    IEEE4888_TEST_INSTRUMENT_CLASS * l_pInstrument = nullptr;
    try
    {
        // Make sure we got a non-null Resource Manager
        if (nullptr == pResourceManager)
            throw MTL::CException<IEEE4888_TEST_INSTRUMENT_CLASS>("No Resource Manager", MTL__LOCATION__);

        // Create an Instrument object and open it.
        l_pInstrument = new IEEE4888_TEST_INSTRUMENT_CLASS(*pResourceManager, InstrumentName);
        if (nullptr == l_pInstrument ||
            !l_pInstrument->Open() ||
            !l_pInstrument->SetTimeout(1000*NSeconds))
            throw MTL::CException<IEEE4888_TEST_INSTRUMENT_CLASS>("Couldn't open instrument", MTL__LOCATION__);

        // Perform queries for the requested time period.
        auto l_EndTime = std::chrono::steady_clock::now() + std::chrono::seconds(NSeconds);
        do
        {
//            std::cout << "a" << std::flush;
            if (!l_pInstrument->LockExclusive(1000*NSeconds))
                throw MTL::CException<IEEE4888_TEST_INSTRUMENT_CLASS>("Couldn't lock", MTL__LOCATION__);
//            std::cout << "b" << std::flush;

            if (!l_pInstrument->Write ("*IDN?"))
                throw MTL::CException<IEEE4888_TEST_INSTRUMENT_CLASS>("Couldn't write", MTL__LOCATION__);

            CSCPIBuffer l_Buffer;
            if (!l_pInstrument->Read (l_Buffer))
                throw MTL::CException<IEEE4888_TEST_INSTRUMENT_CLASS>("Couldn't read", MTL__LOCATION__);

            if (!CheckIDNResponse (l_Buffer))
                throw MTL::CException<IEEE4888_TEST_INSTRUMENT_CLASS>("Invalid response", MTL__LOCATION__);

//            std::cout << "c" << std::flush;
            if (!l_pInstrument->Unlock())
                throw MTL::CException<IEEE4888_TEST_INSTRUMENT_CLASS>("Couldn't unlock", MTL__LOCATION__);

            // Sleep at the end of each cycle to force the scheduler to allow the other thread to run.
            // Windows tends to grant long runs to a single thread, causing the test to fail artificially.
            ++l_LoopCount;
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
        } while (std::chrono::steady_clock::now() < l_EndTime);

    }
    catch (MTL::CException<IEEE4888_TEST_INSTRUMENT_CLASS> & rE)
    {
        MTL_Unused(rE);
        CERR(rE.what());
        CERR(std::string("Instrument status: ") + l_pInstrument->StatusDescription(l_pInstrument->Status()));
        l_Status = false;
    }

    // Close and delete the instrument.
    if (nullptr != l_pInstrument)
    {
        l_pInstrument->Close();
        delete l_pInstrument;
    }

    // Return the promised parameters.
    rSucceeded.set_value(l_Status);
    rNLoopsPerformed.set_value(l_LoopCount);
}

/// \brief Utility function to be run in a separate thread:
/// keep an instrument locked for a while.
static void l_OpenLockWaitClose(IEEE4888_TEST_RESOURCE_MANAGER_CLASS *  pResourceManager,
                                std::string                             InstrumentName,
                                U32                                     NSeconds,
                                std::promise<bool> &                    rSucceeded)
{
    bool l_Status = true;
    IEEE4888_TEST_INSTRUMENT_CLASS * l_pInstrument = nullptr;
    try
    {
        // Make sure we got a non-null Resource Manager
        if (nullptr == pResourceManager)
            throw MTL::CException<IEEE4888_TEST_INSTRUMENT_CLASS>("No Resource Manager", MTL__LOCATION__);

        // Create an Instrument object, open it, and lock it.
        l_pInstrument = new IEEE4888_TEST_INSTRUMENT_CLASS(*pResourceManager, InstrumentName);
        if (nullptr == l_pInstrument ||
            !l_pInstrument->Open() ||
            !l_pInstrument->LockExclusive(1000))
            throw MTL::CException<IEEE4888_TEST_INSTRUMENT_CLASS>("Couldn't open instrument", MTL__LOCATION__);

        // Keep it locked for the specified amount of time.
        std::this_thread::sleep_for(std::chrono::seconds(NSeconds));
    }
    catch (MTL::CException<IEEE4888_TEST_INSTRUMENT_CLASS> & rE)
    {
        MTL_Unused(rE);
        CERR(rE.what());
        l_Status = false;
    }

    // Close and delete the instrument.
    if (nullptr != l_pInstrument)
    {
        l_pInstrument->Close();
        delete l_pInstrument;
    }

    // Return the promised status flag.
    rSucceeded.set_value(l_Status);
}

/// \brief Test Instrument object creation/destruction, Open, Close, Status, StatusDescription.
TEST_F(IEEE488InstrumentOpenCloseLockTest, OpenClose)
{
    // Find all instruments.
    CResourceList l_InstrumentList;
    ASSERT_EQ(true, pResourceManager->FindResources(l_InstrumentList, IEEE4888_TEST_RESOURCE_FILTER));
    ASSERT_EQ(false, l_InstrumentList.empty());

    // Create the Instrument. Run the tests on the first one in the list.
    std::string l_InstrumentName = l_InstrumentList.front();
    IEEE4888_TEST_INSTRUMENT_CLASS * l_pInstrument = new IEEE4888_TEST_INSTRUMENT_CLASS(*pResourceManager, l_InstrumentName);
    ASSERT_NE(nullptr, l_pInstrument);
    ASSERT_EQ(false, l_pInstrument->IsOpen());

    // Open the instrument.
    ASSERT_EQ(true, l_pInstrument->Open());
    ASSERT_EQ(true, l_pInstrument->IsOpen());

    // Check the status.
    I32 l_Status = l_pInstrument->Status();
    ASSERT_EQ(0, l_Status);
    std::string l_StatusDescription = l_pInstrument->StatusDescription(l_Status);
    std::regex  l_Regex(".*Success.*", std::regex::icase);
    std::smatch l_Match;
    EXPECT_EQ(true, std::regex_match(l_StatusDescription, l_Match, l_Regex));

    // Close the instrument.
    l_pInstrument->Close();
    ASSERT_EQ(false, l_pInstrument->IsOpen());

    // Destroy the object.
    delete l_pInstrument;

}

/// \brief Test Instrument object creation/destruction, Open, Close, Status, StatusDescription.
TEST_F(IEEE488InstrumentOpenCloseLockTest, OpenCloseTwice)
{
    // Find all instruments.
    CResourceList l_InstrumentList;
    ASSERT_EQ(true, pResourceManager->FindResources(l_InstrumentList, IEEE4888_TEST_RESOURCE_FILTER));
    ASSERT_EQ(false, l_InstrumentList.empty());

    // Create the Instrument. Run the tests on the first one in the list.
    std::string l_InstrumentName = l_InstrumentList.front();
    IEEE4888_TEST_INSTRUMENT_CLASS * l_pInstrument = new IEEE4888_TEST_INSTRUMENT_CLASS(*pResourceManager, l_InstrumentName);
    ASSERT_NE(nullptr, l_pInstrument);
    ASSERT_EQ(false, l_pInstrument->IsOpen());

    // Open the instrument.
    ASSERT_EQ(true, l_pInstrument->Open());
    ASSERT_EQ(true, l_pInstrument->IsOpen());

    // Check the status.
    I32 l_Status = l_pInstrument->Status();
    ASSERT_EQ(0, l_Status);
    std::string l_StatusDescription = l_pInstrument->StatusDescription(l_Status);
    std::regex  l_Regex(".*Success.*", std::regex::icase);
    std::smatch l_Match;
    EXPECT_EQ(true, std::regex_match(l_StatusDescription, l_Match, l_Regex));

    // Close the instrument.
    l_pInstrument->Close();
    ASSERT_EQ(false, l_pInstrument->IsOpen());

    // Open the instrument.
    ASSERT_EQ(true, l_pInstrument->Open());
    ASSERT_EQ(true, l_pInstrument->IsOpen());

    // Check the status.
    l_Status = l_pInstrument->Status();
    ASSERT_EQ(0, l_Status);
    l_StatusDescription = l_pInstrument->StatusDescription(l_Status);
    l_Regex = std::regex(".*Success.*", std::regex::icase);
    EXPECT_EQ(true, std::regex_match(l_StatusDescription, l_Match, l_Regex));

    // Try to open it again - should just sail through.
    ASSERT_EQ(true, l_pInstrument->Open());

    // Close the instrument.
    l_pInstrument->Close();
    ASSERT_EQ(false, l_pInstrument->IsOpen());

    // Try to close it again - should just sail through.
    l_pInstrument->Close();
    ASSERT_EQ(0, l_pInstrument->Status());

    // Destroy the object.
    delete l_pInstrument;

}

/// \brief Test two threads sharing an instrument.
TEST_F(IEEE488InstrumentOpenCloseLockTest, ShareInstrument)
{
    // Find the instrument.
    CResourceList l_InstrumentList;
    ASSERT_EQ(true, pResourceManager->FindResources(l_InstrumentList, IEEE4888_TEST_RESOURCE_FILTER));
    ASSERT_EQ(false, l_InstrumentList.empty());

    // Create the Instrument. Run the tests on the first one in the list.
    std::string l_InstrumentName = l_InstrumentList.front();
    IEEE4888_TEST_INSTRUMENT_CLASS * l_pInstrument = new IEEE4888_TEST_INSTRUMENT_CLASS(*pResourceManager, l_InstrumentName);
	ASSERT_NE(nullptr, l_pInstrument);

    // Launch a thread to perform queries during the specified time period.
    std::promise<bool>  l_PromisedStatus;
    std::future<bool>   l_FutureStatus = l_PromisedStatus.get_future();
    std::promise<U32>   l_PromisedLoopCount;
    std::future<U32>    l_FutureLoopcount = l_PromisedLoopCount.get_future();
    std::thread l_Thread(l_OpenLoopQueryClose, pResourceManager, l_InstrumentName, IEEE488_TEST_DURATION, std::ref(l_PromisedStatus), std::ref(l_PromisedLoopCount));

    // Open the instrument.
    EXPECT_EQ(true, l_pInstrument->Open());
    EXPECT_EQ(true, l_pInstrument->SetTimeout(1000*IEEE488_TEST_DURATION));

    // Perform queries for the requested time period.
    U32 l_LoopCount = 0;
    auto l_EndTime = std::chrono::steady_clock::now() + std::chrono::seconds(IEEE488_TEST_DURATION);
    do
    {
//        std::cout << "A" << std::flush;
        ASSERT_EQ(true, l_pInstrument->LockExclusive(1000*IEEE488_TEST_DURATION));
//        std::cout << "B" << std::flush;

        // Note: With NI-VISA and Qt, there seems to be a timing issue; hence the sleep_for() calls.
        // From USB protocol analysis, it looks like it's related to periodic calls to assert REN (Remote ENable);
        // the following Write/Read cycle tends to fail. The mechanism is not at all clear.
        ASSERT_EQ(true, l_pInstrument->Write ("*IDN?"));
        std::this_thread::sleep_for(std::chrono::milliseconds(10));

        CSCPIBuffer l_Buffer;
        ASSERT_EQ(true, l_pInstrument->Read (l_Buffer));
        std::this_thread::sleep_for(std::chrono::milliseconds(10));

        EXPECT_EQ(true, CheckIDNResponse (l_Buffer));

//        std::cout << "C" << std::flush;
        ASSERT_EQ(true, l_pInstrument->Unlock());

        // Sleep at the end of each cycle to force the scheduler to allow the other thread to run.
        // Windows tends to grant long runs to a single thread, causing the test to fail artificially.
        ++l_LoopCount;
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    } while (std::chrono::steady_clock::now() < l_EndTime);

    // Wait for the child thread to end and check whether it returned success,
    // and whether the loop counts were both positive.
    EXPECT_GT(l_LoopCount, 10u);
    EXPECT_EQ(true, l_FutureStatus.get());
    EXPECT_GT(l_FutureLoopcount.get(), 10u);
    l_Thread.join();

    // Close and delete the instrument.
    l_pInstrument->Close();
    delete l_pInstrument;
}

/// \test Ensure that LockExclusive() fails if the instrument is already locked in another thread.
TEST_F(IEEE488InstrumentOpenCloseLockTest, LockWhenAlreadyLocked)
{
    // Find the instrument.
    CResourceList l_InstrumentList;
    ASSERT_EQ(true, pResourceManager->FindResources(l_InstrumentList, IEEE4888_TEST_RESOURCE_FILTER));
    ASSERT_EQ(false, l_InstrumentList.empty());

    // Create the Instrument. Run the tests on the first one in the list.
    std::string l_InstrumentName = l_InstrumentList.front();
    IEEE4888_TEST_INSTRUMENT_CLASS * l_pInstrument = new IEEE4888_TEST_INSTRUMENT_CLASS(*pResourceManager, l_InstrumentName);

    // Launch a thread to keep the instrument locked for the specified test duration, giving it a second to start.
    std::promise<bool> l_PromisedStatus;
    std::future<bool> l_FutureStatus = l_PromisedStatus.get_future();
    std::thread l_Thread(l_OpenLockWaitClose, pResourceManager, l_InstrumentName, IEEE488_TEST_DURATION, std::ref(l_PromisedStatus));
    std::this_thread::sleep_for(std::chrono::seconds(1));

    // Open the instrument and obtain a lock.
    // Should fail because we time out before the other thread closes the instrument.
    ASSERT_EQ(true, l_pInstrument->Open());
    auto l_StartTime = std::chrono::steady_clock::now();
    EXPECT_EQ(false, l_pInstrument->LockExclusive(1000*(IEEE488_TEST_DURATION-2)));
    auto l_StopTime = std::chrono::steady_clock::now();
    std::chrono::duration<F64> l_Duration = l_StopTime - l_StartTime;
    EXPECT_NEAR(static_cast<F64>(IEEE488_TEST_DURATION-2), l_Duration.count(), 0.1);

    // Wait for the child thread to end and check whether it returned success.
    EXPECT_EQ(true, l_FutureStatus.get());
    l_Thread.join();

    // Close and delete the instrument.
    l_pInstrument->Close();
    delete l_pInstrument;
}

/// \test Ensure that LockExclusive() succeeds if the instrument is already locked in another thread, but we wait long enough.
TEST_F(IEEE488InstrumentOpenCloseLockTest, LockAfterWait)
{
    // Find the instrument.
    CResourceList l_InstrumentList;
    ASSERT_EQ(true, pResourceManager->FindResources(l_InstrumentList, IEEE4888_TEST_RESOURCE_FILTER));
    ASSERT_EQ(false, l_InstrumentList.empty());

    // Create the Instrument. Run the tests on the first one in the list.
    std::string l_InstrumentName = l_InstrumentList.front();
    IEEE4888_TEST_INSTRUMENT_CLASS * l_pInstrument = new IEEE4888_TEST_INSTRUMENT_CLASS(*pResourceManager, l_InstrumentName);

    // Launch a thread to keep the instrument locked for the specified test duration, giving it a second to start.
    std::promise<bool> l_PromisedStatus;
    std::future<bool> l_FutureStatus = l_PromisedStatus.get_future();
    std::thread l_Thread(l_OpenLockWaitClose, pResourceManager, l_InstrumentName, IEEE488_TEST_DURATION, std::ref(l_PromisedStatus));
    std::this_thread::sleep_for(std::chrono::seconds(1));

    // Open the instrument and obtain a lock.
    // Should succeed when the other thread closes its instrument.
    ASSERT_EQ(true, l_pInstrument->Open());
    auto l_StartTime = std::chrono::steady_clock::now();
    EXPECT_EQ(true, l_pInstrument->LockExclusive(1000*(IEEE488_TEST_DURATION+1)));
    auto l_StopTime = std::chrono::steady_clock::now();
    std::chrono::duration<F64> l_Duration = l_StopTime - l_StartTime;
    EXPECT_NEAR(static_cast<F64>(IEEE488_TEST_DURATION-1), l_Duration.count(), 0.1);

    // Wait for the child thread to end and check whether it returned success.
    EXPECT_EQ(true, l_FutureStatus.get());
    l_Thread.join();

    // Close and delete the instrument.
    l_pInstrument->Close();
    delete l_pInstrument;
}
