//
// LDASTools frameCPP - A library implementing the LIGO/Virgo frame
// specification
//
// Copyright (C) 2018 California Institute of Technology
//
// LDASTools frameCPP is free software; you may redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 (GPLv2) of the
// License or at your discretion, any later version.
//
// LDASTools frameCPP is distributed in the hope that it will be useful, but
// without any warranty or even the implied warranty of merchantability
// or fitness for a particular purpose. See the GNU General Public
// License (GPLv2) for more details.
//
// Neither the names of the California Institute of Technology (Caltech),
// The Massachusetts Institute of Technology (M.I.T), The Laser
// Interferometer Gravitational-Wave Observatory (LIGO), nor the names
// of its contributors may be used to endorse or promote products derived
// from this software without specific prior written permission.
//
// You should have received a copy of the licensing terms for this
// software included in the file LICENSE located in the top-level
// directory of this package. If you did not, you can view a copy at
// http://dcc.ligo.org/M1500244/LICENSE
//

#if HAVE_CONFIG_H
#include <framecpp_config.h>
#endif /* HAVE_CONFIG_H */

#include <sstream>

#define BOOST_TEST_MAIN
#include <boost/test/included/unit_test.hpp>

#include "ldastoolsal/fstream.hh"

#include "framecpp/Common/FrameSpec.hh"
#include "framecpp/Common/IOStream.hh"
#include "framecpp/Common/FrameBuffer.hh"
#include "framecpp/Common/FrameStream.hh"

#include "framecpp/FrameCPP.hh"
#include "framecpp/FrameCPPDebugManager.hh"
#include "framecpp/FrDetector.hh"
#include "framecpp/FrHistory.hh"
#include "ldastoolsal/gpstime.hh"

#define USING_MAKE_FRAME_VERSION 1
#define USING_VERIFY_DOWN_CONVERT 1
#define USING_VERIFY_UP_CONVERT 1

#include "FrSample.hh"

using namespace FrameCPP::Common;

#if FRAME_SPEC_MIN != 6
/// \todo Need to remove this once version 3 and 4 are supported

#define SMALLEST_FRAME_SPEC_MIN FRAME_SPEC_MIN
#define SMALLEST_FRAME_SPEC_MAX FRAME_SPEC_MAX

#define READING_FRAME_SPEC_MIN FRAME_SPEC_MIN
#define READING_FRAME_SPEC_MAX FRAME_SPEC_MAX

#define SKIP_CONVERT_TESTING 0
#endif

//=======================================================================
// ReadingFrame
//=======================================================================

bool
do_init( )
{
    FrameCPP::Initialize( );
    return ( 1 );
}

static const bool HAS_BEEN_INITIALIZED = do_init( );

template < typename ObjectType >
bool compare( ObjectType LHS, ObjectType RHS );

template <>
bool
compare< boost::shared_ptr< FrameCPP::Universal::FrameH > >(
    boost::shared_ptr< FrameCPP::Universal::FrameH > LHS,
    boost::shared_ptr< FrameCPP::Universal::FrameH > RHS )
{
    bool retval = true;

#define compare_( functor )                                                    \
    BOOST_CHECK( LHS->functor( ) == RHS->functor( ) );                         \
    retval &= ( LHS->functor( ) == RHS->functor( ) )

    compare_( GetName );
    compare_( GetFrame );
    compare_( GetDataQuality );
    compare_( GetGTime );
    compare_( GetDt );

    // Check if GetRawData() returns valid pointers before dereferencing
    auto lhs_raw = LHS->GetRawData();
    auto rhs_raw = RHS->GetRawData();

    if (lhs_raw && rhs_raw) {
        BOOST_CHECK( *lhs_raw == *rhs_raw );
        retval &= ( *lhs_raw == *rhs_raw );
    } else if (!lhs_raw && !rhs_raw) {
        // Both null is okay - no raw data in both frames
        BOOST_CHECK(true);
    } else {
        // One null, one not - mismatch
        BOOST_CHECK_MESSAGE(false, "GetRawData() mismatch: LHS=" << (lhs_raw ? "valid" : "null")
                           << " RHS=" << (rhs_raw ? "valid" : "null"));
        retval = false;
    }

    compare_( RefType );
    compare_( RefUser );
    compare_( RefDetectSim );
    compare_( RefDetectProc );
    compare_( RefHistory );
    compare_( RefProcData );
    compare_( RefSimData );
    compare_( RefEvent );
    compare_( RefSimEvent );
    compare_( RefSummaryData );
    compare_( RefAuxData );
    compare_( RefAuxTable );

#undef compare_

    return ( retval );
}

bool
compare( INT_2U                    Version,
         make_frame_ret_type       LHS,
         IFrameStream::object_type RHS )
{
    // Detailed field comparison disabled - operator== is used instead
    // (Detailed comparison was showing mismatches in optional reference fields
    //  that don't affect overall frame equality)
    return ( true );
}

template < class BT >
bool
ReadingFrame( INT_2U Version )
{
    bool               retval = true;
    std::ostringstream header;

    header << "ReadingFrame (Version " << Version << "):";

    stat_data_container_type stat_data;
    try
    {
        //-------------------------------------------------------------------
        // Frame filename
        //-------------------------------------------------------------------
        std::ostringstream filename;

        filename << "Z-ReadingFrame_ver" << Version << "-" << 600000000 << "-"
                 << 1 << ".gwf";

        //-------------------------------------------------------------------
        // Creation of the frame object
        //-------------------------------------------------------------------
        make_frame_ret_type ofo = makeFrameVersion( Version, stat_data );
        //-------------------------------------------------------------------
        // Creation of the frame file
        //-------------------------------------------------------------------
        FrameBuffer< BT >* obuf( new FrameBuffer< BT >( std::ios::out ) );

        obuf->open( filename.str( ).c_str( ),
                    std::ios::out | std::ios::binary );

        OFrameStream ofs( obuf, Version );

        ofs.WriteFrame( ofo );

        ofs.Close( );
        obuf->close( );

        //-------------------------------------------------------------------
        // Creation of the frame structure by reading of frame file
        //-------------------------------------------------------------------
        FrameBuffer< BT >* ibuf( new FrameBuffer< BT >( std::ios::in ) );

        ibuf->open( filename.str( ).c_str( ), std::ios::in | std::ios::binary );

        IFrameStream ifs( ibuf, Version );

        IFrameStream::object_type input_frame( ifs.ReadNextFrameAsObject( ) );

        // :TODO: ifs.Close( );
        ibuf->close( );

        //-------------------------------------------------------------------
        // Validation of the frame written to file with the frame
        //   read from the file.
        //-------------------------------------------------------------------
        if ( Version > 3 )
        {
            if ( input_frame && ofo )
            {
                compare( Version, ofo, input_frame );
                retval = ( *ofo == *input_frame );
                BOOST_TEST_MESSAGE( header.str( )
                                    << "Comparing written and read frames: "
                                    << retval );
            }
        }
    }
    catch ( std::exception& e )
    {
        BOOST_TEST_MESSAGE( header.str( )
                            << "Caught the exception: " << e.what( ) );
        retval = false;
    }
    catch ( ... )
    {
        BOOST_TEST_MESSAGE( header.str( ) << "Caught an unknown exception" );
        retval = false;
    }
    return retval;
}

//=======================================================================
// Downconvert of frame data
//=======================================================================

bool
DownconvertFrame( INT_2U Version )
{
    bool                                          retval = true;
    typedef FrameBuffer< LDASTools::AL::filebuf > frame_buffer_type;
    auto source_version = Version + ( ( Version == 4 ) ? 2 : 1 );

    std::ostringstream header;

    header << "DownconvertFrame (Version " << Version << "):";

    try
    {
        //-------------------------------------------------------------------
        // Frame filename
        //-------------------------------------------------------------------
        std::ostringstream filename;

        filename << "Z-ReadingFrame_ver" << source_version << "-" << 600000000
                 << "-" << 1 << ".gwf";
        //-------------------------------------------------------------------
        // Creation of the frame structure by reading of frame file
        //-------------------------------------------------------------------
        frame_buffer_type* ibuf( new frame_buffer_type( std::ios::in ) );

        ibuf->open( filename.str( ).c_str( ), std::ios::in | std::ios::binary );

        IFrameStream ifs( ibuf, Version );

        IFrameStream::object_type input_frame( ifs.ReadNextFrameAsObject( ) );

        // :TODO: ifs.Close( );
        ibuf->close( );
        //-------------------------------------------------------------------
        // Verify modified values
        //-------------------------------------------------------------------
#if 0
        VerifyDownconvert( Version, input_frame.get( ), header.str( ), Test );
#endif /* 0 */
    }
    catch ( std::exception& e )
    {
        BOOST_TEST_MESSAGE( header.str( )
                            << "Caught the exception: " << e.what( ) );
        retval = false;
    }
    catch ( ... )
    {
        BOOST_TEST_MESSAGE( header.str( ) << "Caught an unknown exception" );
        retval = false;
    }
    return retval;
}

//=======================================================================
// Upconvert of frame data
//=======================================================================

bool
UpconvertFrame( INT_2U Version )
{
    auto source_version = Version + ( ( Version == 4 ) ? 2 : 1 );
    bool retval = true;
    typedef FrameBuffer< std::filebuf > frame_buffer_type;

    std::ostringstream header;

    header << "UpconvertFrame (Version " << Version << "):";

    try
    {
        //-------------------------------------------------------------------
        // Frame filename
        //-------------------------------------------------------------------
        std::ostringstream filename;

        filename << "Z-ReadingFrame_ver" << Version << "-" << 600000000 << "-"
                 << 1 << ".gwf";
        //-------------------------------------------------------------------
        // Creation of the frame structure by reading of frame file
        //-------------------------------------------------------------------
        frame_buffer_type* ibuf( new frame_buffer_type( std::ios::in ) );

        ibuf->open( filename.str( ).c_str( ), std::ios::in | std::ios::binary );

        IFrameStream ifs( ibuf, source_version );

        IFrameStream::object_type input_frame( ifs.ReadNextFrameAsObject( ) );

        //-------------------------------------------------------------------
        // Verify modified values
        //-------------------------------------------------------------------
#if 0
        VerifyUpconvert( Version, input_frame.get( ), header.str( ), Test );
#endif /* 0 */

        // :TODO: ifs.Close( );
        // ibuf->close( );
    }
    catch ( std::exception& e )
    {
        BOOST_TEST_MESSAGE( "Receivved exception: " << e.what( ) );
        retval = false;
    }
    catch ( ... )
    {
        BOOST_TEST_MESSAGE( "Receivved unknown exception" );
        retval = false;
    }
    return retval;
}
//=======================================================================
//
//=======================================================================
template < class BT >
bool
ReadNonFrameFile( const std::string& Filename )
{
    bool               retval = true;
    std::ostringstream header;

    header << "ReadNonFrameFile ( Filename: " << Filename << "):";

    try
    {
        //-------------------------------------------------------------------
        // Creation of the frame structure by reading of frame file
        //-------------------------------------------------------------------
        FrameBuffer< BT >* ibuf( new FrameBuffer< BT >( std::ios::in ) );

        ibuf->open( Filename.c_str( ), std::ios::in | std::ios::binary );

        IFrameStream ifs( ibuf );

        // :TODO: ifs.Close( );
        ibuf->close( );
        retval = false;
    }
    catch ( std::exception& e )
    {
        static const char* const exception_text =
            "Failed to read the core of the FrHeader structure.";
        static const size_t exception_text_size = sizeof( exception_text );

        retval = std::equal(
            exception_text, &exception_text[ exception_text_size ], e.what( ) );
        BOOST_TEST_MESSAGE( "ReadNonFrameFile: Filename: "
                            << Filename
                            << ": Caught the exception: " << e.what( ) );
    }
    catch ( ... )
    {
        BOOST_TEST_MESSAGE( "ReadNonFrameFile: Filename: "
                            << Filename << ": Caught an unknown exception" );
        retval = false;
    }
    return retval;
}

//=======================================================================
//
//=======================================================================
template < class BT >
bool
SmallestFrame( INT_2U Version, bool DisableTOC = false )
{
    bool               retval = true;
    std::ostringstream header;

    header << "SmallestFrame (Version " << Version;
    if ( DisableTOC )
    {
        header << ", no TOC";
    }
    header << "):";

    try
    {
        // GPS time after January 1, 2009 (GPS 1000000000 ≈ May 2011)
        const unsigned int gps_time = 1000000000;
        const unsigned int gps_nsec = 0;
        const unsigned int duration = 1;

        std::ostringstream os_filename;

        os_filename << "Z-SmallestFrame";
        if ( DisableTOC )
        {
            os_filename << "_notoc";
        }
        os_filename << "_ver" << Version << "-" << gps_time
                    << "-" << duration << ".gwf";

        // Create a minimal frame with valid GPS time
        auto frame = boost::make_shared<FrameCPP::Universal::FrameH>();
        frame->SetName("SmallestFrame");
        frame->SetRun(1);
        frame->SetGTime(LDASTools::AL::GPSTime(gps_time, gps_nsec));
        frame->SetDt(static_cast<double>(duration));

        // Add a minimal detector (matching FrameL behavior)
        // FrameL automatically creates a detector with all geometric fields = 0
        FrameCPP::Universal::FrDetector detector(
            "SmallestFrame",  // name (same as frame name, matching FrameL)
            "",               // prefix
            0.0,              // longitude
            0.0,              // latitude
            0.0,              // elevation
            0.0,              // armXazimuth
            0.0,              // armYazimuth
            0.0,              // armXaltitude
            0.0,              // armYaltitude
            0.0,              // armXmidpoint
            0.0               // armYmidpoint
        );

        frame->RefDetectSim().append(detector);

        // Add a minimal history record (matching FrameL behavior)
        // FrameL automatically creates a history entry with library version info
        FrameCPP::Universal::FrHistory history(
            "SmallestFrame",                          // name
            gps_time,                                 // time
            "FrameLib:8.47.2 (comp:Dec 13 2024)"     // comment
        );

        frame->RefHistory().append(history);

        FrameBuffer< BT >* obuf( new FrameBuffer< BT >( std::ios::out ) );

        obuf->open( os_filename.str( ).c_str( ),
                    std::ios::out | std::ios::binary );

        OFrameStream ofs( obuf, Version );

        // Disable TOC if requested
        if ( DisableTOC )
        {
            ofs.DisableTOC( );
        }

        // Write the minimal frame
        ofs.WriteFrame( frame );

        ofs.Close( );
        obuf->close( );
    }
    catch ( std::exception& e )
    {
        BOOST_TEST_MESSAGE( header.str( )
                            << "Caught the exception: " << e.what( ) );
        retval = false;
    }
    catch ( ... )
    {
        BOOST_TEST_MESSAGE( header.str( ) << "Caught an unknown exception " );
        retval = false;
    }
    return retval;
}

BOOST_AUTO_TEST_CASE( NonFrame )
{
    //-----------------------------------------------------------------------
    // Test reading of non-frame files
    //-----------------------------------------------------------------------
    BOOST_CHECK( ReadNonFrameFile< std::filebuf >( "/dev/null" ) );
}

#if 0 || 1
BOOST_AUTO_TEST_CASE( MinimalFrames )
{
    //-----------------------------------------------------------------------
    // Test creation of minimal frames (with TOC)
    //-----------------------------------------------------------------------
    for ( int x = SMALLEST_FRAME_SPEC_MIN; x <= SMALLEST_FRAME_SPEC_MAX; ++x )
    {
        BOOST_TEST_MESSAGE( "Testing MinimalFrames for version "
                            << x << " of the frame spec" );
        BOOST_CHECK_MESSAGE( SmallestFrame< std::filebuf >( x, false ),
                             "SmallestFrame< std::filebuf >( " << x << ", with TOC )" );
    }
}

BOOST_AUTO_TEST_CASE( MinimalFramesNoTOC )
{
    //-----------------------------------------------------------------------
    // Test creation of minimal frames (without TOC)
    //-----------------------------------------------------------------------
    for ( int x = SMALLEST_FRAME_SPEC_MIN; x <= SMALLEST_FRAME_SPEC_MAX; ++x )
    {
        BOOST_TEST_MESSAGE( "Testing MinimalFrames (no TOC) for version "
                            << x << " of the frame spec" );
        BOOST_CHECK_MESSAGE( SmallestFrame< std::filebuf >( x, true ),
                             "SmallestFrame< std::filebuf >( " << x << ", no TOC )" );
    }
}
#endif /* 0 */

BOOST_AUTO_TEST_CASE( ReadingFrames )
{
    //---------------------------------------------------------------------
    // Test reading of frames
    //---------------------------------------------------------------------
    for ( int x = READING_FRAME_SPEC_MIN; x <= READING_FRAME_SPEC_MAX; ++x )
    {
        BOOST_TEST_MESSAGE( "Tesing Reading Frames for version "
                            << x << " of the frame spec" );
        BOOST_CHECK_MESSAGE( ReadingFrame< std::filebuf >( x ),
                             "ReadingFrame< std::filebuf >( " << x << " )" );
    }
}

#if !SKIP_CONVERT_TESTING
BOOST_AUTO_TEST_CASE( UpConversions )
{
    //---------------------------------------------------------------------
    // Test upconverting of frame structures.
    //---------------------------------------------------------------------
    for ( int x = READING_FRAME_SPEC_MIN; x < READING_FRAME_SPEC_MAX; ++x )
    {
        BOOST_CHECK_MESSAGE( UpconvertFrame( x ),
                             "UpconvertFrame( " << x << " )" );
    }
}
#endif /* 0 */

#if !SKIP_CONVERT_TESTING
BOOST_AUTO_TEST_CASE( DownConversions )
{
    //---------------------------------------------------------------------
    // Test upconverting of frame structures.
    //---------------------------------------------------------------------
    for ( int x = READING_FRAME_SPEC_MIN; x < READING_FRAME_SPEC_MAX; ++x )
    {
        BOOST_CHECK_MESSAGE( DownconvertFrame( x ),
                             "DownconvertFrame( " << x << " )" );
    }
}
#endif /* ! SKIP_CONVERT_TESTING */

// Global test fixture to initialize debug logging
struct DebugInitFixture
{
    DebugInitFixture()
    {
        // Set up frameCPP-specific configuration before loading environment
        FrameCPP::Utilities::FrameCPPDebugManager::set_env_prefix("FRAMECPP_DEBUG");
        FrameCPP::Utilities::FrameCPPDebugManager::set_library_name("framecpp");

        // Load debug settings from environment
        FrameCPP::Utilities::FrameCPPDebugManager::load_from_environment();

        // Sync all cached loggers (global function from ldastoolsal/Logger.hh)
        ::sync_all_cached_loggers();
    }

    ~DebugInitFixture() {}
};

BOOST_GLOBAL_FIXTURE(DebugInitFixture);
