#include "mitc4_reference_comparison.hpp"

#include "fesa/app/fesa_application.hpp"

#include <gtest/gtest.h>

#include <algorithm>
#include <cstddef>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>

#ifndef FESA_TEST_SOURCE_DIR
#error FESA_TEST_SOURCE_DIR must identify the repository root.
#endif

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

namespace {

constexpr const char* kInternalFormulation = "FESA-MITC4";
constexpr const char* kIntegrationRule =
    "2x2x2-gauss; mitc4-edge-midpoint-shear";
constexpr std::size_t kNodeCount = 49U;
constexpr std::size_t kComponentCount = 6U;

std::string readBytes(const std::filesystem::path& path) {
    std::ifstream stream{path, std::ios::binary};
    if (!stream) {
        throw std::runtime_error{"Unable to read declared reference artifact."};
    }
    return {std::istreambuf_iterator<char>{stream},
            std::istreambuf_iterator<char>{}};
}

struct ArtifactSnapshot {
    std::string bytes;
    std::filesystem::file_time_type lastWriteTime;
};

ArtifactSnapshot snapshot(const std::filesystem::path& path) {
    return {readBytes(path), std::filesystem::last_write_time(path)};
}

void expectUnchanged(
    const std::filesystem::path& path, const ArtifactSnapshot& before) {
    EXPECT_EQ(readBytes(path), before.bytes) << path.string();
    EXPECT_EQ(std::filesystem::last_write_time(path), before.lastWriteTime)
        << path.string();
}

struct CaseEvidence {
    fesa::test::Mitc4ComparisonReport report;
    std::filesystem::path comparisonJson;
};

CaseEvidence runCase(
    const std::string& caseId,
    const std::string& sourceElementType,
    const std::filesystem::path& referenceDirectory,
    const std::filesystem::path& input,
    const std::filesystem::path& csv,
    const std::string& outputName) {
    const auto inputBefore = snapshot(input);
    const auto csvBefore = snapshot(csv);
    const std::filesystem::path outputDirectory =
        std::filesystem::path{FESA_TEST_BINARY_DIR} / "reference" / outputName;
    std::error_code error;
    std::filesystem::remove_all(outputDirectory, error);
    error.clear();
    if (!std::filesystem::create_directories(outputDirectory, error) || error) {
        throw std::runtime_error{"Unable to create MITC4 evidence directory."};
    }
    const auto results = outputDirectory / "results.h5";
    const auto comparison = outputDirectory / "comparison.json";

    fesa::FesaApplication application;
    EXPECT_EQ(
        application.run({input.string(), "--output", results.string()}), 0);
    EXPECT_TRUE(std::filesystem::is_regular_file(results));
    auto comparisonResult = fesa::test::Mitc4ReferenceComparison::compare(
        {caseId, sourceElementType, input, csv, results});
    if (!comparisonResult.hasValue()) {
        std::string diagnostics;
        for (const auto& diagnostic :
             comparisonResult.status().diagnostics()) {
            diagnostics += "\n" + diagnostic.code + ": " + diagnostic.message;
        }
        ADD_FAILURE() << "MITC4 comparison precheck failed for " << caseId
                      << diagnostics;
        return {{}, comparison};
    }
    EXPECT_TRUE(
        fesa::test::Mitc4ReferenceComparison::writeDeterministicJson(
            comparisonResult.value(), comparison)
            .isOk());
    EXPECT_TRUE(std::filesystem::is_regular_file(comparison));

    std::vector<std::string> generated;
    for (const auto& entry :
         std::filesystem::directory_iterator{outputDirectory}) {
        generated.push_back(entry.path().filename().string());
    }
    std::sort(generated.begin(), generated.end());
    EXPECT_EQ(
        generated,
        (std::vector<std::string>{"comparison.json", "results.h5"}));
    EXPECT_TRUE(std::filesystem::is_directory(referenceDirectory));
    expectUnchanged(input, inputBefore);
    expectUnchanged(csv, csvBefore);
    return {std::move(comparisonResult.value()), comparison};
}

void expectCommonMetadata(
    const fesa::test::Mitc4ComparisonReport& report,
    const std::string& caseId,
    const std::string& sourceElementType) {
    EXPECT_EQ(report.caseId, caseId);
    EXPECT_EQ(report.sourceElementType, sourceElementType);
    EXPECT_EQ(report.internalFormulation, kInternalFormulation);
    EXPECT_EQ(report.integrationRule, kIntegrationRule);
}

void expectComparisonCoverage(
    const fesa::test::Mitc4ComparisonReport& report) {
    ASSERT_EQ(report.rows.size(), kNodeCount * kComponentCount);
    ASSERT_EQ(report.metrics.size(), kComponentCount);
    ASSERT_EQ(report.vectorMetrics.size(), kNodeCount);
    EXPECT_TRUE(report.passed);
    const std::size_t blockingRows = static_cast<std::size_t>(std::count_if(
        report.rows.begin(), report.rows.end(),
        [](const fesa::test::Mitc4RowDecision& row) {
            return row.blocking;
        }));
    const std::size_t rotationRows = report.rows.size() - blockingRows;
    EXPECT_EQ(blockingRows, kNodeCount * 3U);
    EXPECT_EQ(rotationRows, kNodeCount * 3U);
    EXPECT_TRUE(std::all_of(
        report.rows.begin(), report.rows.end(),
        [](const fesa::test::Mitc4RowDecision& row) {
            return !row.blocking || row.withinTolerance;
        }));
    EXPECT_EQ(
        report.warnings.size(),
        static_cast<std::size_t>(std::count_if(
            report.rows.begin(), report.rows.end(),
            [](const fesa::test::Mitc4RowDecision& row) {
                return !row.blocking && !row.withinTolerance;
            })));
}

// MITC4-E2E-S4-001
TEST(Mitc4S4Reference, PreservesS4AndWritesCommonMitc4Metadata) {
    const std::filesystem::path root{FESA_TEST_SOURCE_DIR};
    const auto directory = root / "reference" / "shell";
    const auto evidence = runCase(
        "shell-s4", "S4", directory, directory / "shell.inp",
        directory / "shell displacements.csv", "mitc4-shell-s4-metadata");
    expectCommonMetadata(evidence.report, "shell-s4", "S4");
}

// MITC4-E2E-S4-002
TEST(Mitc4S4Reference, PassesBlockingUAndReportsEveryUrRow) {
    const std::filesystem::path root{FESA_TEST_SOURCE_DIR};
    const auto directory = root / "reference" / "shell";
    const auto evidence = runCase(
        "shell-s4", "S4", directory, directory / "shell.inp",
        directory / "shell displacements.csv", "mitc4-shell-s4-comparison");
    expectCommonMetadata(evidence.report, "shell-s4", "S4");
    expectComparisonCoverage(evidence.report);
}

} // namespace
