//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      Tests/Unit/Resample/SubdivideLayersTest.cpp
//! @brief     Tests layer subdivision around particles placed at 2D interfaces.
//!
//! @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 "Base/Const/Units.h"
#include "Resample/Sample/ReSample.h"
#include "Sample/Aggregate/Dilute2D.h"
#include "Sample/HardParticle/HardParticles.h"
#include "Sample/Material/RefractiveMaterial.h"
#include "Sample/Material/Vacuum.h"
#include "Sample/Multilayer/Layer.h"
#include "Sample/Option/SimulationOptions.h"
#include "Sample/Particle/Particle.h"
#include "Sample/Particle/Rotations.h"
#include "Sample/Sample/Sample.h"
#include "Tests/GTestWrapper/google_test.h"
#include <cmath>

namespace {

constexpr double eps = 1e-12;

RefractiveMaterial particleMaterial()
{
    return {"Particle", MaterialColor(0.5, 0.5, 0.5), 1e-5, 0.0};
}

ReSample processedWithAveraging(const Sample& sample)
{
    SimulationOptions options;
    options.setUseAvgMaterials(true);
    return ReSample::processedSample(sample, options);
}

void expectSpan(const ReSample& re_sample, size_t i, double low, double hig)
{
    ASSERT_LT(i, re_sample.numberOfSlices());
    if (std::isinf(low))
        EXPECT_EQ(re_sample.avgeSlice(i).low(), low);
    else
        EXPECT_NEAR(re_sample.avgeSlice(i).low(), low, eps);
    if (std::isinf(hig))
        EXPECT_EQ(re_sample.avgeSlice(i).hig(), hig);
    else
        EXPECT_NEAR(re_sample.avgeSlice(i).hig(), hig, eps);
}

} // namespace

//! A tilted particle deposited on the fronting/substrate interface must make the averaged-material
//! subdivision cover the particle's actual post-deposit z range, not its pre-deposit local range.
TEST(SubdivideLayers, DepositedTiltedParticleUsesAnchoredExtent)
{
    const Vacuum vacuum;
    Particle particle(::particleMaterial(), Pyramid4(10., 5., 54.73 * Units::deg));
    particle.rotate(RotationY(30. * Units::deg));

    Layer fronting(vacuum);
    fronting.deposit2D(Dilute2D(1e-3, particle));
    const IParticle* placed = fronting.structsAtBottom().front()->particle();
    ASSERT_NEAR(placed->bottomZ(), 0., eps);
    const double top = placed->topZ();

    Sample sample;
    sample.addLayer(fronting);
    sample.addLayer(Layer(vacuum));

    const ReSample re_sample = ::processedWithAveraging(sample);

    ASSERT_EQ(re_sample.numberOfSlices(), 3u);
    ::expectSpan(re_sample, 0, top, Span::inf);
    ::expectSpan(re_sample, 1, 0., top);
    ::expectSpan(re_sample, 2, -Span::inf, 0.);
}

//! A particle flipped upside down has a negative intrinsic bottom before deposit; subdivision must
//! still follow the aligned range [0, height] after deposit.
TEST(SubdivideLayers, DepositedFlippedParticleUsesAnchoredExtent)
{
    const Vacuum vacuum;
    Particle particle(::particleMaterial(), Pyramid4(10., 5., 54.73 * Units::deg));
    particle.rotate(RotationX(180. * Units::deg));

    Layer fronting(vacuum);
    fronting.deposit2D(Dilute2D(1e-3, particle));
    const IParticle* placed = fronting.structsAtBottom().front()->particle();
    ASSERT_NEAR(placed->bottomZ(), 0., eps);
    const double top = placed->topZ();

    Sample sample;
    sample.addLayer(fronting);
    sample.addLayer(Layer(vacuum));

    const ReSample re_sample = ::processedWithAveraging(sample);

    ASSERT_EQ(re_sample.numberOfSlices(), 3u);
    ::expectSpan(re_sample, 0, top, Span::inf);
    ::expectSpan(re_sample, 1, 0., top);
    ::expectSpan(re_sample, 2, -Span::inf, 0.);
}

//! A tilted suspended round particle must occupy one slice directly below the interface. If the
//! subdivision zone is displaced, resampling attempts an unsupported partial tilted-cylinder cut.
TEST(SubdivideLayers, SuspendedTiltedParticleUsesAnchoredExtent)
{
    const Vacuum vacuum;
    Particle particle(::particleMaterial(), Cylinder(2., 6.));
    particle.rotate(RotationY(30. * Units::deg));

    Layer substrate(vacuum);
    substrate.suspend2D(Dilute2D(1e-3, particle));
    const IParticle* placed = substrate.structsAtTop().front()->particle();
    ASSERT_NEAR(placed->topZ(), 0., eps);
    const double bottom = placed->bottomZ();

    Sample sample;
    sample.addLayer(Layer(vacuum));
    sample.addLayer(substrate);

    const ReSample re_sample = ::processedWithAveraging(sample);

    ASSERT_EQ(re_sample.numberOfSlices(), 3u);
    ::expectSpan(re_sample, 0, 0., Span::inf);
    ::expectSpan(re_sample, 1, bottom, 0.);
    ::expectSpan(re_sample, 2, -Span::inf, bottom);
}

//! User z translation survives deposit anchoring. For a tilted particle crossing the interface,
//! subdivision must split exactly around the actual post-deposit top and bottom.
TEST(SubdivideLayers, DepositedTiltedParticleWithZOffsetUsesActualExtent)
{
    const Vacuum vacuum;
    Particle particle(::particleMaterial(), Box(2., 2., 6.));
    particle.rotate(RotationY(90. * Units::deg));
    particle.translate(R3(0., 0., -0.4));

    Layer fronting(vacuum);
    fronting.deposit2D(Dilute2D(1e-3, particle));
    const IParticle* placed = fronting.structsAtBottom().front()->particle();
    const double bottom = placed->bottomZ();
    const double top = placed->topZ();
    ASSERT_LT(bottom, 0.);
    ASSERT_GT(top, 0.);

    Sample sample;
    sample.addLayer(fronting);
    sample.addLayer(Layer(vacuum));

    const ReSample re_sample = ::processedWithAveraging(sample);

    ASSERT_EQ(re_sample.numberOfSlices(), 4u);
    ::expectSpan(re_sample, 0, top, Span::inf);
    ::expectSpan(re_sample, 1, 0., top);
    ::expectSpan(re_sample, 2, bottom, 0.);
    ::expectSpan(re_sample, 3, -Span::inf, bottom);
}
