//
// LDASTools frameCPP - Sequential Reader No-TOC Test
//
// Copyright (C) 2025 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.
//

#include <iostream>
#include <sstream>
#include <stdexcept>
#include <memory>
#include <cstdlib>

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

// FrameCPP includes
#include "framecpp/FrameCPP.hh"
#include "framecpp/Common/FrameSpec.hh"
#include "framecpp/Common/MemoryBuffer.hh"
#include "framecpp/Common/FrameStream.hh"
#include "framecpp/Common/SequentialFrameReader.hh"
#include "framecpp/Common/FrameCPPServices.hh"

// LDAS Tools includes
#include "ldastoolsal/LoggerManager.hh"

// Frame sample helper
#define USING_MAKE_FRAME_VERSION 1
#include "FrSample.hh"

using namespace FrameCPP;
using namespace FrameCPP::Common;

/// \brief Global test configuration
struct TestConfig
{
    int         version = 0;  // 0 means test all versions
    bool        verbose = false;
    std::string command;
};

static TestConfig g_config;

/// \brief Global logger manager (intentionally leaked to avoid destructor ordering issues)
static LDASTools::AL::LoggerManager* g_logger_mgr = nullptr;

/// \brief Initialize FrameCPP (must run before any frame operations)
bool
do_init( )
{
    FrameCPP::Initialize( );
    return true;
}

static const bool HAS_BEEN_INITIALIZED = do_init( );

/// \brief Global initializer - runs before main() to parse environment variables and create LoggerManager
struct GlobalInitializer
{
    GlobalInitializer( )
    {
        // Create LoggerManager early (after FrameCPP::Initialize())
        // Intentionally leaked to avoid static destructor ordering issues
        g_logger_mgr = new LDASTools::AL::LoggerManager( );

        // Parse environment variables
        const char* version_env = std::getenv( "FRAMECPP_TEST_VERSION" );
        if ( version_env )
        {
            try
            {
                g_config.version = std::stoi( version_env );
            }
            catch ( ... )
            {
                std::cerr << "Invalid FRAMECPP_TEST_VERSION: " << version_env
                          << std::endl;
            }
        }

        const char* verbose_env = std::getenv( "FRAMECPP_TEST_VERBOSE" );
        if ( verbose_env && std::string( verbose_env ) == "1" )
        {
            g_config.verbose = true;
        }
    }
};

// Create global instance to run initialization
static GlobalInitializer g_initializer;

/// \brief Test a single frame version without TOC
/// \param version Frame specification version to test
/// \return true if test passes, false otherwise
bool
testVersionNoTOC( int version )
{
    std::ostringstream msg;
    msg << "Testing frame version " << version << " (no TOC)";

    if ( g_config.verbose )
    {
        std::cout << msg.str( ) << "..." << std::endl;
    }

    BOOST_TEST_MESSAGE( msg.str( ) );

    try
    {
        // Create test frame
        stat_data_container_type stat_data;
        make_frame_ret_type      original_frame =
            makeFrameVersion( version, stat_data );

        if ( !original_frame )
        {
            BOOST_TEST_MESSAGE( "  FAIL: makeFrameVersion returned null" );
            return false;
        }

        // Write frame to MemoryBuffer WITHOUT TOC
        std::string frame_data;
        {
            MemoryBuffer* write_buffer = new MemoryBuffer( std::ios::out );
            OFrameStream  ofs( write_buffer, version );

            // Disable TOC - this is the key feature being tested!
            ofs.DisableTOC( );

            // Write the frame
            ofs.WriteFrame( original_frame );
            ofs.Close( );

            // Get the serialized data
            frame_data = write_buffer->str( );

            if ( g_config.verbose )
            {
                std::cout << "  Wrote " << frame_data.size( )
                          << " bytes (no TOC)" << std::endl;
            }

            // Note: OFrameStream takes ownership of the buffer, so don't delete it
        }

        // Read frame back using SequentialFrameReader and verify against original
        FrameCPPServices services( *g_logger_mgr );
        {
            // Create input MemoryBuffer and populate it
            MemoryBuffer* read_buffer = new MemoryBuffer( std::ios::in );
            read_buffer->str( frame_data );

            // Create SequentialFrameReader - takes ownership of buffer
            SequentialFrameReader reader( read_buffer, services, version );

            // Read all objects sequentially and verify we can read them
            size_t object_count = 0;
            reader.processAllObjects(
                [ &object_count ]( boost::shared_ptr< FrameSpec::Object > obj ) {
                    if ( obj )
                    {
                        ++object_count;
                    }
                    return true; // Continue processing
                } );

            const auto& stats = reader.getStatistics( );

            if ( g_config.verbose )
            {
                std::cout << "  Wrote " << frame_data.size( ) << " bytes (no TOC)" << std::endl;
                std::cout << "  Read " << stats.objects_read
                          << " objects sequentially" << std::endl;
                std::cout << "  Skipped: " << stats.objects_skipped
                          << " objects" << std::endl;
            }

            // Verify we read at least some objects
            BOOST_CHECK_GT( stats.objects_read, 0 );

            if ( stats.objects_read == 0 )
            {
                BOOST_TEST_MESSAGE( "  FAIL: No objects read" );
                return false;
            }
        }

        // Additionally verify the data can be read back with IFrameStream
        // and that it matches the original frame
        {
            MemoryBuffer* verify_buffer = new MemoryBuffer( std::ios::in );
            verify_buffer->str( frame_data );
            IFrameStream ifs( verify_buffer, version );
            auto read_frame = ifs.ReadNextFrameAsObject( );

            if ( !read_frame )
            {
                BOOST_TEST_MESSAGE( "  FAIL: Could not read frame back for verification" );
                return false;
            }

            // Verify that read frame matches original frame
            // This ensures the write/read cycle preserves all data correctly
            // including PTR_STRUCT-linked structures
            //
            // NOTE: For version 3, dataQuality field was added in version 4,
            // so we need to manually check fields that exist in version 3
            bool frames_match = false;
            if ( version == 3 )
            {
                // Version 3 doesn't have dataQuality, so compare manually
                auto* orig = dynamic_cast<const FrameCPP::Universal::FrameH*>( original_frame.get() );
                auto* read = dynamic_cast<const FrameCPP::Universal::FrameH*>( read_frame.get() );

                if ( orig && read )
                {
                    frames_match = ( orig->GetName() == read->GetName() ) &&
                                   ( orig->GetRun() == read->GetRun() ) &&
                                   ( orig->GetFrame() == read->GetFrame() ) &&
                                   ( orig->GetDt() == read->GetDt() ) &&
                                   ( orig->GetGTime() == read->GetGTime() );
                }
            }
            else
            {
                // Version 4+ can use operator==
                frames_match = ( *read_frame == *original_frame );
            }

            if ( !frames_match )
            {
                // Try to get more details about the failure
                auto* orig = dynamic_cast<const FrameCPP::Universal::FrameH*>( original_frame.get() );
                auto* read = dynamic_cast<const FrameCPP::Universal::FrameH*>( read_frame.get() );
                if ( orig && read )
                {
                    std::ostringstream details;
                    details << "  FAIL: Read frame does not match original frame (version " << version << ")\n";
                    details << "    Original: name='" << orig->GetName() << "' run=" << orig->GetRun()
                            << " frame=" << orig->GetFrame() << " dataQuality=" << orig->GetDataQuality()
                            << " dt=" << orig->GetDt() << "\n";
                    details << "    Read:     name='" << read->GetName() << "' run=" << read->GetRun()
                            << " frame=" << read->GetFrame() << " dataQuality=" << read->GetDataQuality()
                            << " dt=" << read->GetDt() << "\n";

                    auto orig_time = orig->GetGTime();
                    auto read_time = read->GetGTime();
                    details << "    Original GTime: " << orig_time.GetSeconds() << "." << orig_time.GetNanoseconds() << "\n";
                    details << "    Read GTime:     " << read_time.GetSeconds() << "." << read_time.GetNanoseconds();

                    BOOST_TEST_MESSAGE( details.str() );
                }
                else
                {
                    BOOST_TEST_MESSAGE( "  FAIL: Read frame does not match original frame (version " << version << ")" );
                }
                return false;
            }

            if ( g_config.verbose )
            {
                std::cout << "  Verified read frame matches original" << std::endl;
            }
        }

        BOOST_TEST_MESSAGE( "  PASS: Version " << version );
        return true;
    }
    catch ( const std::exception& e )
    {
        BOOST_TEST_MESSAGE( "  FAIL: Exception: " << e.what( ) );
        return false;
    }
    catch ( ... )
    {
        BOOST_TEST_MESSAGE( "  FAIL: Unknown exception" );
        return false;
    }
}

BOOST_AUTO_TEST_CASE( test_serial_reader_no_toc )
{
    if ( g_config.version > 0 )
    {
        // Test specific version
        BOOST_CHECK( testVersionNoTOC( g_config.version ) );
    }
    else
    {
        // Test all supported versions
        bool all_passed = true;

        for ( int v = FRAME_SPEC_MIN; v <= FRAME_SPEC_MAX; ++v )
        {
            // Skip version 5 (not in FRAME_VERSIONS list)
            if ( v == 5 )
                continue;

            if ( !testVersionNoTOC( v ) )
            {
                all_passed = false;
            }
        }

        BOOST_CHECK( all_passed );
    }
}

