//
// LDASTools frameCPP - Test definition consistency against PDF specification
//
// 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.
//
// This test uses PDF-derived definitions as GROUND TRUTH. Both FrameL and
// frameCPP implementations are compared against the official LIGO frame
// specification documents. Any deviations from the PDF spec are considered errors.
//

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

#include <map>
#include <string>
#include <vector>
#include <iostream>
#include <sstream>

#include "framecpp/FrameCPP.hh"
#include "framecpp/Common/Description.hh"
#include "framecpp/Common/FrameSpec.hh"
#include "framecpp/Universal/FrSH.hh"
#include "framecpp/Universal/FrSE.hh"

// Include PDF definitions as ground truth
#include "../src/Utilities/PDFDefinitions.hh"

#ifdef FRAMEL_FOUND
#include "FrameL.h"
#endif

using namespace FrameCPP;

// Structure to hold parsed field information
struct FieldInfo {
    std::string name;
    std::string type;
    std::string comment;

    bool operator==(const FieldInfo& other) const {
        return name == other.name && type == other.type;
    }
};

// Structure to hold parsed structure definition
struct StructureDefinition {
    std::string name;
    int id;
    std::string comment;
    std::vector<FieldInfo> fields;
};

// Get PDF definition (GROUND TRUTH from specification documents)
StructureDefinition getPDFDefinition(const std::string& objectName, unsigned int version) {
    const auto& defs = FrameCPP::PDFDefinitions::getDefinitions();

    auto versionIt = defs.find(version);
    if (versionIt == defs.end()) {
        throw std::runtime_error("No PDF definitions for version " + std::to_string(version));
    }

    auto objectIt = versionIt->second.find(objectName);
    if (objectIt == versionIt->second.end()) {
        throw std::runtime_error("Object '" + objectName +
                                "' not available in PDF definitions for version " +
                                std::to_string(version));
    }

    const auto& pdfDef = objectIt->second;

    StructureDefinition def;
    def.name = pdfDef.name;
    def.id = -1;  // PDF doesn't have internal IDs
    def.comment = pdfDef.comment;

    for (const auto& field : pdfDef.fields) {
        FieldInfo fieldInfo;
        fieldInfo.name = field.name;
        fieldInfo.type = field.type;
        fieldInfo.comment = field.comment;
        def.fields.push_back(fieldInfo);
    }

    return def;
}

// Get frameCPP definition using FrameSpec API
StructureDefinition getFrameCPPDefinition(const std::string& objectName, unsigned int version) {
    static std::map<std::string, Common::FrameSpec::Info::frame_object_types> objectMap = {
        {"FrameH", Common::FrameSpec::Info::FSI_FRAME_H},
        {"FrAdcData", Common::FrameSpec::Info::FSI_FR_ADC_DATA},
        {"FrDetector", Common::FrameSpec::Info::FSI_FR_DETECTOR},
        {"FrEndOfFile", Common::FrameSpec::Info::FSI_FR_END_OF_FILE},
        {"FrEndOfFrame", Common::FrameSpec::Info::FSI_FR_END_OF_FRAME},
        {"FrEvent", Common::FrameSpec::Info::FSI_FR_EVENT},
        {"FrTrigData", Common::FrameSpec::Info::FSI_FR_EVENT},  // FrTrigData (v3-4) maps to same enum as FrEvent
        {"FrHistory", Common::FrameSpec::Info::FSI_FR_HISTORY},
        {"FrMsg", Common::FrameSpec::Info::FSI_FR_MSG},
        {"FrProcData", Common::FrameSpec::Info::FSI_FR_PROC_DATA},
        {"FrRawData", Common::FrameSpec::Info::FSI_FR_RAW_DATA},
        {"FrSE", Common::FrameSpec::Info::FSI_FR_SE},
        {"FrSerData", Common::FrameSpec::Info::FSI_FR_SER_DATA},
        {"FrSH", Common::FrameSpec::Info::FSI_FR_SH},
        {"FrSimData", Common::FrameSpec::Info::FSI_FR_SIM_DATA},
        {"FrSimEvent", Common::FrameSpec::Info::FSI_FR_SIM_EVENT},
        {"FrStatData", Common::FrameSpec::Info::FSI_FR_STAT_DATA},
        {"FrSummary", Common::FrameSpec::Info::FSI_FR_SUMMARY},
        {"FrTable", Common::FrameSpec::Info::FSI_FR_TABLE},
        {"FrTOC", Common::FrameSpec::Info::FSI_FR_TOC},
        {"FrVect", Common::FrameSpec::Info::FSI_FR_VECT}
    };

    StructureDefinition def;

    auto it = objectMap.find(objectName);
    if (it == objectMap.end()) {
        throw std::runtime_error("Unknown object type: " + objectName);
    }

    auto objectType = it->second;

    // Get the FrameSpec info for this version
    auto* specInfo = Common::FrameSpec::SpecInfo(version);
    if (!specInfo) {
        throw std::runtime_error("No spec info for version " + std::to_string(version));
    }

    // Get the frame object template
    auto* frameObject = specInfo->FrameObject(objectType);
    if (!frameObject) {
        throw std::runtime_error("Object type '" + objectName +
                                "' not available in version " + std::to_string(version));
    }

    // Get description using public virtual method
    const Common::Description* description = frameObject->GetDescription(version);
    if (!description || description->size() == 0) {
        throw std::runtime_error("No description available for " + objectName +
                                " version " + std::to_string(version));
    }

    // Iterate through the description (FrSH + FrSE elements)
    for (const auto& element : *description) {
        // Try casting to FrSH (structure header)
        if (auto* frsh = dynamic_cast<const Universal::FrSH*>(element.get())) {
            unsigned short class_id = frsh->Universal::FrSHImpl::Data::GetClass();
            def.name = frsh->GetName();
            def.id = class_id;
            def.comment = frsh->GetComment();
        }
        // Try casting to FrSE (structure element/field)
        else if (auto* frse = dynamic_cast<const Universal::FrSE*>(element.get())) {
            FieldInfo field;
            field.name = frse->GetName();
            field.type = frse->Universal::FrSEImpl::Data::GetClass();
            field.comment = frse->GetComment();
            def.fields.push_back(field);
        }
    }

    return def;
}

#ifdef FRAMEL_FOUND
// Get FrameL definition using FrLibSetVersion() + *Def() functions
StructureDefinition getFrameLDefinition(const std::string& objectName, int version) {
    typedef FrSH* (*DefFunction)();

    static std::map<std::string, DefFunction> defFunctions = {
        {"FrameH", FrameHDef},
        {"FrAdcData", FrAdcDataDef},
        {"FrDetector", FrDetectorDef},
        {"FrEndOfFile", FrEndOfFileDef},
        {"FrEndOfFrame", FrEndOfFrameDef},
        {"FrEvent", FrEventDef},
        {"FrTrigData", FrEventDef},  // FrTrigData (v3-4) maps to same function as FrEvent
        {"FrHistory", FrHistoryDef},
        {"FrMsg", FrMsgDef},
        {"FrProcData", FrProcDataDef},
        {"FrRawData", FrRawDataDef},
        {"FrSE", FrSEDef},
        {"FrSerData", FrSerDataDef},
        {"FrSH", FrSHDef},
        {"FrSimData", FrSimDataDef},
        {"FrSimEvent", FrSimEventDef},
        {"FrStatData", FrStatDataDef},
        {"FrSummary", FrSummaryDef},
        {"FrTable", FrTableDef},
        {"FrTOC", FrTOCDef},
        {"FrVect", FrVectDef}
    };

    StructureDefinition def;

    auto it = defFunctions.find(objectName);
    if (it == defFunctions.end()) {
        throw std::runtime_error("Unknown object type: " + objectName);
    }

    // Set FrameL library version
    FrLibSetVersion(version);

    // Call the appropriate *Def() function
    FrSH* sh = it->second();
    if (!sh) {
        throw std::runtime_error("Failed to get definition for " + objectName);
    }

    // Extract FrSH information
    def.name = sh->name ? sh->name : "";
    def.id = sh->id;
    def.comment = sh->comment ? sh->comment : "";

    // Extract FrSE information (walk linked list)
    FrSE* se = sh->firstE;
    while (se) {
        FieldInfo field;
        field.name = se->name ? se->name : "";
        field.type = se->type ? se->type : "";
        field.comment = se->comment ? se->comment : "";
        def.fields.push_back(field);
        se = se->next;
    }

    return def;
}

// Helper to check if a field name exists in a field list
bool fieldExists(const std::vector<FieldInfo>& fields, const std::string& name) {
    for (const auto& field : fields) {
        if (field.name == name) return true;
    }
    return false;
}

// Compare implementation against PDF ground truth
std::vector<std::string> compareAgainstPDF(const StructureDefinition& pdfDef,
                                           const StructureDefinition& implDef,
                                           const std::string& implName,
                                           bool ignoreComments = true) {
    std::vector<std::string> differences;

    // Compare structure name
    if (pdfDef.name != implDef.name) {
        std::ostringstream oss;
        oss << "Structure name ERROR: PDF='" << pdfDef.name
            << "' vs " << implName << "='" << implDef.name << "'";
        differences.push_back(oss.str());
    }

    // NOTE: Structure ID mismatches are insignificant - they are internal
    // implementation details. PDF doesn't define IDs anyway.

    // Compare comments if not ignoring
    if (!ignoreComments && pdfDef.comment != implDef.comment) {
        std::ostringstream oss;
        oss << "Structure comment mismatch:\n"
            << "  PDF spec: \"" << pdfDef.comment << "\"\n"
            << "  " << implName << ": \"" << implDef.comment << "\"";
        differences.push_back(oss.str());
    }

    // Compare field counts
    if (pdfDef.fields.size() != implDef.fields.size()) {
        std::ostringstream oss;
        oss << "Field count ERROR: PDF=" << pdfDef.fields.size()
            << " vs " << implName << "=" << implDef.fields.size();
        differences.push_back(oss.str());
    }

    // Compare individual fields
    size_t maxFields = std::max(pdfDef.fields.size(), implDef.fields.size());
    for (size_t i = 0; i < maxFields; ++i) {
        bool pdfHasField = i < pdfDef.fields.size();
        bool implHasField = i < implDef.fields.size();

        if (!pdfHasField) {
            std::ostringstream oss;
            oss << "Field " << i << " ERROR: " << implName << " has extra field '"
                << implDef.fields[i].name << "' ("
                << implDef.fields[i].type << ") not in PDF spec";
            differences.push_back(oss.str());
        } else if (!implHasField) {
            std::ostringstream oss;
            oss << "Field " << i << " ERROR: PDF spec has field '"
                << pdfDef.fields[i].name << "' ("
                << pdfDef.fields[i].type << ") missing in " << implName;
            differences.push_back(oss.str());
        } else {
            const auto& pdfField = pdfDef.fields[i];
            const auto& implField = implDef.fields[i];

            // Compare field name
            if (pdfField.name != implField.name) {
                std::ostringstream oss;

                // Check if this is an ordering issue or truly missing field
                bool pdfFieldExistsInImpl = fieldExists(implDef.fields, pdfField.name);
                bool implFieldExistsInPdf = fieldExists(pdfDef.fields, implField.name);

                if (!pdfFieldExistsInImpl && !implFieldExistsInPdf) {
                    // Both fields are unique - likely a true name change
                    oss << "Field " << i << " name ERROR: PDF='"
                        << pdfField.name << "' (" << pdfField.type
                        << ") vs " << implName << "='" << implField.name << "' ("
                        << implField.type << ")";
                } else if (!pdfFieldExistsInImpl) {
                    // PDF field doesn't exist in implementation at all
                    oss << "Field " << i << " ERROR: PDF spec field '"
                        << pdfField.name << "' (" << pdfField.type
                        << ") missing in " << implName << ", which has '"
                        << implField.name << "' (" << implField.type << ") instead";
                } else if (!implFieldExistsInPdf) {
                    // Implementation field doesn't exist in PDF at all
                    oss << "Field " << i << " ERROR: " << implName << " has non-spec field '"
                        << implField.name << "' (" << implField.type
                        << "), PDF spec has '"
                        << pdfField.name << "' (" << pdfField.type << ") instead";
                } else {
                    // Both fields exist but in different positions - ordering issue
                    oss << "Field " << i << " ordering ERROR: PDF='"
                        << pdfField.name << "', " << implName << "='"
                        << implField.name << "' (both fields exist but at different positions)";
                }
                differences.push_back(oss.str());
            }

            // Compare field type (only if names match to avoid confusing output)
            if (pdfField.name == implField.name && pdfField.type != implField.type) {
                std::ostringstream oss;
                oss << "Field " << i << " (" << pdfField.name
                    << ") type ERROR: PDF='" << pdfField.type
                    << "' vs " << implName << "='" << implField.type << "'";
                differences.push_back(oss.str());
            }

            // Compare field comment if not ignoring (only if names match)
            if (pdfField.name == implField.name && !ignoreComments &&
                pdfField.comment != implField.comment) {
                std::ostringstream oss;
                oss << "Field " << i << " (" << pdfField.name
                    << ") comment mismatch:\n"
                    << "  PDF spec: \"" << pdfField.comment << "\"\n"
                    << "  " << implName << ": \"" << implField.comment << "\"";
                differences.push_back(oss.str());
            }
        }
    }

    return differences;
}

// Test suite for definition consistency
struct DefinitionConsistencyFixture {
    std::vector<std::string> objectTypes = {
        "FrameH",
        "FrDetector",
        "FrHistory",
        "FrRawData",
        "FrAdcData",
        "FrVect",
        "FrProcData",
        "FrSimData",
        "FrSerData",
        "FrTrigData",  // Used in versions 3-4 (renamed to FrEvent in later versions)
        "FrEvent",     // Used in versions 5+ (was FrTrigData in v3-4)
        "FrSimEvent",
        "FrStatData",
        "FrSummary",
        "FrTable",
        "FrMsg",
        "FrTOC",
        "FrEndOfFrame",
        "FrEndOfFile"
    };

    std::vector<int> versions = {3, 4, 5, 6, 7, 8, 9};

    DefinitionConsistencyFixture() {
        // Initialize frameCPP
        FrameCPP::Initialize();
    }
};

BOOST_FIXTURE_TEST_SUITE(DefinitionConsistencyTests, DefinitionConsistencyFixture)

// Test each object type in each version against PDF ground truth
BOOST_AUTO_TEST_CASE(test_all_definitions) {
    int totalTests = 0;
    int passedTests = 0;
    int failedTests = 0;

    std::cout << "\n========================================" << std::endl;
    std::cout << "Testing frameCPP Consistency Against PDF Specification" << std::endl;
    std::cout << "PDF definitions are considered GROUND TRUTH" << std::endl;
    std::cout << "(FrameL testing disabled)" << std::endl;
    std::cout << "========================================\n" << std::endl;

    for (const auto& version : versions) {
        std::cout << "Version " << version << ":" << std::endl;
        std::cout << "----------------------------------------" << std::endl;

        for (const auto& objectType : objectTypes) {
            try {
                // Get PDF definition (ground truth)
                auto pdfDef = getPDFDefinition(objectType, version);

                // Skip FrameL testing - only test frameCPP against PDF
                // // Test FrameL against PDF
                // totalTests++;
                // try {
                //     auto framelDef = getFrameLDefinition(objectType, version);
                //     auto framelDiffs = compareAgainstPDF(pdfDef, framelDef, "FrameL", true);
                //
                //     if (framelDiffs.empty()) {
                //         passedTests++;
                //         std::cout << "  ✓ FrameL:" << objectType << " (v" << version << ") - MATCHES PDF SPEC" << std::endl;
                //     } else {
                //         failedTests++;
                //         std::cout << "  ✗ FrameL:" << objectType << " (v" << version << ") - " << framelDiffs.size()
                //                   << " ERROR(s):" << std::endl;
                //         for (const auto& diff : framelDiffs) {
                //             std::cout << "      • " << diff << std::endl;
                //         }
                //         BOOST_ERROR("FrameL " + objectType + " version " + std::to_string(version) +
                //                    " does not match PDF specification");
                //     }
                // } catch (const std::exception& e) {
                //     failedTests++;
                //     std::cout << "  ⚠ FrameL:" << objectType << " (v" << version << ") - ERROR: " << e.what() << std::endl;
                //     BOOST_ERROR("Failed to get FrameL definition for " + objectType + " version " +
                //                std::to_string(version) + ": " + e.what());
                // }

                // Test frameCPP against PDF
                totalTests++;
                try {
                    auto framecppDef = getFrameCPPDefinition(objectType, version);
                    auto framecppDiffs = compareAgainstPDF(pdfDef, framecppDef, "frameCPP", true);

                    if (framecppDiffs.empty()) {
                        passedTests++;
                        std::cout << "  ✓ frameCPP:" << objectType << " (v" << version << ") - MATCHES PDF SPEC" << std::endl;
                    } else {
                        failedTests++;
                        std::cout << "  ✗ frameCPP:" << objectType << " (v" << version << ") - " << framecppDiffs.size()
                                  << " ERROR(s):" << std::endl;
                        for (const auto& diff : framecppDiffs) {
                            std::cout << "      • " << diff << std::endl;
                        }
                        BOOST_ERROR("frameCPP " + objectType + " version " + std::to_string(version) +
                                   " does not match PDF specification");
                    }
                } catch (const std::exception& e) {
                    failedTests++;
                    std::cout << "  ⚠ frameCPP:" << objectType << " (v" << version << ") - ERROR: " << e.what() << std::endl;
                    BOOST_ERROR("Failed to get frameCPP definition for " + objectType + " version " +
                               std::to_string(version) + ": " + e.what());
                }

            } catch (const std::exception& e) {
                // PDF definition not available
                std::cout << "  ⚠ " << objectType << " (v" << version << ") - No PDF definition: " << e.what() << std::endl;
                // Skip test for this object (only testing frameCPP)
                totalTests += 1;
                failedTests += 1;
            }
        }

        std::cout << std::endl;
    }

    // Print summary
    std::cout << "========================================" << std::endl;
    std::cout << "Summary:" << std::endl;
    std::cout << "  Total tests: " << totalTests << " (frameCPP only)" << std::endl;
    std::cout << "  Passed: " << passedTests << std::endl;
    std::cout << "  Failed: " << failedTests << std::endl;
    std::cout << "  PDF is the definitive ground truth" << std::endl;
    std::cout << "========================================\n" << std::endl;
}

// Test version evolution using PDF ground truth
BOOST_AUTO_TEST_CASE(test_version_evolution) {
    // Version 3 FrDetector has different fields than version 7+
    // This test verifies that frameCPP matches PDF for both versions

    std::cout << "\n========================================" << std::endl;
    std::cout << "Testing frameCPP Version Evolution Against PDF Specification" << std::endl;
    std::cout << "(FrameL testing disabled)" << std::endl;
    std::cout << "========================================\n" << std::endl;

    try {
        auto v3_pdf = getPDFDefinition("FrDetector", 3);
        // auto v3_framel = getFrameLDefinition("FrDetector", 3);
        auto v3_framecpp = getFrameCPPDefinition("FrDetector", 3);

        auto v7_pdf = getPDFDefinition("FrDetector", 7);
        // auto v7_framel = getFrameLDefinition("FrDetector", 7);
        auto v7_framecpp = getFrameCPPDefinition("FrDetector", 7);

        std::cout << "Version 3 FrDetector field count:" << std::endl;
        std::cout << "  PDF:      " << v3_pdf.fields.size() << " fields (ground truth)" << std::endl;
        // std::cout << "  FrameL:   " << v3_framel.fields.size() << " fields" << std::endl;
        std::cout << "  frameCPP: " << v3_framecpp.fields.size() << " fields" << std::endl;

        std::cout << "\nVersion 7 FrDetector field count:" << std::endl;
        std::cout << "  PDF:      " << v7_pdf.fields.size() << " fields (ground truth)" << std::endl;
        // std::cout << "  FrameL:   " << v7_framel.fields.size() << " fields" << std::endl;
        std::cout << "  frameCPP: " << v7_framecpp.fields.size() << " fields" << std::endl;

        // Verify frameCPP matches PDF specification
        // BOOST_CHECK_EQUAL(v3_pdf.fields.size(), v3_framel.fields.size());
        BOOST_CHECK_EQUAL(v3_pdf.fields.size(), v3_framecpp.fields.size());
        // BOOST_CHECK_EQUAL(v7_pdf.fields.size(), v7_framel.fields.size());
        BOOST_CHECK_EQUAL(v7_pdf.fields.size(), v7_framecpp.fields.size());

        // Verify evolution: v3 and v7 should differ
        bool evolution_detected = (v3_pdf.fields.size() != v7_pdf.fields.size());
        std::cout << "\nEvolution detected between v3 and v7: "
                  << (evolution_detected ? "YES" : "NO") << std::endl;

    } catch (const std::exception& e) {
        BOOST_ERROR("Version evolution test failed: " + std::string(e.what()));
    }

    std::cout << "========================================\n" << std::endl;
}

BOOST_AUTO_TEST_SUITE_END()

#else // !FRAMEL_FOUND

// Dummy test when FrameL is not available
BOOST_AUTO_TEST_CASE(test_framel_not_available) {
    std::cout << "\n========================================" << std::endl;
    std::cout << "FrameL library not found - skipping tests" << std::endl;
    std::cout << "========================================\n" << std::endl;
    BOOST_TEST_MESSAGE("FrameL not available - definition consistency tests skipped");
}

#endif // FRAMEL_FOUND
