/*
  Copyright 2025 EQUINOR ASA

  This file is part of the Open Porous Media project (OPM).

  OPM is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  OPM is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with OPM.  If not, see <http://www.gnu.org/licenses/>.
*/
#include <config.h>

#define BOOST_TEST_MODULE TestMiniVector

#include <boost/test/unit_test.hpp>
#include <cuda_runtime.h>
#include <opm/simulators/linalg/gpuistl/MiniVector.hpp>
#include <opm/simulators/linalg/gpuistl/detail/gpu_safe_call.hpp>
#include <opm/simulators/linalg/gpuistl/gpu_smart_pointer.hpp>
#include <utility> // for std::ignore

BOOST_AUTO_TEST_CASE(DefaultConstructor)
{
    ::Opm::gpuistl::MiniVector<int, 3> v; // Default constructor initializes all elements to zero
    BOOST_TEST(v.size() == 3u);
    for (std::size_t i = 0; i < v.size(); ++i) {
        BOOST_TEST(v[i] == 0);
    }
}

BOOST_AUTO_TEST_CASE(UniformValueConstructor)
{
    constexpr float kVal = 2.5f;
    ::Opm::gpuistl::MiniVector<float, 4> v(kVal);
    for (auto e : v) {
        BOOST_TEST(e == kVal, boost::test_tools::tolerance(1e-6f));
    }
}

BOOST_AUTO_TEST_CASE(InitializerListConstructor)
{
    ::Opm::gpuistl::MiniVector<double, 2> v {1.1, 2.2};
    BOOST_TEST(v[0] == 1.1, boost::test_tools::tolerance(1e-12));
    BOOST_TEST(v[1] == 2.2, boost::test_tools::tolerance(1e-12));
}

BOOST_AUTO_TEST_CASE(AtBoundsChecking)
{
    ::Opm::gpuistl::MiniVector<int, 2> v {5, 6};
    BOOST_CHECK_THROW(v.at(2), std::out_of_range);
}

BOOST_AUTO_TEST_CASE(IteratorTraversal)
{
    ::Opm::gpuistl::MiniVector<int, 3> v {1, 2, 3};
    int sum = 0;
    for (int e : v)
        sum += e;
    BOOST_TEST(sum == 6);
}

template <class VecT>
__global__ void
fillKernel(VecT* v, typename VecT::value_type val)
{
    (*v)[threadIdx.x] = val;
}

template <class VecT, class IntT>
__global__ void
sumKernel(const VecT* v, IntT* out)
{
    if (threadIdx.x == 0) {
        IntT s = 0;
        for (std::size_t i = 0; i < VecT::size(); ++i) {
            s += (*v)[i];
        }
        *out = s;
    }
}

BOOST_AUTO_TEST_CASE(GPUFill)
{
    // make_gpu_shared_ptr<T>() should:
    //   1. allocate device memory for a single T
    //   2. return a std::shared_ptr<T> whose custom deleter frees GPU memory
    //   3. optionally take a host value to copy to device (overload)

    auto dvec = ::Opm::gpuistl::make_gpu_shared_ptr<::Opm::gpuistl::MiniVector<int, 3>>();

    // Fill the vector from the device
    fillKernel<<<1, 3>>>(dvec.get(), 7);
    OPM_GPU_SAFE_CALL(cudaDeviceSynchronize());

    // Copy back and verify
    ::Opm::gpuistl::MiniVector<int, 3> hvec = ::Opm::gpuistl::copyFromGPU(dvec);
    for (int i = 0; i < 3; ++i) {
        BOOST_TEST(hvec[i] == 7);
    }
}

BOOST_AUTO_TEST_CASE(GPUSum)
{
    ::Opm::gpuistl::MiniVector<int, 4> hsrc(1); // [1, 1, 1, 1]

    auto dsrc = ::Opm::gpuistl::make_gpu_shared_ptr<::Opm::gpuistl::MiniVector<int, 4>>(hsrc);
    auto dout = ::Opm::gpuistl::make_gpu_shared_ptr<int>();

    sumKernel<<<1, 1>>>(dsrc.get(), dout.get());
    OPM_GPU_SAFE_CALL(cudaDeviceSynchronize());

    int sum = ::Opm::gpuistl::copyFromGPU(dout);
    BOOST_TEST(sum == 4);
}

BOOST_AUTO_TEST_CASE(TestArithmeticOperations)
{
    ::Opm::gpuistl::MiniVector<int, 3> v1 {1, 2, 3};
    ::Opm::gpuistl::MiniVector<int, 3> v2 {4, 5, 6};

    auto v3 = v1;
    v3 += v2;
    BOOST_TEST(v3[0] == 5);
    BOOST_TEST(v3[1] == 7);
    BOOST_TEST(v3[2] == 9);

    v3 = v2;
    v3 -= v1;
    BOOST_TEST(v3[0] == 3);
    BOOST_TEST(v3[1] == 3);
    BOOST_TEST(v3[2] == 3);

    v3 = v1;
    v3 *= 2;
    BOOST_TEST(v3[0] == 2);
    BOOST_TEST(v3[1] == 4);
    BOOST_TEST(v3[2] == 6);

    v3 = v1 + v2;
    BOOST_TEST(v3[0] == 5);
    BOOST_TEST(v3[1] == 7);
    BOOST_TEST(v3[2] == 9);

    v3 = v2 - v1;
    BOOST_TEST(v3[0] == 3);
    BOOST_TEST(v3[1] == 3);
    BOOST_TEST(v3[2] == 3);
}
