//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      Tests/Unit/Sample/CrystalTest.cpp
//! @brief     Tests alignment of a Crystal and a Mesocrystal: Crystal::transformed rotates
//!            lattice and basis together, and Mesocrystal z-extents follow its placement.
//!
//! @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 "Sample/Particle/Crystal.h"
#include "Base/Const/Units.h"
#include "Sample/HardParticle/HardParticles.h"
#include "Sample/Lattice/Lattice3D.h"
#include "Sample/Material/Vacuum.h"
#include "Sample/Particle/Mesocrystal.h"
#include "Sample/Particle/Particle.h"
#include "Sample/Particle/Rotations.h"
#include "Tests/GTestWrapper/google_test.h"
#include <memory>

//! Verifies that Crystal::transformed rotates the lattice and the basis as a single rigid
//! whole and then translates the basis: the basis offset rotates with the lattice axes, the
//! basis acquires the crystal orientation, and the position variance is carried over.
TEST(Crystal, TransformedRotatesLatticeAndBasisRigidly)
{
    const Vacuum vacuum;
    Particle basis(vacuum, Sphere(1.));
    basis.translate(R3(4., 0., 0.));
    const Lattice3D lattice(R3(8., 0., 0.), R3(0., 8., 0.), R3(0., 0., 8.));
    const Crystal crystal(basis, lattice, 0.25);

    const RotationZ rotation(90 * Units::deg);
    const std::unique_ptr<Crystal> transformed(crystal.transformed(R3(1., 2., 3.), &rotation));

    // basis position: Rz(90)*(4,0,0) + (1,2,3) = (0,4,0) + (1,2,3) = (1,6,3)
    const R3 basis_pos = transformed->basis()->particlePosition();
    EXPECT_NEAR(basis_pos.x(), 1., 1e-12);
    EXPECT_NEAR(basis_pos.y(), 6., 1e-12);
    EXPECT_NEAR(basis_pos.z(), 3., 1e-12);

    // basis orientation: Rz(90): (1,0,0) -> (0,1,0)
    ASSERT_TRUE(transformed->basis()->particleRotation());
    const R3 probe = transformed->basis()->particleRotation()->transformed(R3(1., 0., 0.));
    EXPECT_NEAR(probe.x(), 0., 1e-12);
    EXPECT_NEAR(probe.y(), 1., 1e-12);
    EXPECT_NEAR(probe.z(), 0., 1e-12);

    // lattice axes rotate with the crystal: a=(8,0,0)->(0,8,0), b=(0,8,0)->(-8,0,0), c fixed
    const R3 a = transformed->lattice()->basisVectorA();
    const R3 b = transformed->lattice()->basisVectorB();
    const R3 c = transformed->lattice()->basisVectorC();
    EXPECT_NEAR(a.x(), 0., 1e-12);
    EXPECT_NEAR(a.y(), 8., 1e-12);
    EXPECT_NEAR(a.z(), 0., 1e-12);
    EXPECT_NEAR(b.x(), -8., 1e-12);
    EXPECT_NEAR(b.y(), 0., 1e-12);
    EXPECT_NEAR(b.z(), 0., 1e-12);
    EXPECT_NEAR(c.x(), 0., 1e-12);
    EXPECT_NEAR(c.y(), 0., 1e-12);
    EXPECT_NEAR(c.z(), 8., 1e-12);

    EXPECT_DOUBLE_EQ(transformed->position_variance(), 0.25);
}

//! Verifies that Crystal::transformed with a null rotation translates the basis only and
//! leaves the lattice axes and the basis orientation untouched.
TEST(Crystal, TransformedWithoutRotationTranslatesBasisOnly)
{
    const Vacuum vacuum;
    Particle basis(vacuum, Sphere(1.));
    basis.translate(R3(4., 0., 0.));
    const Lattice3D lattice(R3(8., 0., 0.), R3(0., 8., 0.), R3(0., 0., 8.));
    const Crystal crystal(basis, lattice);

    const std::unique_ptr<Crystal> transformed(crystal.transformed(R3(1., 2., 3.), nullptr));

    EXPECT_EQ(transformed->basis()->particlePosition(), R3(5., 2., 3.));
    EXPECT_EQ(transformed->basis()->particleRotation(), nullptr);
    EXPECT_EQ(transformed->lattice()->basisVectorA(), R3(8., 0., 0.));
    EXPECT_EQ(transformed->lattice()->basisVectorB(), R3(0., 8., 0.));
    EXPECT_EQ(transformed->lattice()->basisVectorC(), R3(0., 0., 8.));
}

//! Verifies that Crystal::transformed composes a pre-rotated basis with the applied rotation
//! and rotates the lattice by the same rotation: the basis orientation becomes
//! transform-after-basis, and lattice axes rotate as a rigid whole with the basis.
TEST(Crystal, TransformedComposesBasisRotation)
{
    const Vacuum vacuum;
    Particle basis(vacuum, Sphere(1.));
    basis.rotate(RotationZ(90 * Units::deg)); // basis's own rotation
    basis.translate(R3(4., 0., 0.));
    const Lattice3D lattice(R3(8., 0., 0.), R3(0., 8., 0.), R3(0., 0., 8.));
    const Crystal crystal(basis, lattice);

    const RotationX rotation(90 * Units::deg);
    const std::unique_ptr<Crystal> transformed(crystal.transformed(R3(1., 2., 3.), &rotation));

    // basis orientation: Rx(90)*Rz(90): (1,0,0) -Rz-> (0,1,0) -Rx-> (0,0,1)
    ASSERT_TRUE(transformed->basis()->particleRotation());
    const R3 probe = transformed->basis()->particleRotation()->transformed(R3(1., 0., 0.));
    EXPECT_NEAR(probe.x(), 0., 1e-12);
    EXPECT_NEAR(probe.y(), 0., 1e-12);
    EXPECT_NEAR(probe.z(), 1., 1e-12);

    // basis position: Rx(90)*(4,0,0) + (1,2,3) = (4,0,0) + (1,2,3) = (5,2,3)
    const R3 basis_pos = transformed->basis()->particlePosition();
    EXPECT_NEAR(basis_pos.x(), 5., 1e-12);
    EXPECT_NEAR(basis_pos.y(), 2., 1e-12);
    EXPECT_NEAR(basis_pos.z(), 3., 1e-12);

    // lattice axes rotate by Rx(90): a fixed, b=(0,8,0)->(0,0,8), c=(0,0,8)->(0,-8,0)
    const R3 a = transformed->lattice()->basisVectorA();
    const R3 b = transformed->lattice()->basisVectorB();
    const R3 c = transformed->lattice()->basisVectorC();
    EXPECT_NEAR(a.x(), 8., 1e-12);
    EXPECT_NEAR(a.y(), 0., 1e-12);
    EXPECT_NEAR(a.z(), 0., 1e-12);
    EXPECT_NEAR(b.x(), 0., 1e-12);
    EXPECT_NEAR(b.y(), 0., 1e-12);
    EXPECT_NEAR(b.z(), 8., 1e-12);
    EXPECT_NEAR(c.x(), 0., 1e-12);
    EXPECT_NEAR(c.y(), -8., 1e-12);
    EXPECT_NEAR(c.z(), 0., 1e-12);
}

//! Verifies that a Mesocrystal reports z-extents that include its position: the outer shape
//! (a cylinder of height 6, z in [0,6]) is lifted by the mesocrystal translation.
TEST(Mesocrystal, ZExtentsUnderTranslation)
{
    const Vacuum vacuum;
    const Lattice3D lattice(R3(8., 0., 0.), R3(0., 8., 0.), R3(0., 0., 8.));
    const Crystal crystal(Particle(vacuum, Sphere(2.)), lattice);
    const Cylinder outer(10., 6.);
    Mesocrystal meso(crystal, outer);
    meso.translate(R3(0., 0., 5.));

    EXPECT_DOUBLE_EQ(meso.bottomZ(), 5.);
    EXPECT_DOUBLE_EQ(meso.topZ(), 11.);
    EXPECT_DOUBLE_EQ(meso.centerZ(), 8.);
}

//! Verifies that the mesocrystal's own rotation feeds through to its outer-shape z-extent:
//! tipping the cylinder (radius 10) by 90 deg about x makes the intrinsic height equal to the
//! cylinder diameter.
TEST(Mesocrystal, HeightTracksRotation)
{
    const Vacuum vacuum;
    const Lattice3D lattice(R3(8., 0., 0.), R3(0., 8., 0.), R3(0., 0., 8.));
    const Crystal crystal(Particle(vacuum, Sphere(2.)), lattice);
    const Cylinder outer(10., 6.);
    Mesocrystal meso(crystal, outer);
    meso.rotate(RotationX(90 * Units::deg));

    EXPECT_NEAR(meso.height(), 20., 1e-12);
}
