//
// LDASTools frameCPP - A library implementing the LIGO/Virgo frame specification
//
// Copyright (C) 2025 California Institute of Technology
//
// Test infrastructure for I/O-based version compatibility testing
//

#ifndef FRAMECPP_TEST_FRAME_IO_TEST_HELPERS_HH
#define FRAMECPP_TEST_FRAME_IO_TEST_HELPERS_HH

#include <boost/shared_ptr.hpp>
#include <boost/shared_array.hpp>
#include <boost/make_shared.hpp>
#include <boost/test/unit_test.hpp>
#include <string>
#include <sstream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>
#include <unistd.h>

#include "framecpp/IFrameStream.hh"
#include "framecpp/OFrameStream.hh"
#include "framecpp/FrameH.hh"
#include "framecpp/FrAdcData.hh"
#include "framecpp/FrProcData.hh"
#include "framecpp/FrSimData.hh"
#include "framecpp/FrRawData.hh"
#include "framecpp/FrVect.hh"
#include "framecpp/Dimension.hh"
#include "ldastoolsal/gpstime.hh"

namespace FrameIOTest
{
    using namespace FrameCPP;
    using LDASTools::AL::GPSTime;

    //=========================================================================
    // Temporary File Management
    //=========================================================================

    /**
     * @brief RAII wrapper for temporary test files
     */
    class TempFile
    {
    public:
        TempFile(const std::string& prefix = "test_frame")
            : m_filename(generate_filename(prefix))
        {
        }

        ~TempFile()
        {
            if (!m_filename.empty())
            {
                std::remove(m_filename.c_str());
            }
        }

        const std::string& filename() const { return m_filename; }

        // Prevent copying
        TempFile(const TempFile&) = delete;
        TempFile& operator=(const TempFile&) = delete;

    private:
        static std::string generate_filename(const std::string& prefix)
        {
            static int counter = 0;
            std::ostringstream oss;
            oss << prefix << "_" << getpid() << "_" << counter++ << ".gwf";
            return oss.str();
        }

        std::string m_filename;
    };

    //=========================================================================
    // Frame Construction Helpers
    //=========================================================================

    /**
     * @brief Create a minimal valid frame for testing
     */
    inline boost::shared_ptr<FrameH> create_test_frame(
        const std::string& name = "TestFrame",
        int gps_seconds = 1000000000,
        double dt = 1.0)
    {
        // Use constructor with all required parameters (matches working test pattern)
        auto frame = boost::make_shared<FrameH>(
            name,
            1,  // run
            0,  // frame number
            GPSTime(gps_seconds, 0),
            dt,
            0);  // dataQuality
        return frame;
    }

    //=========================================================================
    // Generic I/O Operations
    //=========================================================================

    /**
     * @brief Write a frame to a file at a specific version
     */
    inline void write_frame(
        const std::string& filename,
        boost::shared_ptr<FrameH> frame,
        int version = 9)
    {
        OFrameFStream out(filename.c_str(), version);
        out.WriteFrame(frame);
        out.Stream()->Close();  // Explicitly close the underlying stream
    }

    /**
     * @brief Read a frame from a file
     */
    inline boost::shared_ptr<FrameH> read_frame(
        const std::string& filename,
        int frame_number = 0)
    {
        IFrameFStream in(filename.c_str());
        return in.ReadFrameN(frame_number);
    }

    //=========================================================================
    // Version Compatibility Testing
    //=========================================================================

    /**
     * @brief Test I/O round-trip: write at one version, read back
     *
     * This is the replacement for promote/demote testing. It validates
     * that frame objects can be written at one version and read back
     * correctly, which is what actually matters for compatibility.
     */
    inline void test_frame_io_round_trip(
        boost::shared_ptr<FrameH> frame,
        int write_version,
        const std::string& test_name = "")
    {
        TempFile temp_file(test_name.empty() ? "frame_io" : test_name);

        // Write at specified version
        write_frame(temp_file.filename(), frame, write_version);

        // Read back (always reads into Universal objects regardless of file version)
        auto frame_read = read_frame(temp_file.filename(), 0);

        // Validate basic frame properties
        BOOST_CHECK(frame_read != nullptr);
        if (frame_read)
        {
            BOOST_CHECK_EQUAL(frame_read->GetName(), frame->GetName());
            BOOST_CHECK(frame_read->GetGTime() == frame->GetGTime());
            BOOST_CHECK_CLOSE(frame_read->GetDt(), frame->GetDt(), 1e-9);
            BOOST_CHECK_EQUAL(frame_read->GetRun(), frame->GetRun());
        }
    }

    /**
     * @brief Test cross-version compatibility: write at version A, "read" as version B
     *
     * Note: In the Universal architecture, we always read into Universal objects.
     * The version adaptation happens automatically at the I/O layer (pCreate).
     * This test validates that files written at different versions can be read correctly.
     */
    inline void test_cross_version_compatibility(
        boost::shared_ptr<FrameH> frame,
        int write_version,
        int verify_version,  // Used for test naming only
        const std::string& test_name = "")
    {
        std::ostringstream name;
        name << (test_name.empty() ? "cross_version" : test_name)
             << "_v" << write_version << "_to_v" << verify_version;

        TempFile temp_file(name.str());

        // Write at specified version
        write_frame(temp_file.filename(), frame, write_version);

        // Read back - Universal objects handle all versions
        auto frame_read = read_frame(temp_file.filename(), 0);

        // Validate
        BOOST_CHECK(frame_read != nullptr);
        if (frame_read)
        {
            BOOST_CHECK_EQUAL(frame_read->GetName(), frame->GetName());
            BOOST_CHECK(frame_read->GetGTime() == frame->GetGTime());
        }
    }

    //=========================================================================
    // Object-Specific Helpers
    //=========================================================================

    /**
     * @brief Add FrAdcData to a frame (via FrRawData)
     */
    inline void add_adc_data(
        boost::shared_ptr<FrameH> frame,
        const std::string& name,
        int channel_group = 0,
        int num_samples = 100)
    {
        // Create ADC data using constructor
        auto adc = boost::make_shared<FrAdcData>(name, channel_group, 0, -64, 16384.0);

        // Create and fill data vector
        auto dim = boost::make_shared<Dimension>(num_samples, 1.0/16384.0, "sample", 0.0);
        auto vect = boost::make_shared<FrVect>(name + "_data", FrVect::FR_VECT_4R, 1, dim.get(), "counts");

        // Fill with test pattern using GetDataRaw and memcpy pattern
        boost::shared_array<float> test_data(new float[num_samples]);
        for (int i = 0; i < num_samples; ++i)
        {
            test_data[i] = static_cast<float>(i);
        }
        std::memcpy(vect->GetDataRaw().get(), test_data.get(), num_samples * sizeof(float));

        adc->RefData().append(vect);

        // ADC data goes into RawData - create explicitly
        auto rawData = frame->GetRawData();
        if (!rawData) {
            rawData = boost::make_shared<FrRawData>("Raw");
            frame->SetRawData(rawData);
        }
        rawData->RefFirstAdc().append(adc);
    }

    /**
     * @brief Add FrProcData to a frame
     */
    inline void add_proc_data(
        boost::shared_ptr<FrameH> frame,
        const std::string& name,
        int num_samples = 100)
    {
        auto proc = boost::make_shared<FrProcData>();

        // Create and fill data vector
        auto dim = boost::make_shared<Dimension>(num_samples, 1.0/num_samples, "sample", 0.0);
        auto vect = boost::make_shared<FrVect>(name + "_data", FrVect::FR_VECT_8R, 1, dim.get(), "counts");

        // Fill with test pattern using GetDataRaw and memcpy pattern
        boost::shared_array<double> test_data(new double[num_samples]);
        for (int i = 0; i < num_samples; ++i)
        {
            test_data[i] = static_cast<double>(i) * 0.1;
        }
        std::memcpy(vect->GetDataRaw().get(), test_data.get(), num_samples * sizeof(double));

        proc->RefData().append(vect);
        frame->RefProcData().append(proc);
    }

    /**
     * @brief Add FrSimData to a frame
     */
    inline void add_sim_data(
        boost::shared_ptr<FrameH> frame,
        const std::string& name,
        int num_samples = 100)
    {
        auto sim = boost::make_shared<FrSimData>();

        // Create and fill data vector
        auto dim = boost::make_shared<Dimension>(num_samples, 1.0/num_samples, "sample", 0.0);
        auto vect = boost::make_shared<FrVect>(name + "_data", FrVect::FR_VECT_4R, 1, dim.get(), "counts");

        // Fill with test pattern using GetDataRaw and memcpy pattern
        boost::shared_array<float> test_data(new float[num_samples]);
        for (int i = 0; i < num_samples; ++i)
        {
            test_data[i] = std::sin(i * 0.1f);
        }
        std::memcpy(vect->GetDataRaw().get(), test_data.get(), num_samples * sizeof(float));

        sim->RefData().append(vect);
        frame->RefSimData().append(sim);
    }

    /**
     * @brief Validate FrAdcData fields match between original and read-back
     */
    inline void validate_adc_data(
        boost::shared_ptr<FrAdcData> original,
        boost::shared_ptr<FrAdcData> read_back)
    {
        BOOST_REQUIRE(original != nullptr);
        BOOST_REQUIRE(read_back != nullptr);

        // Basic validation - just check name matches
        BOOST_CHECK_EQUAL(read_back->GetName(), original->GetName());

        // Validate data vector exists
        auto orig_data = original->RefData();
        auto read_data = read_back->RefData();

        BOOST_CHECK_EQUAL(read_data.size(), orig_data.size());
    }

    /**
     * @brief Validate FrProcData fields match
     */
    inline void validate_proc_data(
        boost::shared_ptr<FrProcData> original,
        boost::shared_ptr<FrProcData> read_back)
    {
        BOOST_REQUIRE(original != nullptr);
        BOOST_REQUIRE(read_back != nullptr);

        // Basic validation - just check name matches
        BOOST_CHECK_EQUAL(read_back->GetName(), original->GetName());
    }

    /**
     * @brief Validate FrSimData fields match
     */
    inline void validate_sim_data(
        boost::shared_ptr<FrSimData> original,
        boost::shared_ptr<FrSimData> read_back)
    {
        BOOST_REQUIRE(original != nullptr);
        BOOST_REQUIRE(read_back != nullptr);

        // Basic validation - just check name matches
        BOOST_CHECK_EQUAL(read_back->GetName(), original->GetName());
    }

    //=========================================================================
    // Multi-Version Testing Helpers
    //=========================================================================

    /**
     * @brief Test a frame across all supported versions (v3-v9, excluding v5)
     */
    inline void test_all_version_compatibility(
        boost::shared_ptr<FrameH> frame,
        const std::string& test_name = "")
    {
        const int versions[] = {3, 4, 6, 7, 8, 9};

        for (int write_ver : versions)
        {
            // Test write at version X, read back
            test_frame_io_round_trip(frame, write_ver,
                                    test_name + "_v" + std::to_string(write_ver));

            // Test cross-version: write at X, verify readable
            for (int read_ver : versions)
            {
                test_cross_version_compatibility(frame, write_ver, read_ver, test_name);
            }
        }
    }

} // namespace FrameIOTest

#endif // FRAMECPP_TEST_FRAME_IO_TEST_HELPERS_HH
