// -*- C++ -*-
//===----------------------------------------------------------------------===//
//
// Copyright (C) Intel Corporation
//
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
// This file incorporates work covered by the following copyright and permission
// notice:
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
//
//===----------------------------------------------------------------------===//

#ifndef _STD_RANGES_TEST_H
#define _STD_RANGES_TEST_H

#include <oneapi/dpl/execution>
#include <oneapi/dpl/algorithm>

#include "support/test_config.h"
#include "support/test_macros.h"
#include "support/utils.h"
#include "support/utils_invoke.h"       // for CLONE_TEST_POLICY macro

#if _ENABLE_STD_RANGES_TESTING

static_assert(ONEDPL_HAS_RANGE_ALGORITHMS >= 202605L);

#if TEST_CPP20_SPAN_PRESENT
#include <span>
#endif
#include <vector>
#include <typeinfo>
#include <type_traits>
#include <string>
#include <ranges>
#include <algorithm>
#include <memory>
#include <array>

namespace test_std_ranges
{

// To suppress dangling iterator checks for some tests
template <typename>
constexpr bool supress_dangling_iterators_check = false;

#if PSTL_USE_DEBUG
inline constexpr int big_size = (1<<22) + 37; //4M
#else
// The largest specializations of algorithms with device policies handle 16M+ elements.
inline constexpr int big_size = (1<<24) + 10; //16M
#endif

// Cap used for the device tier of scan-based std_ranges tests when the dpcpp
// backend targets a CPU device.
inline constexpr int big_size_scan_cpu = (1 << 20) + 10; //1M

// ~100K is sufficient for parallel policies.
// It also usually results in using multiple-work-group specializations for device policies.
inline constexpr int medium_size = (1<<17) + 10; //128K

// It is a sufficient size for sequential policies.
// It also usually results in using single-work-group specializations for device policies.
inline constexpr int small_size = 2025;

#if TEST_DPCPP_BACKEND_PRESENT
inline constexpr std::array<int, 3> big_sz = {/*serial*/ small_size, /*par*/ medium_size, /*device*/ big_size};

// Size tuple for scan-based std_ranges tests (copy_if, remove*, unique*,
// set_*). Shrinks the device tier for CPU+dpcpp at runtime.
inline std::array<int, 3>
get_scan_big_sz()
{
    const int device_size = TestUtils::test_queue_is_cpu() ? big_size_scan_cpu : big_size;
    return {/*serial*/ small_size, /*par*/ medium_size, /*device*/ device_size};
}
#else
inline constexpr std::array<int, 2> big_sz = {/*serial*/ small_size, /*par*/ medium_size};

inline std::array<int, 2>
get_scan_big_sz()
{
    return {/*serial*/ small_size, /*par*/ medium_size};
}
#endif

enum TestDataMode
{
    data_in,
    data_in_out,
    data_in_out_lim,
    data_in_in,
    data_in_in_out,
    data_in_in_out_lim
};

auto f_mutuable = [](auto&& val) { return val *= val; };
auto proj_mutuable = [](auto&& val) { return val *= 2; };

auto f = [](auto&& val) { return val * val; };
auto binary_f = [](auto&& val1, auto&& val2) { return val1 * val2; };
auto proj = [](auto&& val){ return val * 2; };
auto pred = [](auto&& val) { return val == 5; };
auto select_many = [](int val) { return (val % 29) > 1; };

auto binary_pred = [](auto&& val1, auto&& val2) { return val1 == val2; };
auto binary_pred_const = [](const auto& val1, const auto& val2) { return val1 == val2; };

auto pred1 = [](auto&& val) -> bool { return val > 0; };
auto pred2 = [](auto&& val) -> bool { return val == 4; };
auto pred3 = [](auto&& val) -> bool { return val < 0; };

auto mul1 = [](auto&& v) { return v; };
using mul1_t = decltype(mul1);
auto div3 = [](auto&& v) { return v / 3; };
using div3_t = decltype(div3);

struct P2
{
    P2() {}
    P2(int v): x(v) {}
    P2(int v, int w): x(v), y(w) {}
    int x = {};
    int y = {};

    int proj() const { return x; }
    friend bool operator==(const P2& a, const P2& b) { return a.x == b.x && a.y == b.y; }
};

struct P3 : public P2
{
    using P2::P2;
    P3(int v): P2(v, v + 13) {}

    friend bool operator==(const P3& a, const P3& b) { return a.x == b.x && a.y == b.y; }
};

struct A
{
    int a;
    operator int() const { return a; }
};

struct B
{
    int b;
    operator int() const { return b; }
};

auto proj_a = [](const A& a) { return a.a; };
auto proj_b = [](const B& b) { return b.b; };

// These are copies of __range_size and __range_size_t utilities from oneDPL
// to get a size type of a range be it sized or not
template <typename R>
struct range_size {
    using type = std::uint8_t;
};

template <std::ranges::sized_range R>
struct range_size<R> {
    using type = std::ranges::range_size_t<R>;
};

template <typename R>
using range_size_t = typename range_size<R>::type;

template<typename, typename = void>
static constexpr bool is_iterator{};

template<typename T>
static constexpr
bool is_iterator<T, std::void_t<decltype(++std::declval<T&>()), decltype(*std::declval<T&>())>> = true;

template<typename, typename = void>
static constexpr bool check_in{};

template<typename T>
static constexpr
bool check_in<T, std::void_t<decltype(std::declval<T>().in)>> = true;

template<typename, typename = void>
static constexpr bool check_in1{};

template<typename T>
static constexpr
bool check_in1<T, std::void_t<decltype(std::declval<T>().in1)>> = true;

template<typename, typename = void>
static constexpr bool check_in2{};

template<typename T>
static constexpr
bool check_in2<T, std::void_t<decltype(std::declval<T>().in2)>> = true;

template<typename, typename = void>
static constexpr bool check_out{};

template<typename T>
static constexpr
bool check_out<T, std::void_t<decltype(std::declval<T>().out)>> = true;

template<typename, typename = void>
static constexpr bool is_range{};

template<typename T>
static constexpr
bool is_range<T, std::void_t<decltype(std::declval<T&>().begin())>> = true;

template<typename, typename = void>
static constexpr bool check_minmax{};

template<typename T>
static constexpr
bool check_minmax<T, std::void_t<decltype(std::declval<T>().min, std::declval<T>().max)>> = true;

template<typename>
constexpr int trivial_size{0};

template<typename>
constexpr int calc_res_size(int n, int) { return n; }

// If in1 range is empty, then the out range is always empty
// Can be specialized with an algorithm type if the behaviour is different, e.g. see set_union test.
template<typename>
int out_size_with_empty_in1(int) { return 0; }

// If in2 range is empty, then the out range is always empty
// Can be specialized with an algorithm type if the behaviour is different, e.g. see set_union test.
template<typename>
int out_size_with_empty_in2(int) { return 0; }

auto data_gen2_default = [](auto i) { return i % 5 ? i : 0;};
auto data_gen_unprocessed = [](auto) { return -1;};

template <typename T>
static constexpr bool check_in_in_result{};

template <typename I1, typename I2>
static constexpr bool check_in_in_result<std::ranges::in_in_result<I1, I2>> = true;

template <typename T>
static constexpr bool check_in_out_result{};

template <typename I1, typename O>
static constexpr bool check_in_out_result<std::ranges::in_out_result<I1, O>> = true;

template <typename T>
static constexpr bool check_in_in_out_result{};

template <typename I1, typename I2, typename O>
static constexpr bool check_in_in_out_result<std::ranges::in_in_out_result<I1, I2, O>> = true;

template <typename _ReturnType>
struct all_dangling_in_result : std::false_type
{
};

template <>
struct all_dangling_in_result<std::ranges::dangling> : std::true_type
{
};

template <>
struct all_dangling_in_result<std::ranges::in_in_result<std::ranges::dangling, std::ranges::dangling>> : std::true_type
{
};

template <>
struct all_dangling_in_result<std::ranges::in_out_result<std::ranges::dangling, std::ranges::dangling>> : std::true_type
{
};

template <>
struct all_dangling_in_result<std::ranges::in_in_out_result<std::ranges::dangling, std::ranges::dangling, std::ranges::dangling>> : std::true_type
{
};

template <>
struct all_dangling_in_result<std::ranges::in_out_out_result<std::ranges::dangling, std::ranges::dangling, std::ranges::dangling>> : std::true_type
{
};

template <>
struct all_dangling_in_result<std::ranges::min_max_result<std::ranges::dangling>> : std::true_type
{
};

template <typename _ReturnType>
constexpr bool all_dangling_in_result_v = all_dangling_in_result<_ReturnType>::value;

void call_with_host_policies(auto algo, auto... args)
{
    algo(oneapi::dpl::execution::seq, args...);
    algo(oneapi::dpl::execution::unseq, args...);
    algo(oneapi::dpl::execution::par, args...);
    algo(oneapi::dpl::execution::par_unseq, args...);
}

// TODO remove after implementation range-based set operations for bounded output range with hetero policies
template <typename Algo>
inline constexpr bool skip_test_for_hetero_policy = false;

template<typename DataType, typename Container, TestDataMode test_mode = data_in, typename DataGen1 = std::identity,
         typename DataGen2 = decltype(data_gen2_default)>
struct test
{
    void
    host_policies(int n_serial, int n_parallel, auto algo, auto& checker, auto... args)
    {
        operator()(n_serial,   oneapi::dpl::execution::seq,       algo, checker, args...);
        operator()(n_serial,   oneapi::dpl::execution::unseq,     algo, checker, args...);
        operator()(n_parallel, oneapi::dpl::execution::par,       algo, checker, args...);
        operator()(n_parallel, oneapi::dpl::execution::par_unseq, algo, checker, args...);
    }

    template<typename Policy, typename Algo, typename Checker, typename TransIn, typename TransOut, TestDataMode mode = test_mode>
    std::enable_if_t<mode == data_in>
    operator()(int max_n, Policy&& exec, Algo algo, Checker& checker, TransIn tr_in, TransOut, auto... args)
    {
        process_data_in(max_n, CLONE_TEST_POLICY(exec), algo, checker, tr_in, args...);

        //test with empty sequence
        process_data_in(trivial_size<std::remove_cvref_t<Algo>>, CLONE_TEST_POLICY(exec), algo, checker, tr_in, args...);
    }

private:

    using rvalue_container_t = std::array<typename Container::value_type, 0>;

    template<typename Policy, typename Algo, typename Checker, typename TransIn>
    void
    process_data_in(int max_n, Policy&& exec, Algo algo, Checker& checker, TransIn tr_in, auto... args)
    {
        std::string sizes{" for "};
        sizes += std::to_string(max_n) + " elements";

        Container cont_in(exec, max_n, DataGen1{});
        Container cont_exp(exec, max_n, DataGen1{});

        auto expected_view = tr_in(std::views::all(cont_exp()));
        auto expected_res = checker(expected_view, args...);

        typename Container::type& A = cont_in();
        decltype(auto) r_in = tr_in(A);
        auto res = algo(CLONE_TEST_POLICY(exec), r_in, args...);

        // check result types
        static_assert(std::is_same_v<decltype(res), decltype(expected_res)>, "Wrong return type");

        if constexpr(is_range<std::remove_cvref_t<decltype(res)>>)
        {
            std::pair exp_split = ret_in_val(expected_res, expected_view.begin());
            std::pair split = ret_in_val(res, r_in.begin());
            EXPECT_EQ(exp_split.first, split.first, (std::string("wrong stop position with ") + typeid(Algo).name() +
                       typeid(decltype(tr_in(std::declval<Container&>()()))).name() + sizes).c_str());
            EXPECT_EQ(exp_split.second, split.second, (std::string("wrong subrange size with ") + typeid(Algo).name() +
                       typeid(decltype(tr_in(std::declval<Container&>()()))).name() + sizes).c_str());
        }
        else
        {
            EXPECT_EQ(ret_in_val(expected_res, expected_view.begin()), ret_in_val(res, r_in.begin()),
                      (std::string("wrong stop position with ") + typeid(Algo).name() +
                       typeid(decltype(tr_in(std::declval<Container&>()()))).name() + sizes).c_str());
        }

        //check result
        auto n = std::ranges::size(expected_view);
        if constexpr(is_range<std::remove_cvref_t<decltype(res)>>)
            n = calc_res_size<std::remove_cvref_t<Algo>>(n, std::ranges::size(res));

        EXPECT_EQ_N(cont_exp().begin(), cont_in().begin(), n, (std::string("data mismatch with ")
            + typeid(Algo).name() + typeid(decltype(tr_in(std::declval<Container&>()()))).name() + sizes).c_str());

        if constexpr(!supress_dangling_iterators_check<std::remove_cvref_t<decltype(algo)>>)
        {
#if _ONEDPL_CPP20_OWNING_VIEW_PRESENT // Otherwise, `tr_in = std::views::all` leads to a compile error for `rvalue_container_t`.
            // Check dangling iterators in return types for call with r-value ranges; 
            // TransIn may modify the non-borrowed range to a borrowed one, so we need to check it.        
            if constexpr(!std::ranges::borrowed_range<decltype(tr_in(std::declval<rvalue_container_t&&>()))>)
            {
                using res_ret_t = decltype(algo(exec, tr_in(std::declval<rvalue_container_t&&>()), args...));

                if constexpr(!std::is_fundamental_v<res_ret_t>)
                {
                    static_assert(all_dangling_in_result_v<res_ret_t>,
                                "res_ret_t is expected to be or consist of std::ranges::dangling");
                }
            }
#endif //_ONEDPL_CPP20_OWNING_VIEW_PRESENT
        }
    }

    template<typename Policy, typename Algo, typename Checker, typename TransIn, typename TransOut,
             TestDataMode mode = test_mode>
    void
    process_data_in_out(int max_n, int n_in, int n_out, Policy&& exec, Algo algo, Checker& checker, TransIn tr_in,
                        TransOut tr_out, auto... args)
    {
        static_assert(mode == data_in_out || mode == data_in_out_lim);
        std::string names{typeid(Algo).name()};
        names += "<" + std::string{typeid(Policy).name()} + ">";
        std::string sizes{" for "};
        sizes += std::to_string(n_in) + " elements and " + std::to_string(n_out) + " space";

        Container cont_in(exec, n_in, DataGen1{});
        Container cont_in_exp(exec, n_in, DataGen1{});

        Container cont_out(exec, n_out, data_gen_unprocessed);
        Container cont_out_exp(exec, n_out, data_gen_unprocessed);

        assert(n_in <= max_n);
        assert(n_out <= max_n);

        auto in_exp_view = tr_in(std::views::all(cont_in_exp()));
        auto out_exp_view = tr_out(std::views::all(cont_out_exp()));
        auto expected_res = checker(in_exp_view, out_exp_view, args...);

        typename Container::type& A = cont_in();
        typename Container::type& B = cont_out();

        auto res = algo(CLONE_TEST_POLICY(exec), tr_in(A), tr_out(B), args...);

        // check result types
        static_assert(std::is_same_v<decltype(res), decltype(expected_res)>, "Wrong return type");

        if constexpr (check_in_out_result<decltype(expected_res)>)
        {
            EXPECT_EQ(ret_in_val(expected_res, in_exp_view.begin()), ret_in_val(res, tr_in(A).begin()),
                        (std::string("wrong input stop position with ") + typeid(Algo).name() + sizes).c_str());
        }
        else if constexpr (check_in_in_out_result<decltype(expected_res)>)
        {
            EXPECT_EQ(ret_in_val<1>(expected_res, in_exp_view.begin()), ret_in_val<1>(res, tr_in(A).begin()),
                      (std::string("wrong input stop position with ") + names + sizes).c_str());

            EXPECT_EQ(ret_in_val<2>(expected_res, in_exp_view.end()), ret_in_val<2>(res, tr_in(A).end()),
                      (std::string("wrong input stop position with ") + names + sizes).c_str());
        }
        else if constexpr (check_in_in_result<decltype(expected_res)>)
        {
            EXPECT_EQ(ret_in_val<1>(expected_res, in_exp_view.begin()), ret_in_val<1>(res, tr_in(A).begin()),
                      (std::string("wrong input stop position with ") + names + sizes).c_str());

            EXPECT_EQ(ret_in_val<2>(expected_res, out_exp_view.begin()), ret_in_val<2>(res, tr_out(B).begin()),
                      (std::string("wrong input stop position with ") + names + sizes).c_str());
        }
        else
        {
            EXPECT_EQ(ret_in_val(expected_res, in_exp_view.begin()), ret_in_val(res, tr_in(A).begin()),
                      (std::string("wrong input stop position with ") + names + sizes).c_str());
        }

        EXPECT_EQ(ret_out_val(expected_res, out_exp_view.begin()), ret_out_val(res, tr_out(B).begin()),
                  (std::string("wrong output stop position with ") + names + sizes).c_str());

        //check result
        auto n = std::ranges::size(out_exp_view);
        EXPECT_EQ_N(cont_out_exp().begin(), cont_out().begin(), n, 
                    (std::string("output mismatch with ") + names + sizes).c_str());

        //check result
        auto n_in_exp = std::ranges::size(in_exp_view);
        EXPECT_EQ_N(cont_in_exp().begin(), cont_in().begin(), n_in_exp,
                    (std::string("input mismatch with ") + names + sizes).c_str());

        if constexpr(!supress_dangling_iterators_check<std::remove_cvref_t<decltype(algo)>>)
        {
#if _ONEDPL_CPP20_OWNING_VIEW_PRESENT // Otherwise, `tr_in = std::views::all` leads to a compile error for `rvalue_container_t`.
            // Check dangling iterators in return types for call with r-value ranges; 
            // TransIn and TransOut may modify the non-borrowed range to a borrowed one, so we need to check it.
            if constexpr(!std::ranges::borrowed_range<decltype(tr_in(std::declval<rvalue_container_t&&>()))>
                        && !std::ranges::borrowed_range<decltype(tr_out(std::declval<rvalue_container_t&&>()))>)
            {
                using res_ret_t = decltype(algo(exec, tr_in(std::declval<rvalue_container_t&&>()),
                                        tr_out(std::declval<rvalue_container_t&&>()), args...));

                if constexpr(!std::is_fundamental_v<res_ret_t>)
                {
                    static_assert(all_dangling_in_result_v<res_ret_t>,
                                "res_ret_t is expected to be or consist of std::ranges::dangling");
                }
            }
#endif //_ONEDPL_CPP20_OWNING_VIEW_PRESENT
        }
    }

public:
    template<typename Policy, typename Algo, typename Checker, TestDataMode mode = test_mode>
    std::enable_if_t<mode == data_in_out>
    operator()(int max_n, Policy&& exec, Algo algo, Checker& checker, auto... args)
    {
        const int r_size = max_n;
        process_data_in_out(max_n, r_size, r_size, CLONE_TEST_POLICY(exec), algo, checker, args...);

        //test cases with empty sequence(s)
	    process_data_in_out(max_n, 0, 0, CLONE_TEST_POLICY(exec), algo, checker, args...);
    }

    template<typename Policy, typename Algo, typename Checker, TestDataMode mode = test_mode>
    std::enable_if_t<mode == data_in_out_lim>
    operator()(int max_n, Policy&& exec, Algo algo, Checker& checker, auto... args)
    {
        const int r_size = max_n;
        process_data_in_out(max_n, r_size, r_size, CLONE_TEST_POLICY(exec), algo, checker, args...);

        //test case size of input range is less than size of output and vice-versa
        process_data_in_out(max_n, r_size/2, r_size, CLONE_TEST_POLICY(exec), algo, checker, args...);
        process_data_in_out(max_n, r_size, r_size/2, CLONE_TEST_POLICY(exec), algo, checker, args...);

        //test cases with empty sequence(s)
        process_data_in_out(max_n, 0, 0, CLONE_TEST_POLICY(exec), algo, checker, args...);
        process_data_in_out(max_n, r_size, 0, CLONE_TEST_POLICY(exec), algo, checker, args...);
        process_data_in_out(max_n, 0, r_size, CLONE_TEST_POLICY(exec), algo, checker, args...);

        //test cases with only one element
        process_data_in_out(max_n, r_size, 1, CLONE_TEST_POLICY(exec), algo, checker, args...);
        process_data_in_out(max_n, 1, r_size, CLONE_TEST_POLICY(exec), algo, checker, args...);
    }

    template<typename Policy, typename Algo, typename Checker, typename TransIn, typename TransOut, 
             TestDataMode mode = test_mode>
    std::enable_if_t<mode == data_in_in>
    operator()(int max_n, Policy&& exec, Algo algo, Checker& checker, TransIn tr_in, TransOut, auto... args)
    {
        const int r_size = max_n;
        process_data_in_in(max_n, r_size, r_size, CLONE_TEST_POLICY(exec), algo, checker, tr_in, args...);

        //test case the sizes of input ranges are different
        process_data_in_in(max_n, r_size/2, r_size, CLONE_TEST_POLICY(exec), algo, checker, tr_in, args...);
        process_data_in_in(max_n, r_size, r_size/2, CLONE_TEST_POLICY(exec), algo, checker, tr_in, args...);

        //test cases with empty sequence(s)
        process_data_in_in(max_n, 0, 0, CLONE_TEST_POLICY(exec), algo, checker, tr_in, args...);
        process_data_in_in(max_n, r_size, 0, CLONE_TEST_POLICY(exec), algo, checker, tr_in, args...);
        process_data_in_in(max_n, 0, r_size, CLONE_TEST_POLICY(exec), algo, checker, tr_in, args...);
    }

private:
    template<typename Policy, typename Algo, typename Checker, typename TransIn>
    void
    process_data_in_in(int max_n, int n_in1, int n_in2, Policy&& exec, Algo algo, Checker& checker, TransIn tr_in,
                       auto... args)
    {
        std::string sizes{" for "};
        sizes += std::to_string(n_in1) + " and " + std::to_string(n_in2) + " elements";

        assert(n_in1 <= max_n);
        assert(n_in2 <= max_n);

        Container cont_in1(exec, n_in1, DataGen1{});
        Container cont_in2(exec, n_in2, DataGen2{});

        auto src_view1 = tr_in(std::views::all(cont_in1()));
        auto src_view2 = tr_in(std::views::all(cont_in2()));
        auto expected_res = checker(src_view1, src_view2, args...);

        typename Container::type& A = cont_in1();
        typename Container::type& B = cont_in2();

        auto res = algo(CLONE_TEST_POLICY(exec), tr_in(A), tr_in(B), args...);

        // check result types
        static_assert(std::is_same_v<decltype(res), decltype(expected_res)>, "Wrong return type");

        if constexpr (check_in_in_result<decltype(expected_res)>)
        {
            EXPECT_EQ(ret_in_val<1>(expected_res, src_view1.begin()), ret_in_val<1>(res, tr_in(A).begin()),
                      (std::string("wrong stop position with ") + typeid(Algo).name() +
                       typeid(decltype(tr_in(std::declval<Container&>()()))).name() + sizes).c_str());

            EXPECT_EQ(ret_in_val<2>(expected_res, src_view2.begin()), ret_in_val<2>(res, tr_in(B).begin()),
                      (std::string("wrong stop position with ") + typeid(Algo).name() +
                       typeid(decltype(tr_in(std::declval<Container&>()()))).name() + sizes).c_str());
        }
        else if constexpr (!std::is_same_v<decltype(res), bool>)
        {
            EXPECT_EQ(ret_in_val(expected_res, src_view1.begin()), ret_in_val(res, tr_in(A).begin()),
                      (std::string("wrong stop position with ") + typeid(Algo).name() +
                       typeid(decltype(tr_in(std::declval<Container&>()()))).name() + sizes).c_str());

            EXPECT_EQ(ret_in_val(expected_res, src_view2.begin()), ret_in_val(res, tr_in(B).begin()),
                      (std::string("wrong stop position with ") + typeid(Algo).name() +
                       typeid(decltype(tr_in(std::declval<Container&>()()))).name() + sizes).c_str());
        }
        else
        {
            EXPECT_EQ(expected_res, res,
                      (std::string("wrong return value from ") + typeid(Algo).name() +
                       typeid(decltype(tr_in(std::declval<Container&>()()))).name() + sizes).c_str());
        }

        if constexpr(!supress_dangling_iterators_check<std::remove_cvref_t<decltype(algo)>>)
        {
#if _ONEDPL_CPP20_OWNING_VIEW_PRESENT // Otherwise, `tr_in = std::views::all` leads to a compile error for `rvalue_container_t`.
            // Check dangling iterators in return types for call with r-value ranges; 
            // TransIn may modify the non-borrowed range to a borrowed one, so we need to check it.
            if constexpr(!std::ranges::borrowed_range<decltype(tr_in(std::declval<rvalue_container_t&&>()))>)
            {
                using res_ret_t = decltype(algo(exec, tr_in(std::declval<rvalue_container_t&&>()),
                                        tr_in(std::declval<rvalue_container_t&&>()), args...));

                if constexpr(!std::is_fundamental_v<res_ret_t>)
                {
                    static_assert(all_dangling_in_result_v<res_ret_t>,
                                "res_ret_t is expected to be or consist of std::ranges::dangling");
                }
            }
#endif //_ONEDPL_CPP20_OWNING_VIEW_PRESENT
        }
    }

    template<typename Policy, typename Algo, typename Checker, typename TransIn, typename TransOut,
             TestDataMode mode = test_mode>
    void
    process_data_in_in_out(int max_n, int n_in1, int n_in2, int n_out, Policy&& exec, Algo algo, Checker& checker,
                           TransIn tr_in, TransOut tr_out, auto... args)
    {
        static_assert(mode == data_in_in_out || mode == data_in_in_out_lim);
        std::string sizes{" for "};
        sizes += std::to_string(n_in1) + " and " + std::to_string(n_in2) + " elements and " + std::to_string(n_out) + " space";

        Container cont_in1(exec, n_in1, DataGen1{});
        Container cont_in2(exec, n_in2, DataGen2{});

        Container cont_out(exec, n_out, data_gen_unprocessed);
        Container cont_exp(exec, n_out, data_gen_unprocessed);

        assert(n_in1 <= max_n);
        assert(n_in2 <= max_n);

        auto src_view1 = tr_in(std::views::all(cont_in1()));
        auto src_view2 = tr_in(std::views::all(cont_in2()));
        auto expected_view = tr_out(std::views::all(cont_exp()));
        auto expected_res = checker(src_view1, src_view2, expected_view, args...);

        typename Container::type& A = cont_in1();
        typename Container::type& B = cont_in2();
        typename Container::type& C = cont_out();

        auto res = algo(CLONE_TEST_POLICY(exec), tr_in(A), tr_in(B), tr_out(C), args...);

        // check result types
        static_assert(std::is_same_v<decltype(res), decltype(expected_res)>, "Wrong return type");

        if constexpr (check_in_out_result<decltype(expected_res)>)
        {
            EXPECT_EQ(ret_in_val(expected_res, src_view1.begin()), ret_in_val(res, tr_in(A).begin()),
                      (std::string("wrong first input stop position with ") + typeid(Algo).name() + sizes).c_str());
        }
        else if constexpr (check_in_in_out_result<decltype(expected_res)>)
        {
            EXPECT_EQ(ret_in_val<1>(expected_res, src_view1.begin()), ret_in_val<1>(res, tr_in(A).begin()),
                      (std::string("wrong first input stop position with ") + typeid(Algo).name() + sizes).c_str());

            EXPECT_EQ(ret_in_val<2>(expected_res, src_view2.begin()), ret_in_val<2>(res, tr_in(B).begin()),
                      (std::string("wrong second input stop position with ") + typeid(Algo).name() + sizes).c_str());
        }
        else
        {
            EXPECT_EQ(ret_in_val(expected_res, src_view1.begin()), ret_in_val(res, tr_in(A).begin()),
                      (std::string("wrong first input stop position with ") + typeid(Algo).name() + sizes).c_str());

            EXPECT_EQ(ret_in_val(expected_res, src_view2.begin()), ret_in_val(res, tr_in(B).begin()),
                      (std::string("wrong second input stop position with ") + typeid(Algo).name() + sizes).c_str());
        }
        EXPECT_EQ(ret_out_val(expected_res, expected_view.begin()), ret_out_val(res, tr_out(C).begin()),
                    (std::string("wrong output stop position with ") + typeid(Algo).name() + sizes).c_str());

        //check result
        auto n = std::ranges::size(expected_view);
        EXPECT_EQ_N(cont_exp().begin(), cont_out().begin(), n, (std::string("output mismatch with ")
                    + typeid(Algo).name() + typeid(Policy).name() + sizes).c_str());

        if constexpr(!supress_dangling_iterators_check<std::remove_cvref_t<decltype(algo)>>)
        {
#if _ONEDPL_CPP20_OWNING_VIEW_PRESENT // Otherwise, `tr_in = std::views::all` leads to a compile error for `rvalue_container_t`.
            // Check dangling iterators in return types for call with r-value ranges; 
            // TransIn and TransOut may modify the non-borrowed range to a borrowed one, so we need to check it.
            if constexpr(!std::ranges::borrowed_range<decltype(tr_in(std::declval<rvalue_container_t&&>()))>
                        && !std::ranges::borrowed_range<decltype(tr_out(std::declval<rvalue_container_t&&>()))>)
            {
                using res_ret_t = decltype(algo(exec, tr_in(std::declval<rvalue_container_t&&>()),
                                        tr_in(std::declval<rvalue_container_t&&>()),
                                        tr_out(std::declval<rvalue_container_t&&>()), args...));

                if constexpr(!std::is_fundamental_v<res_ret_t>)
                {
                    static_assert(all_dangling_in_result_v<res_ret_t>,
                                "res_ret_t is expected to be or consist of std::ranges::dangling");
                }
            }
#endif //_ONEDPL_CPP20_OWNING_VIEW_PRESENT
        }
    }

public:
    template<typename Policy, typename Algo, typename Checker, TestDataMode mode = test_mode>
    std::enable_if_t<mode == data_in_in_out>
    operator()(int max_n, Policy&& exec, Algo algo, Checker& checker, auto... args)
    {
        const int r_size = max_n;
        process_data_in_in_out(max_n, r_size, r_size, r_size*2, CLONE_TEST_POLICY(exec), algo, checker, args...);

        //test cases with empty sequence(s)
        process_data_in_in_out(max_n, 0, 0, 0, CLONE_TEST_POLICY(exec), algo, checker, args...);
        process_data_in_in_out(max_n, 0, r_size, out_size_with_empty_in1<Algo>(r_size), CLONE_TEST_POLICY(exec), algo, checker, args...);
        process_data_in_in_out(max_n, r_size, 0, out_size_with_empty_in2<Algo>(r_size), CLONE_TEST_POLICY(exec), algo, checker, args...);
    }

    template<typename Policy, typename Algo, typename Checker, TestDataMode mode = test_mode>
    std::enable_if_t<mode == data_in_in_out_lim>
    operator()(int max_n, Policy&& exec, Algo algo, Checker& checker, auto... args)
    {
        const int r_size = max_n;
        process_data_in_in_out(max_n, r_size, r_size, r_size, CLONE_TEST_POLICY(exec), algo, checker, args...);
        process_data_in_in_out(max_n, r_size, r_size, r_size*2, CLONE_TEST_POLICY(exec), algo, checker, args...);
        process_data_in_in_out(max_n, r_size/2, r_size, r_size, CLONE_TEST_POLICY(exec), algo, checker, args...);
        process_data_in_in_out(max_n, r_size, r_size/2, r_size, CLONE_TEST_POLICY(exec), algo, checker, args...);
        process_data_in_in_out(max_n, r_size, r_size, r_size/2, CLONE_TEST_POLICY(exec), algo, checker, args...);

        //test cases with empty sequence(s) and/or zero output capacity
        process_data_in_in_out(max_n, 0, 0, 0, CLONE_TEST_POLICY(exec), algo, checker, args...);
        process_data_in_in_out(max_n, r_size, r_size, 0, CLONE_TEST_POLICY(exec), algo, checker, args...);
        process_data_in_in_out(max_n, 0, r_size / 2, r_size, CLONE_TEST_POLICY(exec), algo, checker, args...);
        process_data_in_in_out(max_n, r_size / 2, 0, r_size, CLONE_TEST_POLICY(exec), algo, checker, args...);
        process_data_in_in_out(max_n, 0, r_size / 2, r_size / 4, CLONE_TEST_POLICY(exec), algo, checker, args...);
        process_data_in_in_out(max_n, r_size / 2, 0, r_size / 4, CLONE_TEST_POLICY(exec), algo, checker, args...);
    }
private:

    template <std::size_t InIdx = 0, typename Ret, typename Begin>
    auto ret_in_val(Ret&& ret, Begin&& begin)
    {
        if constexpr (check_in<Ret>)
        {
            static_assert(InIdx == 0, "InIdx should be 0 for check_in");
            return std::distance(begin, ret.in);
        }
        else if constexpr (check_in1<Ret> && InIdx == 1)
        {
            return std::distance(begin, ret.in1);
        }
        else if constexpr (check_in2<Ret> && InIdx == 2)
        {
            return std::distance(begin, ret.in2);
        }
        else if constexpr (is_iterator<Ret>)
        {
            static_assert(InIdx == 0, "InIdx should be 0 for iterator return type");
            return std::distance(begin, ret);
        }
        else if constexpr (is_range<Ret>)
        {
            static_assert(InIdx == 0, "InIdx should be 0 for range return type");
            return std::pair{std::distance(begin, ret.begin()), std::ranges::distance(ret.begin(), ret.end())};
        }
        else if constexpr(check_minmax<Ret>)
        {
            static_assert(InIdx == 0, "InIdx should be 0 for minmax return type");

            const auto& [first, second] = ret;
            if constexpr(std::random_access_iterator<std::remove_cvref_t<decltype(first)>>)
                return std::pair{std::distance(begin, first), std::ranges::distance(begin, second)};
            else
                return std::pair{first, second};
        }
        else
        {
            static_assert(InIdx == 0, "InIdx should be 0 for fundamental return type");
            return ret;
        }
    }

    template<typename Ret, typename Begin>
    auto ret_out_val(Ret&& ret, Begin&& begin)
    {
        if constexpr (check_out<Ret>)
            return std::distance(begin, ret.out);
        else if constexpr (is_iterator<Ret>)
            return std::distance(begin, ret);
        else if constexpr (check_in2<Ret>)
            return std::distance(begin, ret.in2);
        else if constexpr(is_range<Ret>)
            return std::pair{std::distance(begin, ret.begin()), std::ranges::distance(ret.begin(), ret.end())};
        else
            return ret;
    }
};

template<typename T, typename ViewType>
struct host_subrange_impl
{
    using value_type = T;

    static_assert(std::is_trivially_copyable_v<T>,
        "Memory initialization within the class relies on trivially copyability of the type T");

    using type = ViewType;
    ViewType view;
    T* mem = nullptr;

    std::allocator<T> alloc;

    template<typename Policy>
    host_subrange_impl(Policy&&, T* data, int n): view(data, data + n) {}

    template<typename Policy, typename DataGen>
    host_subrange_impl(Policy&&, int n, DataGen gen)
    {
        mem = alloc.allocate(n);
        view = ViewType(mem, mem + n);
        for(int i = 0; i < n; ++i)
            view[i] = gen(i);
    }
    ViewType& operator()()
    {
        return view;
    }
    ~host_subrange_impl()
    {
        if(mem)
            alloc.deallocate(mem, view.size());
    }
};

template<typename T>
using  host_subrange = host_subrange_impl<T, std::ranges::subrange<T*>>;

#if TEST_CPP20_SPAN_PRESENT
template<typename T>
using  host_span = host_subrange_impl<T, std::span<T>>;
#endif

template<typename T>
struct host_vector
{
    using value_type = T;

    using type = std::vector<T>;
    type vec;
    T* p = nullptr;

    template<typename Policy>
    host_vector(Policy&&, T* data, int n): vec(data, data + n), p(data) {}

    template<typename Policy, typename DataGen>
    host_vector(Policy&&, int n, DataGen gen): vec(n)
    {
        for(int i = 0; i < n; ++i)
            vec[i] = gen(i);
    }
    type& operator()()
    {
        return vec;
    }
    ~host_vector()
    {
        if(p)
            std::copy_n(vec.begin(), vec.size(), p);
    }
};

#if TEST_DPCPP_BACKEND_PRESENT
template<typename T>
struct usm_vector
{
    using value_type = T;

    using shared_allocator = sycl::usm_allocator<T, sycl::usm::alloc::shared>;
    using type = std::vector<T, shared_allocator>;

    std::vector<T, shared_allocator> vec;
    T* p = nullptr;

    template<typename Policy>
    usm_vector(Policy&& exec, T* data, int n): vec(data, data + n, shared_allocator(exec.queue())), p(data)
    {
        assert(vec.size() == n);
    }
    template<typename Policy, typename DataGen>
    usm_vector(Policy&& exec, int n, DataGen gen): vec(n, shared_allocator(exec.queue()))
    {
        for(int i = 0; i < n; ++i)
            vec[i] = gen(i);
    }
    type& operator()()
    {
        return vec;
    }
    ~usm_vector()
    {
        if(p)
            std::copy_n(vec.begin(), vec.size(), p);
    }
};

template<typename T, typename ViewType>
struct usm_subrange_impl
{
    using value_type = T;

    static_assert(std::is_trivially_copyable_v<T>,
        "Memory initialization within the class relies on trivially copyability of the type T");

    using shared_allocator = sycl::usm_allocator<T, sycl::usm::alloc::shared>;
    using type = ViewType;

    shared_allocator alloc;
    T* p = nullptr;
    ViewType view;

    template<typename Policy>
    usm_subrange_impl(Policy&& exec, T* data, int n): alloc(exec.queue()), p(data)
    {
        auto mem = alloc.allocate(n);
        view = ViewType(mem, mem + n);
        std::copy_n(data, n, view.data());
    }
    template<typename Policy, typename DataGen>
    usm_subrange_impl(Policy&& exec, int n, DataGen gen): alloc(exec.queue())
    {
        auto mem = alloc.allocate(n);
        view = ViewType(mem, mem + n);
        for(int i = 0; i < n; ++i)
            view[i] = gen(i);
    }

    ViewType& operator()()
    {
        return view;
    }

    ~usm_subrange_impl()
    {
        if(p)
            std::copy_n(view.data(), view.size(), p);
        alloc.deallocate(view.data(), view.size());
    }
};

template<typename T>
using  usm_subrange = usm_subrange_impl<T, std::ranges::subrange<T*>>;

#if TEST_CPP20_SPAN_PRESENT
template<typename T>
using  usm_span = usm_subrange_impl<T, std::span<T>>;
#endif

#endif // TEST_DPCPP_BACKEND_PRESENT

struct subrange_view_fo
{
    template <typename T>
    auto operator()(T&& v) const
    {
        return std::ranges::subrange(v);
    }
};

#if TEST_CPP20_SPAN_PRESENT
struct span_view_fo
{
    template <typename T>
    auto operator()(T&& v) const
    {
        return std::span(v);
    }
};
#endif

// TODO remove after implementation range-based set operations for bounded output range with hetero policies
template <TestDataMode mode>
struct ResolveTestDataModeForHeteroPolicy
{
    static constexpr TestDataMode res_mode = mode;
};

template<int call_id = 0, typename T = int, TestDataMode mode = data_in, typename DataGen1 = std::identity,
         typename DataGen2 = decltype(data_gen2_default)>
struct test_range_algo
{
    const int n_serial = small_size;
    const int n_parallel = medium_size/8; // 16K
#if TEST_DPCPP_BACKEND_PRESENT
    const int n_device = medium_size;
#endif

    test_range_algo() = default;

    // Mode with a uniform number of elements for each policy type
#if TEST_DPCPP_BACKEND_PRESENT
    test_range_algo(int n) : n_serial(n), n_parallel(n), n_device(n) {}
#else
    test_range_algo(int n) : n_serial(n), n_parallel(n) {}
#endif

    // Mode that tests different policy types with different sizes.
    // Serial (seq/unseq), parallel (par/par_unseq), and device policies
    // specialize algorithms for different number of elements, which this mode covers.
#if TEST_DPCPP_BACKEND_PRESENT
    test_range_algo(std::array<int, 3> sizes) : n_serial(sizes[0]), n_parallel(sizes[1]), n_device(sizes[2]) {}
#else
    test_range_algo(std::array<int, 2> sizes) : n_serial(sizes[0]), n_parallel(sizes[1]) {}
#endif

    void test_view_host(auto view, auto algo, auto& checker, auto... args)
    {
        test<T, host_subrange<T>, mode, DataGen1, DataGen2>{}.host_policies(n_serial, n_parallel, algo, checker, view, std::identity{}, args...);
    }

#if TEST_DPCPP_BACKEND_PRESENT
    template <typename Policy>
    void test_view_hetero(Policy&& exec, auto view, auto algo, auto& checker, auto... args)
    {
        test<T, usm_subrange<T>, mode, DataGen1, DataGen2>{}(n_device, CLONE_TEST_POLICY_IDX(exec, call_id), algo, checker, view, std::identity{}, args...);
    }
#endif //TEST_DPCPP_BACKEND_PRESENT

    void
    test_range_algo_impl_host(auto algo, auto& checker, auto... args)
    {
        auto subrange_view = subrange_view_fo{};
#if TEST_CPP20_SPAN_PRESENT
        auto span_view = span_view_fo{};
#endif

        test<T, host_vector<T>,   mode, DataGen1, DataGen2>{}.host_policies(n_serial, n_parallel, algo, checker, std::identity{},  std::identity{}, args...);
        test<T, host_vector<T>,   mode, DataGen1, DataGen2>{}.host_policies(n_serial, n_parallel, algo, checker, subrange_view,    std::identity{}, args...);
        test<T, host_vector<T>,   mode, DataGen1, DataGen2>{}.host_policies(n_serial, n_parallel, algo, checker, std::views::all,  std::identity{}, args...);
        test<T, host_subrange<T>, mode, DataGen1, DataGen2>{}.host_policies(n_serial, n_parallel, algo, checker, std::views::all,  std::identity{}, args...);
#if TEST_CPP20_SPAN_PRESENT
        test<T, host_vector<T>,   mode, DataGen1, DataGen2>{}.host_policies(n_serial, n_parallel, algo, checker, span_view,        std::identity{}, args...);
        test<T, host_span<T>,     mode, DataGen1, DataGen2>{}.host_policies(n_serial, n_parallel, algo, checker, std::views::all,  std::identity{}, args...);
#endif
    }

#if TEST_DPCPP_BACKEND_PRESENT
    template <typename Policy>
    void test_range_algo_impl_hetero(Policy&& exec, auto algo, auto& checker, auto... args)
    {
        auto subrange_view = subrange_view_fo{};
#if TEST_CPP20_SPAN_PRESENT
        auto span_view = span_view_fo{};
#endif

        //Skip the cases with pointer-to-function and hetero policy because pointer-to-function is not supported within kernel code.
        if constexpr(!std::disjunction_v<std::is_member_function_pointer<decltype(args)>...>)
        {
#if _PSTL_LAMBDA_PTR_TO_MEMBER_WINDOWS_BROKEN
            if constexpr(!std::disjunction_v<std::is_member_pointer<decltype(args)>...>)
#endif
            {
                // TODO remove after implementation range-based set operations for bounded output range with hetero policies
                constexpr TestDataMode resHeteroMode = ResolveTestDataModeForHeteroPolicy<mode>::res_mode;

                test<T, usm_vector<T>,   resHeteroMode, DataGen1, DataGen2>{}(n_device, CLONE_TEST_POLICY_IDX(exec, call_id + 10), algo, checker, subrange_view,   subrange_view,   args...);
                test<T, usm_subrange<T>, resHeteroMode, DataGen1, DataGen2>{}(n_device, CLONE_TEST_POLICY_IDX(exec, call_id + 30), algo, checker, std::identity{}, std::identity{}, args...);
#if TEST_CPP20_SPAN_PRESENT
                test<T, usm_vector<T>,   resHeteroMode, DataGen1, DataGen2>{}(n_device, CLONE_TEST_POLICY_IDX(exec, call_id + 20), algo, checker, span_view,       subrange_view,   args...);
                test<T, usm_span<T>,     resHeteroMode, DataGen1, DataGen2>{}(n_device, CLONE_TEST_POLICY_IDX(exec, call_id + 40), algo, checker, std::identity{}, std::identity{}, args...);
#endif
            }
        }
    }
#endif // TEST_DPCPP_BACKEND_PRESENT

    void
    operator()(auto algo, auto& checker, auto... args)
    {
        test_range_algo_impl_host(algo, checker, args...);

#if TEST_DPCPP_BACKEND_PRESENT
        // TODO remove after implementation range-based set operations for bounded output range with hetero policies
        if constexpr (!skip_test_for_hetero_policy<decltype(algo)>)
        {
            auto policy = TestUtils::get_dpcpp_test_policy();
            test_range_algo_impl_hetero(policy, algo, checker, args...);

#if TEST_CHECK_COMPILATION_WITH_DIFF_POLICY_VAL_CATEGORY
            TestUtils::check_compilation(policy, [&](auto&& policy) { test_range_algo_impl_hetero(policy, algo, checker, args...); });
#endif
        }
#endif // TEST_DPCPP_BACKEND_PRESENT
    }
};

// @param TSize size - 1-based size of the range, zero for empty range
// @param TIndex index - 0-based index of item in range to calculate remaining space for
// @return TSize - remaining space in range starting from index
template <typename TSize, typename TIndex>
TSize
eval_remaining_space(TSize size, TIndex index)
{
    if (size >= index)
        return size - index;

    return 0;
}

template <typename TSize1, typename TIndex1, typename TSize2, typename TIndex2>
std::common_type_t<TSize1, TSize2>
eval_remaining_space_min(TSize1 size1, TIndex1 index1, TSize2 size2, TIndex2 index2)
{
    const TSize1 space1 = eval_remaining_space(size1, index1);
    const TSize2 space2 = eval_remaining_space(size2, index2);

    return std::min<std::common_type_t<TSize1, TSize2>>(space1, space2);
}

} //namespace test_std_ranges

#endif //_ENABLE_STD_RANGES_TESTING

#endif //_STD_RANGES_TEST_H
