//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      Tests/Unit/Sim/Dense2DValidationTest.cpp
//! @brief     Tests Dense2D validation during scattering simulations.
//!
//! @homepage  http://www.bornagainproject.org
//! @license   GNU General Public License v3 or higher (see COPYING)
//! @copyright Forschungszentrum Jülich GmbH 2026
//! @authors   Scientific Computing Group at MLZ (see CITATION, AUTHORS)
//
//  ************************************************************************************************

#include "Device/Beam/Beam.h"
#include "Device/Data/Datafield.h"
#include "Device/Detector/SphericalDetector.h"
#include "Sample/Aggregate/Dense2D.h"
#include "Sample/HardParticle/Cylinder.h"
#include "Sample/Material/RefractiveMaterial.h"
#include "Sample/Material/Vacuum.h"
#include "Sample/Multilayer/Layer.h"
#include "Sample/Particle/Particle.h"
#include "Sample/Sample/Sample.h"
#include "Sim/Simulation/ScatteringSimulation.h"
#include "Tests/GTestWrapper/google_test.h"
#include <cmath>
#include <memory>
#include <numbers>
#include <string>

namespace {

constexpr double py_fluid_limit_packing = 0.65; // Source is documented in Dense2D::validate().

//! Converts a dimensionless hard-disk packing fraction eta to BornAgain number density.
double densityForPackingFraction(double radius, double packing)
{
    return packing / (std::numbers::pi * radius * radius);
}

//! Builds a minimal Dense2D sample for scattering-validation checks.
std::unique_ptr<Sample> dense2DSample(double density)
{
    const RefractiveMaterial particle_material("particle", MaterialColor(0.8, 0.2, 0.2), 6e-4,
                                               2e-8);
    const RefractiveMaterial substrate_material("substrate", MaterialColor(0.2, 0.4, 0.8), 6e-6,
                                                2e-8);
    const Particle particle(particle_material, Cylinder(5.0, 5.0));
    const Dense2D structure(density, particle);

    Layer top{Vacuum()};
    top.deposit2D(structure);
    Layer bottom(substrate_material);

    auto sample = std::make_unique<Sample>();
    sample->addLayer(top);
    sample->addLayer(bottom);
    return sample;
}

//! Creates a tiny detector simulation so validation runs before any expensive work.
std::unique_ptr<ScatteringSimulation> dense2DSimulation(const Sample& sample)
{
    const Beam beam(1.0, 0.1, 0.2);
    const SphericalDetector detector(2, -0.02, 0.02, 2, 0.0, 0.02);
    return std::make_unique<ScatteringSimulation>(beam, sample, detector);
}

} // namespace

TEST(Dense2DValidation, ScatteringRejectsPackingAbovePercusYevickFluidLimit)
{
    // Calculation must reject densities where the PY fluid model can yield unphysical S(q).
    const auto sample =
        dense2DSample(densityForPackingFraction(5.0, 1.01 * py_fluid_limit_packing));
    const auto simulation = dense2DSimulation(*sample);

    try {
        simulation->simulate();
        FAIL() << "Expected Dense2D validation to reject packing above the PY fluid limit";
    } catch (const std::runtime_error& e) {
        const std::string message = e.what();
        EXPECT_NE(message.find("Dense2D hard-disk packing fraction"), std::string::npos);
        EXPECT_NE(message.find("Percus-Yevick fluid-model limit"), std::string::npos);
    }
}

TEST(Dense2DValidation, ScatteringAllowsPackingBelowPercusYevickFluidLimit)
{
    // A high but still supported fluid density should pass validation.
    const auto sample =
        dense2DSample(densityForPackingFraction(5.0, 0.99 * py_fluid_limit_packing));
    const auto simulation = dense2DSimulation(*sample);

    EXPECT_NO_THROW(simulation->simulate());
}
