#include "Device/Coord/CoordSystem1D.h"
#include "Base/Axis/MakeScale.h"
#include "Base/Axis/Scale.h"
#include "Base/Const/Units.h"
#include "Sim/Scan/QzScan.h"
#include "Tests/GTestWrapper/google_test.h"
#include <algorithm>
#include <numbers>

using std::numbers::pi;

class CoordSystem1DTest : public ::testing::Test {
public:
    double getQ(double angle) { return 4.0 * pi * std::sin(angle) / m_wavelength; }

protected:
    void checkConventionalConverter(const CoordSystem1D& test_object);
    void checkQSpecConverter(const CoordSystem1D& test_object);
    Scale m_axis = EquiDivision("Angles", 5, 0.5, 1.0);
    Scale m_q_axis = EquiDivision("Q values", 5, 0.0, 1.0);
    QzScan m_qscan{m_q_axis};
    double m_wavelength{1.};
};

void CoordSystem1DTest::checkConventionalConverter(const CoordSystem1D& test_object)
{
    double expected_min = m_axis.binCenter(0);
    EXPECT_NEAR(test_object.calculateMin(0, Coords::UNDEFINED), Units::rad2deg(expected_min),
                Units::rad2deg(expected_min) * 1e-10);
    EXPECT_NEAR(test_object.calculateMin(0, Coords::NBINS), 0.0, 1e-10);
    EXPECT_NEAR(test_object.calculateMin(0, Coords::RADIANS), expected_min, expected_min * 1e-10);
    EXPECT_NEAR(test_object.calculateMin(0, Coords::DEGREES), Units::rad2deg(expected_min),
                Units::rad2deg(expected_min) * 1e-10);
    EXPECT_NEAR(test_object.calculateMin(0, Coords::QSPACE), getQ(expected_min),
                getQ(expected_min) * 1e-10);

    double expected_max = m_axis.binCenters().back();
    EXPECT_NEAR(test_object.calculateMax(0, Coords::UNDEFINED), Units::rad2deg(expected_max),
                Units::rad2deg(expected_max) * 1e-10);
    EXPECT_NEAR(test_object.calculateMax(0, Coords::NBINS), static_cast<double>(m_axis.size()),
                1e-10);
    EXPECT_NEAR(test_object.calculateMax(0, Coords::RADIANS), expected_max, expected_max * 1e-10);
    EXPECT_NEAR(test_object.calculateMax(0, Coords::DEGREES), Units::rad2deg(expected_max),
                Units::rad2deg(expected_max) * 1e-10);
    EXPECT_NEAR(test_object.calculateMax(0, Coords::QSPACE), getQ(expected_max),
                getQ(expected_max) * 1e-10);

    // DEFAULT
    std::unique_ptr<Scale> axis_default(test_object.convertedAxis(0, Coords::UNDEFINED));
    EXPECT_EQ(axis_default->min(), test_object.calculateMin(0, Coords::UNDEFINED));
    EXPECT_EQ(axis_default->max(), test_object.calculateMax(0, Coords::UNDEFINED));

    // QSPACE
    std::unique_ptr<const Scale> axis_qspace(test_object.convertedAxis(0, Coords::QSPACE));
    EXPECT_EQ(axis_qspace->min(), test_object.calculateMin(0, Coords::QSPACE));
    EXPECT_EQ(axis_qspace->max(), test_object.calculateMax(0, Coords::QSPACE));

    // NBINS
    std::unique_ptr<Scale> axis_nbins(test_object.convertedAxis(0, Coords::NBINS));
    EXPECT_EQ(axis_nbins->min(), test_object.calculateMin(0, Coords::NBINS));
    EXPECT_EQ(axis_nbins->max(), test_object.calculateMax(0, Coords::NBINS));
}

void CoordSystem1DTest::checkQSpecConverter(const CoordSystem1D& test_object)
{
    double expected_min = m_q_axis.binCenter(0);
    EXPECT_EQ(test_object.calculateMin(0, Coords::UNDEFINED), expected_min);
    EXPECT_NEAR(test_object.calculateMin(0, Coords::NBINS), 0.0, 1e-10);
    EXPECT_EQ(test_object.calculateMin(0, Coords::QSPACE), expected_min);

    double expected_max = m_q_axis.binCenters().back();
    EXPECT_EQ(test_object.calculateMax(0, Coords::UNDEFINED), expected_max);
    EXPECT_NEAR(test_object.calculateMax(0, Coords::NBINS), static_cast<double>(m_q_axis.size()),
                1e-10);
    EXPECT_EQ(test_object.calculateMax(0, Coords::QSPACE), expected_max);

    // DEFAULT
    std::unique_ptr<Scale> axis_default(test_object.convertedAxis(0, Coords::UNDEFINED));
    EXPECT_EQ(axis_default->min(), test_object.calculateMin(0, Coords::UNDEFINED));
    EXPECT_EQ(axis_default->max(), test_object.calculateMax(0, Coords::UNDEFINED));

    // QSPACE
    std::unique_ptr<Scale> axis_qspace(test_object.convertedAxis(0, Coords::QSPACE));
    EXPECT_EQ(axis_qspace->min(), test_object.calculateMin(0, Coords::QSPACE));
    EXPECT_EQ(axis_qspace->max(), test_object.calculateMax(0, Coords::QSPACE));
    EXPECT_EQ(*axis_default, *axis_qspace);

    // NBINS
    std::unique_ptr<Scale> axis_nbins(test_object.convertedAxis(0, Coords::NBINS));
    EXPECT_EQ(axis_nbins->min(), test_object.calculateMin(0, Coords::NBINS));
    EXPECT_EQ(axis_nbins->max(), test_object.calculateMax(0, Coords::NBINS));
}

TEST_F(CoordSystem1DTest, MainFunctionality)
{
    checkConventionalConverter(AngularReflectometryCoords(m_wavelength, m_axis));
    checkQSpecConverter(WavenumberReflectometryCoords(m_qscan.coordinateAxis()->clone()));
}

TEST_F(CoordSystem1DTest, Exceptions)
{
    AngularReflectometryCoords converter(m_wavelength, m_axis);

    EXPECT_FAILED_ASSERT(converter.calculateMin(0, Coords::MM));
    EXPECT_FAILED_ASSERT(converter.calculateMin(1, Coords::RADIANS));

    EXPECT_FAILED_ASSERT(converter.calculateMax(0, Coords::MM));
    EXPECT_FAILED_ASSERT(converter.calculateMax(1, Coords::RADIANS));

    EXPECT_FAILED_ASSERT(converter.convertedAxis(0, Coords::MM));
    EXPECT_FAILED_ASSERT(converter.convertedAxis(1, Coords::UNDEFINED));

    Scale axis = EquiDivision("Angles", 100, 0.0, (2 * pi));
    EXPECT_FAILED_ASSERT(AngularReflectometryCoords converter2(m_wavelength, axis));

    WavenumberReflectometryCoords converter2(m_qscan.coordinateAxis()->clone());
    // wrong units
    EXPECT_FAILED_ASSERT(converter2.calculateMin(0, Coords::MM));
    EXPECT_FAILED_ASSERT(converter2.calculateMin(0, Coords::RADIANS));
    EXPECT_FAILED_ASSERT(converter2.calculateMin(0, Coords::DEGREES));
    EXPECT_FAILED_ASSERT(converter2.calculateMax(0, Coords::MM));
    EXPECT_FAILED_ASSERT(converter2.calculateMax(0, Coords::RADIANS));
    EXPECT_FAILED_ASSERT(converter2.calculateMax(0, Coords::DEGREES));
    EXPECT_FAILED_ASSERT(converter2.convertedAxis(0, Coords::MM));

    // wrong axis index
    EXPECT_FAILED_ASSERT(converter2.calculateMin(1, Coords::QSPACE));
    EXPECT_FAILED_ASSERT(converter2.convertedAxis(1, Coords::UNDEFINED));
}

TEST_F(CoordSystem1DTest, Clone)
{
    AngularReflectometryCoords converter(m_wavelength, m_axis);
    std::unique_ptr<CoordSystem1D> converter_clone(converter.clone());
    checkConventionalConverter(*converter_clone);

    WavenumberReflectometryCoords converterQ(m_qscan.coordinateAxis()->clone());
    std::unique_ptr<CoordSystem1D> converterQ_clone(converterQ.clone());
    checkQSpecConverter(*converterQ_clone);
}

TEST_F(CoordSystem1DTest, NonDefaultUnitsInInput)
{
    Scale axis = ListScan("x", std::vector<double>{0.0, 0.5, 1.0});

    {
        AngularReflectometryCoords converter(m_wavelength, axis, Coords::DEGREES);
        auto out = converter.convertedAxis(0, Coords::DEGREES);
        EXPECT_TRUE(axis.size() == out->size());
        EXPECT_DOUBLE_EQ(axis.binCenter(0), out->binCenter(0));
        EXPECT_DOUBLE_EQ(axis.binCenter(1), out->binCenter(1));
        EXPECT_DOUBLE_EQ(axis.binCenter(2), out->binCenter(2));
    }

    auto values = axis.binCenters();
    std::for_each(values.begin(), values.end(), [this](double& value) { value = getQ(value); });
    Scale q_axis = ListScan("q", values);
    {
        AngularReflectometryCoords converter(m_wavelength, q_axis, Coords::QSPACE);
        auto out = converter.convertedAxis(0, Coords::RADIANS);
        EXPECT_TRUE(axis.size() == out->size());
        EXPECT_DOUBLE_EQ(axis.binCenter(0), out->binCenter(0));
        EXPECT_DOUBLE_EQ(axis.binCenter(1), out->binCenter(1));
        EXPECT_DOUBLE_EQ(axis.binCenter(2), out->binCenter(2));
    }
}
