//
// LDASTools frameCPP - Boost.Test Debug Helper for frameCPP tests
//
// 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.
//

#ifndef FRAMECPP_TEST_BOOST_TEST_DEBUG_HELPER_HH
#define FRAMECPP_TEST_BOOST_TEST_DEBUG_HELPER_HH

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

#include <boost/test/included/unit_test.hpp>
#include <boost/test/framework.hpp>
#include <boost/log/core.hpp>
#include <boost/log/sinks/basic_sink_backend.hpp>
#include <boost/log/sinks/sync_frontend.hpp>
#include <boost/log/attributes/value_extraction.hpp>
#include <boost/log/expressions.hpp>
#include <boost/log/utility/formatting_ostream.hpp>
#include <boost/log/utility/manipulators/to_log.hpp>

#include "ldastoolsal/LoggerManager.hh"
#include "ldastoolsal/SimpleLogger.hh"
#include "framecpp/Common/FrameCPPServices.hh"
#include "framecpp/FrameCPPDebugManager.hh"

namespace FrameCPP {
namespace Test {

/**
 * @brief Custom Boost.Log sink backend that redirects log messages to Boost.Test
 * 
 * This backend intercepts all boost::log messages and redirects them to 
 * BOOST_TEST_MESSAGE so they appear as test framework info messages rather
 * than going to stderr.
 */
class BoostTestSinkBackend : public boost::log::sinks::basic_sink_backend<
    boost::log::sinks::combine_requirements<
        boost::log::sinks::synchronized_feeding
    >::type>
{
public:
    /**
     * @brief Process a log record and send it to Boost.Test
     * @param rec The log record to process
     */
    void consume(boost::log::record_view const& rec) {
        std::string formatted_message;
        
        // Extract channel name if available
        auto channel = boost::log::extract<std::string>("Channel", rec);
        if (channel) {
            formatted_message += "[" + *channel + "] ";
        }
        
        // Extract the message itself using boost::log's formatting stream
        boost::log::formatting_ostream format_strm(formatted_message);
        format_strm << rec[boost::log::expressions::smessage];
        
        // Send to Boost.Test as an info message
        // This will respect Boost.Test's verbosity settings
        BOOST_TEST_MESSAGE(formatted_message);
    }
};

/**
 * @brief Helper class for integrating FrameCPP debug options with Boost.Test
 * 
 * This class provides a simple way to add debug command-line options to any
 * Boost.Test-based test program. It handles the complex initialization sequence
 * required to process custom arguments passed after the "--" separator.
 * 
 * Usage:
 * @code
 * // At the top of your test file, after includes:
 * #define BOOST_TEST_MODULE MyTestModule
 * #include <boost/test/included/unit_test.hpp>
 * #include "BoostTestDebugHelper.hh"
 * 
 * // Create the helper instance (this registers everything automatically)
 * FRAMECPP_TEST_DEBUG_HELPER("my_test_name");
 * 
 * // Your test cases follow...
 * BOOST_AUTO_TEST_CASE(my_test) {
 *     // Test code here
 * }
 * @endcode
 * 
 * Command line usage:
 * @code
 * ./my_test -- --debug-level 30
 * ./my_test -- --debug-channels "framecpp.frtoc.*:20"
 * ./my_test -- --debug-help
 * ./my_test -- --debug-show
 * @endcode
 */
class BoostTestDebugHelper {
private:
    // Static storage for services (needed for lifetime management)
    static std::unique_ptr<LDASTools::AL::LoggerManager> s_logger_mgr;
    static std::unique_ptr<FrameCPP::Common::FrameCPPServices> s_services;
    static std::string s_program_name;
    static bool s_debug_enabled;
    
    // Private constructor - use the macro instead
    BoostTestDebugHelper() = delete;
    
public:
    /**
     * @brief Create a Boost.Test sink instance
     * 
     * This creates a sink instance that redirects all boost::log messages
     * to Boost.Test info messages. Following dependency injection principles,
     * this creates a single instance to be injected into LoggerManager.
     */
    static boost::shared_ptr<boost::log::sinks::sink> create_boost_test_sink() {
        // Create our custom sink backend
        auto backend = boost::make_shared<BoostTestSinkBackend>();
        
        // Wrap it in a synchronous frontend
        auto sink = boost::make_shared<boost::log::sinks::synchronous_sink<BoostTestSinkBackend>>(backend);
        
        return sink;
    }
    
    /**
     * @brief Initialize the debug system (called by Boost.Test)
     * 
     * This function is called automatically by Boost.Test during initialization.
     * It processes command-line arguments and sets up the debug system.
     * 
     * @return true if tests should run, false if help was displayed
     */
    static bool init_unit_test() {
        // Get the original argc/argv from Boost.Test
        // Note: Boost.Test removes the "--" separator and passes custom arguments directly
        int argc = boost::unit_test::framework::master_test_suite().argc;
        char** argv = boost::unit_test::framework::master_test_suite().argv;
        
        // Create custom sink for Boost.Test integration
        // Following Muldoon's dependency injection principles - create dependency first
        auto boost_test_sink = create_boost_test_sink();
        
        // Set global custom sink to override ALL SimpleLogger instances (including static ones)
        // This ensures even legacy static loggers use our Boost.Test sink
        SimpleLogger::set_global_custom_sink(boost_test_sink);
        
        // Initialize services with custom sink injection
        // LoggerManager receives the sink via constructor injection (for consistency)
        s_logger_mgr = std::make_unique<LDASTools::AL::LoggerManager>(boost_test_sink);
        s_services = std::make_unique<FrameCPP::Common::FrameCPPServices>(*s_logger_mgr);
        
        // Check if we have custom arguments (more than just the executable name)
        if (argc > 1) {
            try {
                // Initialize debug system with ALL arguments (including argv[0])
                // FrameCPPDebugManager expects standard argc/argv with program name in argv[0]
                bool init_result = FrameCPP::Utilities::FrameCPPDebugManager::initialize_with_services(
                    argc, argv, *s_services, s_program_name);
                
                if (!init_result) {
                    // Help was requested - debug system handled it
                    // Return false to indicate tests should not run
                    return false;
                }
                
                s_debug_enabled = true;
            } catch (const std::exception& e) {
                // If debug option parsing fails, show error using Boost.Test messaging
                // Use BOOST_TEST_MESSAGE for info that should be visible at normal verbosity
                BOOST_TEST_MESSAGE("Error parsing debug options: " << e.what());
                BOOST_TEST_MESSAGE("Usage: ./" << s_program_name 
                                   << " [boost_test_options...] -- [debug_options...]");
                BOOST_TEST_MESSAGE("Examples:");
                BOOST_TEST_MESSAGE("  ./" << s_program_name << " -- --debug-level 30");
                BOOST_TEST_MESSAGE("  ./" << s_program_name 
                                   << " -- --debug-channels \"framecpp.*:20\"");
                BOOST_TEST_MESSAGE("  ./" << s_program_name << " -- --debug-help");
            }
        }
        
        // Initialize FrameCPP
        FrameCPP::Initialize();
        return true;
    }
    
    /**
     * @brief Clean up the debug system (called by Boost.Test)
     * 
     * This function is called automatically when the test program exits.
     */
    static void cleanup() {
        // Clean shutdown of services
        if (s_logger_mgr) {
            s_logger_mgr->shutdown();
        }
        s_services.reset();
        s_logger_mgr.reset();
        
        // Clear global custom sink to restore default behavior
        SimpleLogger::set_global_custom_sink(nullptr);
    }
    
    /**
     * @brief Set the program name for help messages
     * @param name The name to use in help text
     */
    static void set_program_name(const std::string& name) {
        s_program_name = name;
    }
    
    /**
     * @brief Check if debug options were enabled
     * @return true if debug options were processed
     */
    static bool is_debug_enabled() {
        return s_debug_enabled;
    }
    
    /**
     * @brief Get the services object for use in tests
     * @return Reference to the FrameCPPServices object
     */
    static FrameCPP::Common::FrameCPPServices& get_services() {
        if (!s_services) {
            throw std::runtime_error("Services not initialized");
        }
        return *s_services;
    }
};

// Static member definitions
std::unique_ptr<LDASTools::AL::LoggerManager> BoostTestDebugHelper::s_logger_mgr;
std::unique_ptr<FrameCPP::Common::FrameCPPServices> BoostTestDebugHelper::s_services;
std::string BoostTestDebugHelper::s_program_name = "test_program";
bool BoostTestDebugHelper::s_debug_enabled = false;

/**
 * @brief Global test fixture that integrates with Boost.Test
 */
struct FrameCPPTestGlobalFixture {
    FrameCPPTestGlobalFixture() {
        // Call our initialization function at the start
        if (!BoostTestDebugHelper::init_unit_test()) {
            // If initialization failed (e.g., help was requested), throw to stop tests
            throw std::runtime_error("Debug help was requested and displayed");
        }
    }
    
    ~FrameCPPTestGlobalFixture() {
        // Clean shutdown
        BoostTestDebugHelper::cleanup();
    }
};

} // namespace Test
} // namespace FrameCPP

/**
 * @brief Macro to easily enable debug support in a test program
 * 
 * This macro should be placed after the BOOST_TEST_MODULE definition
 * and before any test cases.
 * 
 * @param program_name The name to use in help messages (without path or extension)
 */
#define FRAMECPP_TEST_DEBUG_HELPER(program_name) \
    namespace { \
        struct _FrameCPPTestDebugInit { \
            _FrameCPPTestDebugInit() { \
                ::FrameCPP::Test::BoostTestDebugHelper::set_program_name(program_name); \
            } \
        } _framecpp_test_debug_init_instance; \
    } \
    using FrameCPPTestGlobalFixture = ::FrameCPP::Test::FrameCPPTestGlobalFixture; \
    BOOST_GLOBAL_FIXTURE(FrameCPPTestGlobalFixture);

#endif // FRAMECPP_TEST_BOOST_TEST_DEBUG_HELPER_HH