#include "mitc4_reference_comparison.hpp"

#include <hdf5.h>

#include <gtest/gtest.h>

#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <iterator>
#include <limits>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>

#ifndef FESA_TEST_BINARY_DIR
#error FESA_TEST_BINARY_DIR must identify the CMake binary root.
#endif

namespace {

constexpr const char* kInstanceName = "PART-1-1";
constexpr const char* kInternalFormulation = "FESA-MITC4";
constexpr const char* kIntegrationRule =
    "2x2x2-gauss; mitc4-edge-midpoint-shear";
constexpr const char* kDisplacementPath =
    "/steps/Step-1/frames/0/nodal/displacement";
constexpr std::array<const char*, 6> kComponents{
    "U1", "U2", "U3", "UR1", "UR2", "UR3"};

class Hdf5Handle {
public:
    using Closer = herr_t (*)(hid_t);

    Hdf5Handle(const hid_t value, Closer closer)
        : value_{value}, closer_{closer} {}
    Hdf5Handle(const Hdf5Handle&) = delete;
    Hdf5Handle& operator=(const Hdf5Handle&) = delete;
    Hdf5Handle(Hdf5Handle&& other) noexcept
        : value_{other.value_}, closer_{other.closer_} {
        other.value_ = -1;
        other.closer_ = nullptr;
    }
    ~Hdf5Handle() {
        if (value_ >= 0 && closer_ != nullptr) {
            (void)closer_(value_);
        }
    }

    hid_t get() const noexcept { return value_; }

private:
    hid_t value_;
    Closer closer_;
};

hid_t requireId(const hid_t value, const char* message) {
    if (value < 0) {
        throw std::runtime_error{message};
    }
    return value;
}

void requireHdf(const herr_t value, const char* message) {
    if (value < 0) {
        throw std::runtime_error{message};
    }
}

Hdf5Handle makeUtf8StringType() {
    Hdf5Handle type{
        requireId(H5Tcopy(H5T_C_S1), "Unable to copy string type."), H5Tclose};
    requireHdf(
        H5Tset_size(type.get(), H5T_VARIABLE),
        "Unable to create variable string type.");
    requireHdf(
        H5Tset_cset(type.get(), H5T_CSET_UTF8),
        "Unable to create UTF-8 string type.");
    return type;
}

void writeStringAttribute(
    const hid_t object, const char* name, const std::string& value) {
    auto type = makeUtf8StringType();
    Hdf5Handle space{
        requireId(H5Screate(H5S_SCALAR), "Unable to create attribute space."),
        H5Sclose};
    Hdf5Handle attribute{
        requireId(
            H5Acreate2(
                object, name, type.get(), space.get(), H5P_DEFAULT,
                H5P_DEFAULT),
            "Unable to create string attribute."),
        H5Aclose};
    const char* raw = value.c_str();
    requireHdf(
        H5Awrite(attribute.get(), type.get(), &raw),
        "Unable to write string attribute.");
}

void writeUint64Attribute(
    const hid_t object, const char* name, const std::uint64_t value) {
    Hdf5Handle space{
        requireId(H5Screate(H5S_SCALAR), "Unable to create attribute space."),
        H5Sclose};
    Hdf5Handle attribute{
        requireId(
            H5Acreate2(
                object, name, H5T_STD_U64LE, space.get(), H5P_DEFAULT,
                H5P_DEFAULT),
            "Unable to create integer attribute."),
        H5Aclose};
    requireHdf(
        H5Awrite(attribute.get(), H5T_NATIVE_UINT64, &value),
        "Unable to write integer attribute.");
}

Hdf5Handle createGroup(const hid_t parent, const char* path) {
    return {
        requireId(
            H5Gcreate2(
                parent, path, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT),
            "Unable to create HDF5 group."),
        H5Gclose};
}

struct ComparisonValueRow {
    std::string instanceName{kInstanceName};
    std::int64_t sourceNodeLabel{};
    std::array<double, 6> values{};
};

std::vector<ComparisonValueRow> defaultRows() {
    return {
        {kInstanceName, 1, {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}},
        {kInstanceName, 2, {2.0, -4.0, 8.0, 0.1, -0.2, 0.3}}};
}

struct Hdf5Options {
    std::string sourceElementType{"S4"};
    std::string internalFormulation{kInternalFormulation};
    std::string integrationRule{kIntegrationRule};
    std::string displacementComponents{"UX,UY,UZ,URX,URY,URZ"};
};

struct NodeWriteRow {
    std::uint64_t internalNodeId;
    const char* instanceName;
    const char* sourceLabel;
    double coordinates[3];
};

struct ElementWriteRow {
    std::uint64_t internalElementId;
    const char* instanceName;
    const char* sourceLabel;
    const char* sourceElementType;
    const char* internalFormulation;
    std::uint64_t nodeInternalIds[4];
    std::uint64_t shellSectionInternalId;
    std::uint64_t materialInternalId;
};

void writeNodes(
    const hid_t file, const std::vector<ComparisonValueRow>& values) {
    std::vector<std::string> labels;
    labels.reserve(values.size());
    for (const auto& row : values) {
        labels.push_back(std::to_string(row.sourceNodeLabel));
    }
    std::vector<NodeWriteRow> rows;
    rows.reserve(values.size());
    for (std::size_t index = 0U; index < values.size(); ++index) {
        rows.push_back({
            static_cast<std::uint64_t>(index),
            values[index].instanceName.c_str(),
            labels[index].c_str(),
            {static_cast<double>(index), 0.0, 0.0}});
    }

    auto stringType = makeUtf8StringType();
    const hsize_t coordinateDimensions[] = {3U};
    Hdf5Handle fileCoordinates{
        requireId(
            H5Tarray_create2(H5T_IEEE_F64LE, 1, coordinateDimensions),
            "Unable to create coordinate file type."),
        H5Tclose};
    Hdf5Handle memoryCoordinates{
        requireId(
            H5Tarray_create2(H5T_NATIVE_DOUBLE, 1, coordinateDimensions),
            "Unable to create coordinate memory type."),
        H5Tclose};
    Hdf5Handle fileType{
        requireId(
            H5Tcreate(H5T_COMPOUND, sizeof(NodeWriteRow)),
            "Unable to create node file type."),
        H5Tclose};
    Hdf5Handle memoryType{
        requireId(
            H5Tcreate(H5T_COMPOUND, sizeof(NodeWriteRow)),
            "Unable to create node memory type."),
        H5Tclose};
    const auto insert = [&](const hid_t type,
                            const hid_t integerType,
                            const hid_t coordinateType) {
        requireHdf(
            H5Tinsert(
                type, "internal_node_id",
                HOFFSET(NodeWriteRow, internalNodeId), integerType),
            "Unable to define node ID.");
        requireHdf(
            H5Tinsert(
                type, "instance_name", HOFFSET(NodeWriteRow, instanceName),
                stringType.get()),
            "Unable to define node instance.");
        requireHdf(
            H5Tinsert(
                type, "source_label", HOFFSET(NodeWriteRow, sourceLabel),
                stringType.get()),
            "Unable to define node label.");
        requireHdf(
            H5Tinsert(
                type, "coordinates", HOFFSET(NodeWriteRow, coordinates),
                coordinateType),
            "Unable to define node coordinates.");
    };
    insert(fileType.get(), H5T_STD_U64LE, fileCoordinates.get());
    insert(memoryType.get(), H5T_NATIVE_UINT64, memoryCoordinates.get());
    const hsize_t dimensions[] = {static_cast<hsize_t>(rows.size())};
    Hdf5Handle space{
        requireId(
            H5Screate_simple(1, dimensions, nullptr),
            "Unable to create node space."),
        H5Sclose};
    Hdf5Handle dataset{
        requireId(
            H5Dcreate2(
                file, "/model/nodes", fileType.get(), space.get(),
                H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT),
            "Unable to create node dataset."),
        H5Dclose};
    requireHdf(
        H5Dwrite(
            dataset.get(), memoryType.get(), H5S_ALL, H5S_ALL, H5P_DEFAULT,
            rows.data()),
        "Unable to write node dataset.");
    writeStringAttribute(dataset.get(), "coordinate_system", "global-cartesian");
    writeStringAttribute(dataset.get(), "units_label", "length");
}

void writeElements(const hid_t file, const Hdf5Options& options) {
    ElementWriteRow row{
        0U,
        kInstanceName,
        "1",
        options.sourceElementType.c_str(),
        options.internalFormulation.c_str(),
        {0U, 1U, 0U, 1U},
        0U,
        0U};
    auto stringType = makeUtf8StringType();
    const hsize_t nodeDimensions[] = {4U};
    Hdf5Handle fileNodes{
        requireId(
            H5Tarray_create2(H5T_STD_U64LE, 1, nodeDimensions),
            "Unable to create element node file type."),
        H5Tclose};
    Hdf5Handle memoryNodes{
        requireId(
            H5Tarray_create2(H5T_NATIVE_UINT64, 1, nodeDimensions),
            "Unable to create element node memory type."),
        H5Tclose};
    Hdf5Handle fileType{
        requireId(
            H5Tcreate(H5T_COMPOUND, sizeof(ElementWriteRow)),
            "Unable to create element file type."),
        H5Tclose};
    Hdf5Handle memoryType{
        requireId(
            H5Tcreate(H5T_COMPOUND, sizeof(ElementWriteRow)),
            "Unable to create element memory type."),
        H5Tclose};
    const auto insert = [&](const hid_t type,
                            const hid_t integerType,
                            const hid_t nodesType) {
        requireHdf(
            H5Tinsert(
                type, "internal_element_id",
                HOFFSET(ElementWriteRow, internalElementId), integerType),
            "Unable to define element ID.");
        requireHdf(
            H5Tinsert(
                type, "instance_name", HOFFSET(ElementWriteRow, instanceName),
                stringType.get()),
            "Unable to define element instance.");
        requireHdf(
            H5Tinsert(
                type, "source_label", HOFFSET(ElementWriteRow, sourceLabel),
                stringType.get()),
            "Unable to define element label.");
        requireHdf(
            H5Tinsert(
                type, "source_element_type",
                HOFFSET(ElementWriteRow, sourceElementType), stringType.get()),
            "Unable to define source element type.");
        requireHdf(
            H5Tinsert(
                type, "internal_formulation",
                HOFFSET(ElementWriteRow, internalFormulation), stringType.get()),
            "Unable to define internal formulation.");
        requireHdf(
            H5Tinsert(
                type, "node_internal_ids",
                HOFFSET(ElementWriteRow, nodeInternalIds), nodesType),
            "Unable to define element connectivity.");
        requireHdf(
            H5Tinsert(
                type, "shell_section_internal_id",
                HOFFSET(ElementWriteRow, shellSectionInternalId), integerType),
            "Unable to define section ID.");
        requireHdf(
            H5Tinsert(
                type, "material_internal_id",
                HOFFSET(ElementWriteRow, materialInternalId), integerType),
            "Unable to define material ID.");
    };
    insert(fileType.get(), H5T_STD_U64LE, fileNodes.get());
    insert(memoryType.get(), H5T_NATIVE_UINT64, memoryNodes.get());
    const hsize_t dimensions[] = {1U};
    Hdf5Handle space{
        requireId(
            H5Screate_simple(1, dimensions, nullptr),
            "Unable to create element space."),
        H5Sclose};
    Hdf5Handle dataset{
        requireId(
            H5Dcreate2(
                file, "/model/elements", fileType.get(), space.get(),
                H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT),
            "Unable to create element dataset."),
        H5Dclose};
    requireHdf(
        H5Dwrite(
            dataset.get(), memoryType.get(), H5S_ALL, H5S_ALL, H5P_DEFAULT,
            &row),
        "Unable to write element dataset.");
    writeStringAttribute(dataset.get(), "formulation", kInternalFormulation);
}

void writeDisplacement(
    const hid_t file,
    const std::vector<ComparisonValueRow>& values,
    const Hdf5Options& options) {
    std::vector<double> flattened;
    flattened.reserve(values.size() * kComponents.size());
    for (const auto& row : values) {
        flattened.insert(flattened.end(), row.values.begin(), row.values.end());
    }
    const hsize_t dimensions[] = {
        static_cast<hsize_t>(values.size()),
        static_cast<hsize_t>(kComponents.size())};
    Hdf5Handle space{
        requireId(
            H5Screate_simple(2, dimensions, nullptr),
            "Unable to create displacement space."),
        H5Sclose};
    Hdf5Handle dataset{
        requireId(
            H5Dcreate2(
                file, kDisplacementPath, H5T_IEEE_F64LE, space.get(),
                H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT),
            "Unable to create displacement dataset."),
        H5Dclose};
    requireHdf(
        H5Dwrite(
            dataset.get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL,
            H5P_DEFAULT, flattened.data()),
        "Unable to write displacement dataset.");
    writeStringAttribute(
        dataset.get(), "component_names", options.displacementComponents);
    writeStringAttribute(
        dataset.get(), "component_unit_dimensions",
        "length,length,length,radian,radian,radian");
    writeStringAttribute(dataset.get(), "coordinate_system", "global-cartesian");
    writeStringAttribute(dataset.get(), "location", "nodal");
    writeStringAttribute(dataset.get(), "step_name", "Step-1");
    writeUint64Attribute(dataset.get(), "frame_index", 0U);
}

void writeHdf5(
    const std::filesystem::path& path,
    const std::filesystem::path& inputPath,
    const std::vector<ComparisonValueRow>& values,
    const Hdf5Options& options = {}) {
    Hdf5Handle file{
        requireId(
            H5Fcreate(
                path.string().c_str(), H5F_ACC_TRUNC, H5P_DEFAULT, H5P_DEFAULT),
            "Unable to create comparison HDF5 fixture."),
        H5Fclose};
    auto metadata = createGroup(file.get(), "/metadata");
    auto model = createGroup(file.get(), "/model");
    auto steps = createGroup(file.get(), "/steps");
    auto step = createGroup(file.get(), "/steps/Step-1");
    auto frames = createGroup(file.get(), "/steps/Step-1/frames");
    auto frame = createGroup(file.get(), "/steps/Step-1/frames/0");
    auto nodal = createGroup(file.get(), "/steps/Step-1/frames/0/nodal");
    (void)model;
    (void)steps;
    (void)step;
    (void)frames;
    (void)frame;
    (void)nodal;

    writeUint64Attribute(metadata.get(), "schema_version", 0U);
    writeStringAttribute(metadata.get(), "feature_id", "linear-static-mitc4-shell");
    writeStringAttribute(
        metadata.get(), "source_input_identity",
        "path=" + std::filesystem::absolute(inputPath)
                      .lexically_normal()
                      .generic_u8string() +
            ";content_identity=test");
    writeStringAttribute(
        metadata.get(), "internal_formulation", options.internalFormulation);
    writeStringAttribute(
        metadata.get(), "integration_rule", options.integrationRule);
    writeStringAttribute(metadata.get(), "step_name", "Step-1");
    writeUint64Attribute(metadata.get(), "frame_index", 0U);
    writeNodes(file.get(), values);
    writeElements(file.get(), options);
    writeDisplacement(file.get(), values, options);
}

void writeCsv(
    const std::filesystem::path& path,
    const std::vector<ComparisonValueRow>& rows,
    const std::string& header =
        "Part Instance Name,Node Label,U-U1,U-U2,U-U3,UR-UR1,UR-UR2,UR-UR3") {
    std::ofstream stream{path, std::ios::binary | std::ios::trunc};
    if (!stream) {
        throw std::runtime_error{"Unable to create comparison CSV fixture."};
    }
    stream << header << '\n' << std::setprecision(17);
    for (const auto& row : rows) {
        stream << row.instanceName << ',' << row.sourceNodeLabel;
        for (const double value : row.values) {
            stream << ',' << value;
        }
        stream << '\n';
    }
}

class ContractFixture {
public:
    explicit ContractFixture(const std::string& name)
        : root_{std::filesystem::path{FESA_TEST_BINARY_DIR} /
                "reference" / ("mitc4-comparator-" + name)},
          input_{root_ / "case.inp"},
          csv_{root_ / "displacements.csv"},
          results_{root_ / "results.h5"} {
        std::error_code error;
        std::filesystem::remove_all(root_, error);
        error.clear();
        if (!std::filesystem::create_directories(root_, error) || error) {
            throw std::runtime_error{"Unable to create comparison fixture directory."};
        }
        std::ofstream inputStream{input_, std::ios::binary | std::ios::trunc};
        inputStream << "*Element, type=S4\n1,1,2,1,2\n";
        inputStream.close();
        writeCsv(csv_, defaultRows());
        writeHdf5(results_, input_, defaultRows());
    }

    ~ContractFixture() {
        std::error_code error;
        std::filesystem::remove_all(root_, error);
    }

    fesa::test::Mitc4ReferenceCase referenceCase() const {
        return {"shell-contract", "S4", input_, csv_, results_};
    }

    const std::filesystem::path& root() const noexcept { return root_; }
    const std::filesystem::path& input() const noexcept { return input_; }
    const std::filesystem::path& csv() const noexcept { return csv_; }
    const std::filesystem::path& results() const noexcept { return results_; }

private:
    std::filesystem::path root_;
    std::filesystem::path input_;
    std::filesystem::path csv_;
    std::filesystem::path results_;
};

void expectFailureCode(
    const fesa::Result<fesa::test::Mitc4ComparisonReport>& result,
    const std::string& code) {
    ASSERT_FALSE(result.hasValue());
    ASSERT_FALSE(result.status().diagnostics().empty());
    EXPECT_EQ(result.status().diagnostics().front().code, code);
}

const fesa::test::Mitc4RowDecision* findRow(
    const fesa::test::Mitc4ComparisonReport& report,
    const std::int64_t label,
    const std::string& component) {
    const auto found = std::find_if(
        report.rows.begin(), report.rows.end(),
        [&](const fesa::test::Mitc4RowDecision& row) {
            return row.sourceNodeLabel == label && row.component == component;
        });
    return found == report.rows.end() ? nullptr : &*found;
}

const fesa::test::Mitc4ComponentMetrics* findMetric(
    const fesa::test::Mitc4ComparisonReport& report,
    const std::string& component) {
    const auto found = std::find_if(
        report.metrics.begin(), report.metrics.end(),
        [&](const fesa::test::Mitc4ComponentMetrics& metric) {
            return metric.component == component;
        });
    return found == report.metrics.end() ? nullptr : &*found;
}

std::string readBytes(const std::filesystem::path& path) {
    std::ifstream stream{path, std::ios::binary};
    return {std::istreambuf_iterator<char>{stream},
            std::istreambuf_iterator<char>{}};
}

// MITC4-REF-001
TEST(Mitc4ReferenceComparison, MapsTrimmedHeaderAndSixComponentsBySourceIdentity) {
    ContractFixture fixture{"mapping"};
    auto csvRows = defaultRows();
    std::reverse(csvRows.begin(), csvRows.end());
    for (auto& row : csvRows) {
        row.instanceName = "part-1-1";
    }
    writeCsv(
        fixture.csv(), csvRows,
        " Part Instance Name , Node Label , U-U1 , U-U2 , U-U3 , "
        "UR-UR1 , UR-UR2 , UR-UR3 ");

    auto result = fesa::test::Mitc4ReferenceComparison::compare(
        fixture.referenceCase());
    ASSERT_TRUE(result.hasValue());
    const auto& report = result.value();
    ASSERT_TRUE(report.passed);
    ASSERT_EQ(report.rows.size(), 12U);
    EXPECT_EQ(report.rows[0U].sourceNodeLabel, 1);
    EXPECT_EQ(report.rows[0U].component, "U1");
    EXPECT_EQ(report.rows[5U].component, "UR3");
    EXPECT_EQ(report.rows[6U].sourceNodeLabel, 2);
    EXPECT_TRUE(std::all_of(
        report.rows.begin(), report.rows.end(),
        [](const fesa::test::Mitc4RowDecision& row) {
            return row.caseId == "shell-contract" &&
                   row.instanceName == kInstanceName && row.withinTolerance;
        }));
}

// MITC4-REF-002
TEST(Mitc4ReferenceComparison, RejectsInvalidInventoryBeforeNumericComparison) {
    {
        ContractFixture fixture{"missing-input"};
        ASSERT_TRUE(std::filesystem::remove(fixture.input()));
        expectFailureCode(
            fesa::test::Mitc4ReferenceComparison::compare(
                fixture.referenceCase()),
            "needs-reference-artifacts");
    }
    {
        ContractFixture fixture{"header"};
        writeCsv(
            fixture.csv(), defaultRows(),
            "Part Instance Name,Node Label,U1,U-U2,U-U3,UR-UR1,UR-UR2,UR-UR3");
        expectFailureCode(
            fesa::test::Mitc4ReferenceComparison::compare(
                fixture.referenceCase()),
            "schema-mismatch");
    }
    {
        ContractFixture fixture{"missing-row"};
        auto rows = defaultRows();
        rows.pop_back();
        writeCsv(fixture.csv(), rows);
        expectFailureCode(
            fesa::test::Mitc4ReferenceComparison::compare(
                fixture.referenceCase()),
            "schema-mismatch");
    }
    {
        ContractFixture fixture{"extra-row"};
        auto rows = defaultRows();
        rows.push_back({kInstanceName, 3, {}});
        writeCsv(fixture.csv(), rows);
        expectFailureCode(
            fesa::test::Mitc4ReferenceComparison::compare(
                fixture.referenceCase()),
            "schema-mismatch");
    }
    {
        ContractFixture fixture{"duplicate-row"};
        auto rows = defaultRows();
        rows.push_back(rows.front());
        writeCsv(fixture.csv(), rows);
        expectFailureCode(
            fesa::test::Mitc4ReferenceComparison::compare(
                fixture.referenceCase()),
            "schema-mismatch");
    }
    {
        ContractFixture fixture{"nonfinite-csv"};
        auto rows = defaultRows();
        rows[0U].values[0U] = std::numeric_limits<double>::quiet_NaN();
        writeCsv(fixture.csv(), rows);
        expectFailureCode(
            fesa::test::Mitc4ReferenceComparison::compare(
                fixture.referenceCase()),
            "schema-mismatch");
    }
    {
        ContractFixture fixture{"nonfinite-hdf5"};
        auto rows = defaultRows();
        rows[0U].values[0U] = std::numeric_limits<double>::infinity();
        writeHdf5(fixture.results(), fixture.input(), rows);
        expectFailureCode(
            fesa::test::Mitc4ReferenceComparison::compare(
                fixture.referenceCase()),
            "schema-mismatch");
    }
    {
        ContractFixture fixture{"source-identity"};
        auto rows = defaultRows();
        rows[0U].instanceName = "WRONG-INSTANCE";
        writeCsv(fixture.csv(), rows);
        expectFailureCode(
            fesa::test::Mitc4ReferenceComparison::compare(
                fixture.referenceCase()),
            "schema-mismatch");
    }
    {
        ContractFixture fixture{"hdf5-schema"};
        Hdf5Options options;
        options.displacementComponents = "U1,U2,U3,UR1,UR2,UR3";
        writeHdf5(fixture.results(), fixture.input(), defaultRows(), options);
        expectFailureCode(
            fesa::test::Mitc4ReferenceComparison::compare(
                fixture.referenceCase()),
            "schema-mismatch");
    }
}

// MITC4-REF-003
TEST(Mitc4ReferenceComparison, AppliesFixedAbsoluteToleranceWithoutScaleClampOrRowDenominator) {
    ContractFixture fixture{"tolerance"};
    auto reference = defaultRows();
    reference[0U].values[0U] = 0.0;
    reference[1U].values[0U] = 2.0;
    reference[0U].values[1U] = 0.0;
    reference[1U].values[1U] = 0.0;
    writeCsv(fixture.csv(), reference);
    auto fesaValues = reference;
    fesaValues[0U].values[0U] = 0.9999e-5;
    fesaValues[1U].values[0U] += 1.0001e-5;
    fesaValues[0U].values[1U] = 0.9999e-5;
    fesaValues[1U].values[1U] = 1.0001e-5;
    writeHdf5(fixture.results(), fixture.input(), fesaValues);

    auto result = fesa::test::Mitc4ReferenceComparison::compare(
        fixture.referenceCase());
    ASSERT_TRUE(result.hasValue());
    const auto& report = result.value();
    EXPECT_FALSE(report.passed);
    const auto* u1Zero = findRow(report, 1, "U1");
    const auto* u1Scaled = findRow(report, 2, "U1");
    const auto* u2Near = findRow(report, 1, "U2");
    const auto* u2Over = findRow(report, 2, "U2");
    ASSERT_NE(u1Zero, nullptr);
    ASSERT_NE(u1Scaled, nullptr);
    ASSERT_NE(u2Near, nullptr);
    ASSERT_NE(u2Over, nullptr);
    EXPECT_DOUBLE_EQ(u1Zero->tolerance, 1.0e-5);
    EXPECT_TRUE(u1Zero->withinTolerance);
    EXPECT_NEAR(u1Zero->normalizedError, 0.9999, 1.0e-12);
    EXPECT_FALSE(u1Scaled->withinTolerance);
    EXPECT_NEAR(u1Scaled->normalizedError, 1.0001, 2.0e-11);
    EXPECT_DOUBLE_EQ(u2Near->tolerance, 1.0e-5);
    EXPECT_TRUE(u2Near->withinTolerance);
    EXPECT_NEAR(u2Near->normalizedError, 0.9999, 1.0e-12);
    EXPECT_FALSE(u2Over->withinTolerance);
    EXPECT_NEAR(u2Over->normalizedError, 1.0001, 1.0e-12);
    for (const auto& row : report.rows) {
        EXPECT_DOUBLE_EQ(row.tolerance, 1.0e-5);
    }
    for (const auto& metric : report.metrics) {
        EXPECT_DOUBLE_EQ(metric.tolerance, 1.0e-5);
    }
    const auto* u1Metric = findMetric(report, "U1");
    ASSERT_NE(u1Metric, nullptr);
    EXPECT_DOUBLE_EQ(u1Metric->referenceScale, 2.0);
}

// MITC4-REF-004
TEST(Mitc4ReferenceComparison, RotationExceedanceWarnsWithoutBlockingTranslationVerdict) {
    ContractFixture fixture{"warning"};
    auto fesaValues = defaultRows();
    fesaValues[0U].values[3U] = 1.0;
    writeHdf5(fixture.results(), fixture.input(), fesaValues);

    auto result = fesa::test::Mitc4ReferenceComparison::compare(
        fixture.referenceCase());
    ASSERT_TRUE(result.hasValue());
    const auto& report = result.value();
    EXPECT_TRUE(report.passed);
    ASSERT_EQ(report.warnings.size(), 1U);
    EXPECT_EQ(report.warnings[0U].code, "rotation-reference-exceedance");
    const auto* row = findRow(report, 1, "UR1");
    ASSERT_NE(row, nullptr);
    EXPECT_FALSE(row->blocking);
    EXPECT_FALSE(row->withinTolerance);
    EXPECT_EQ(report.warnings[0U].row,
              static_cast<std::size_t>(row - report.rows.data()));
    EXPECT_NE(report.warnings[0U].message.find("shell-contract"),
              std::string::npos);
    EXPECT_NE(report.warnings[0U].message.find("UR1"), std::string::npos);
}

// MITC4-REF-005
TEST(Mitc4ReferenceComparison, ReportsMetricsVectorsWorstRowAndJsonDeterministically) {
    ContractFixture fixture{"report"};
    auto fesaValues = defaultRows();
    fesaValues[0U].values[0U] = 0.5e-9;
    fesaValues[0U].values[1U] = -0.25e-9;
    fesaValues[0U].values[3U] = 0.75e-9;
    writeHdf5(fixture.results(), fixture.input(), fesaValues);

    auto result = fesa::test::Mitc4ReferenceComparison::compare(
        fixture.referenceCase());
    ASSERT_TRUE(result.hasValue());
    const auto& report = result.value();
    ASSERT_TRUE(report.passed);
    ASSERT_EQ(report.metrics.size(), 6U);
    ASSERT_EQ(report.vectorMetrics.size(), 2U);
    EXPECT_NEAR(
        report.vectorMetrics[0U].displacementNormError,
        std::sqrt(0.3125) * 1.0e-9,
        1.0e-21);
    EXPECT_DOUBLE_EQ(
        report.vectorMetrics[0U].rotationNormError, 0.75e-9);
    ASSERT_LT(report.worstRow, report.rows.size());
    EXPECT_EQ(report.rows[report.worstRow].component, "UR1");
    for (const auto& metric : report.metrics) {
        EXPECT_TRUE(std::isfinite(metric.referenceScale));
        EXPECT_TRUE(std::isfinite(metric.maximumAbsoluteError));
        EXPECT_TRUE(std::isfinite(metric.maximumNormalizedError));
        EXPECT_TRUE(std::isfinite(metric.rmsError));
        EXPECT_TRUE(std::isfinite(metric.vectorNormError));
        EXPECT_LT(metric.worstRow, report.rows.size());
    }

    const auto jsonA = fixture.root() / "comparison-a.json";
    const auto jsonB = fixture.root() / "comparison-b.json";
    ASSERT_TRUE(
        fesa::test::Mitc4ReferenceComparison::writeDeterministicJson(
            report, jsonA)
            .isOk());
    ASSERT_TRUE(
        fesa::test::Mitc4ReferenceComparison::writeDeterministicJson(
            report, jsonB)
            .isOk());
    const std::string first = readBytes(jsonA);
    EXPECT_EQ(first, readBytes(jsonB));
    for (const char* key : {
             "\"rows\"", "\"metrics\"", "\"vector_metrics\"",
             "\"warnings\"", "\"worst_row\"", "\"passed\""}) {
        EXPECT_NE(first.find(key), std::string::npos) << key;
    }
}

// MITC4-REF-006
TEST(Mitc4ReferenceComparison, RequiresOnlyDeclaredInputCsvAndHdf5) {
    ContractFixture fixture{"minimal-artifacts"};
    std::vector<std::string> names;
    for (const auto& entry : std::filesystem::directory_iterator{fixture.root()}) {
        names.push_back(entry.path().filename().string());
    }
    std::sort(names.begin(), names.end());
    EXPECT_EQ(
        names,
        (std::vector<std::string>{"case.inp", "displacements.csv", "results.h5"}));
    auto result = fesa::test::Mitc4ReferenceComparison::compare(
        fixture.referenceCase());
    ASSERT_TRUE(result.hasValue());
    EXPECT_TRUE(result.value().passed);
}

} // namespace
