// SPDX-FileCopyrightText: 2006-2024 Knut Reinert & Freie Universität Berlin
// SPDX-FileCopyrightText: 2016-2024 Knut Reinert & MPI für molekulare Genetik
// SPDX-License-Identifier: BSD-3-Clause

#include <gtest/gtest.h>

#include <algorithm>
#include <type_traits>

#include <seqan3/search/fm_index/concept.hpp>
#include <seqan3/test/cereal.hpp>
#include <seqan3/test/expect_range_eq.hpp>
#include <seqan3/utility/views/slice.hpp>

#include "../helper.hpp"

using sdsl_byte_index_type = sdsl::csa_wt<
    sdsl::wt_blcd<sdsl::bit_vector, sdsl::rank_support_v<>, sdsl::select_support_scan<>, sdsl::select_support_scan<0>>,
    16,
    10'000'000,
    sdsl::sa_order_sa_sampling<>,
    sdsl::isa_sampling<>,
    sdsl::byte_alphabet>;

template <typename T>
struct fm_index_cursor_collection_test;

TYPED_TEST_SUITE_P(fm_index_cursor_collection_test);

TYPED_TEST_P(fm_index_cursor_collection_test, ctr)
{
    typename TypeParam::index_type fm{this->text_col1}; // {"ACGACG", "ACGACG"}

    // custom constructor
    TypeParam it0{fm};
    EXPECT_EQ(it0.query_length(), 0u);
    if constexpr (!TestFixture::is_bi_fm_index)
    {
        EXPECT_TRUE(it0.suffix_array_interval() == (seqan3::suffix_array_interval{0u, fm.size()}));
    }
    EXPECT_EQ(it0.locate().size(), fm.size());

    // default construction (does not initialize the cursor)
    [[maybe_unused]] TypeParam it1;

    // copy construction
    TypeParam it2{it0};
    EXPECT_EQ(it0, it2);

    // copy assignment
    TypeParam it3 = it0;
    EXPECT_EQ(it0, it3);

    // move construction
    TypeParam it4{std::move(it0)};
    EXPECT_EQ(it0, it4);

    // move assigment
    TypeParam it5 = std::move(it0);
    EXPECT_EQ(it0, it5);
}

TYPED_TEST_P(fm_index_cursor_collection_test, begin)
{
    typename TypeParam::index_type fm{this->text_col1}; // {"ACGACG", "ACGACG"}

    // begin
    TypeParam it(fm);
    EXPECT_EQ(seqan3::uniquify(it.locate()),
              (std::vector<std::pair<uint64_t, uint64_t>>{{0, 0},
                                                          {0, 1},
                                                          {0, 2},
                                                          {0, 3},
                                                          {0, 4},
                                                          {0, 5},
                                                          {0, 6},
                                                          {1, 0},
                                                          {1, 1},
                                                          {1, 2},
                                                          {1, 3},
                                                          {1, 4},
                                                          {1, 5},
                                                          {1, 6}}));
    // one sentinel position included
    EXPECT_EQ(it.query_length(), 0u);
    if constexpr (!TestFixture::is_bi_fm_index)
    {
        EXPECT_TRUE(it.suffix_array_interval() == (seqan3::suffix_array_interval{0u, fm.size()}));
    }
    EXPECT_EQ(it.count(), 14u);
}

TYPED_TEST_P(fm_index_cursor_collection_test, extend_right_range)
{
    typename TypeParam::index_type fm{this->text_col2}; // {"ACGACG", "TGCGATCGA"}

    // successful extend_right(range)
    TypeParam it(fm);
    EXPECT_TRUE(it.extend_right(seqan3::views::slice(this->text1, 1, 3))); // "CG"
    EXPECT_EQ(seqan3::uniquify(it.locate()),
              (std::vector<std::pair<uint64_t, uint64_t>>{{0, 1}, {0, 4}, {1, 2}, {1, 6}}));
    EXPECT_EQ(it.query_length(), 2u);
    if constexpr (!TestFixture::is_bi_fm_index)
    {
        EXPECT_TRUE(it.suffix_array_interval() == (seqan3::suffix_array_interval{9u, 13u}));
    }
    EXPECT_EQ(it.count(), 4u);

    EXPECT_TRUE(it.extend_right(seqan3::views::slice(this->text1, 0, 1))); // "A"
    EXPECT_EQ(it.locate(), (std::vector<std::pair<uint64_t, uint64_t>>{{0, 1}, {1, 2}, {1, 6}}));
    EXPECT_EQ(it.query_length(), 3u);
    if constexpr (!TestFixture::is_bi_fm_index)
    {
        EXPECT_TRUE(it.suffix_array_interval() == (seqan3::suffix_array_interval{2u, 5u}));
    }
    EXPECT_EQ(it.count(), 3u);

    // unsuccessful extend_right(range), it remains untouched
    TypeParam it_cpy = it;
    EXPECT_FALSE(it.extend_right(seqan3::views::slice(this->text1, 0, 1))); // "A"
    EXPECT_EQ(it, it_cpy);

    // extend_right(empty range)
    it_cpy = it;
    EXPECT_TRUE(it.extend_right(this->empty_text));
    EXPECT_EQ(it, it_cpy);
}

TYPED_TEST_P(fm_index_cursor_collection_test, extend_right_range_empty_text)
{
    typename TypeParam::index_type fm{this->text_col3}; // {"ACGACG", "", "", "TGCGATCGA"}

    // successful extend_right(range)
    TypeParam it(fm);
    EXPECT_TRUE(it.extend_right(seqan3::views::slice(this->text1, 1, 3))); // "CG"
    EXPECT_EQ(seqan3::uniquify(it.locate()),
              (std::vector<std::pair<uint64_t, uint64_t>>{{0, 1}, {0, 4}, {3, 2}, {3, 6}}));
    EXPECT_EQ(it.query_length(), 2u);
    if constexpr (!TestFixture::is_bi_fm_index)
    {
        EXPECT_TRUE(it.suffix_array_interval() == (seqan3::suffix_array_interval{9u, 13u}));
    }
    EXPECT_EQ(it.count(), 4u);

    EXPECT_TRUE(it.extend_right(seqan3::views::slice(this->text1, 0, 1))); // "A"
    EXPECT_EQ(it.locate(), (std::vector<std::pair<uint64_t, uint64_t>>{{0, 1}, {3, 2}, {3, 6}}));
    EXPECT_EQ(it.query_length(), 3u);
    if constexpr (!TestFixture::is_bi_fm_index)
    {
        EXPECT_TRUE(it.suffix_array_interval() == (seqan3::suffix_array_interval{2u, 5u}));
    }
    EXPECT_EQ(it.count(), 3u);

    // unsuccessful extend_right(range), it remains untouched
    TypeParam it_cpy = it;
    EXPECT_FALSE(it.extend_right(seqan3::views::slice(this->text1, 0, 1))); // "A"
    EXPECT_EQ(it, it_cpy);

    // extend_right(empty range)
    it_cpy = it;
    EXPECT_TRUE(it.extend_right(this->empty_text));
    EXPECT_EQ(it, it_cpy);
}

// TODO: doesn't work with the current structure of typed tests
// TYPED_TEST_P(fm_index_cursor_collection_test, extend_right_convertible_range)
// {
//     const std::vector<seqan3::dna4_vector> text{"ANGACGNN"_dna5};
//     typename TypeParam::index_type fm{text};
//
//     // successful extend_right(range) using a different alphabet
//     TypeParam it(fm);
//     EXPECT_TRUE(it.extend_right("GA"_dna4));
//     EXPECT_EQ(it.locate(), (std::vector<uint64_t>{2}));
//     EXPECT_EQ(it.query_length(), 2);
// }

TYPED_TEST_P(fm_index_cursor_collection_test, extend_right_char)
{
    typename TypeParam::index_type fm{this->text_col2}; // {"ACGACG", "TGCGATCGA"}

    // successful extend_right(char)
    TypeParam it(fm);
    EXPECT_TRUE(it.extend_right(seqan3::views::slice(this->text1, 0, 1))); // "A"
    EXPECT_EQ(seqan3::uniquify(it.locate()),
              (std::vector<std::pair<uint64_t, uint64_t>>{{0, 0}, {0, 3}, {1, 4}, {1, 8}}));
    EXPECT_EQ(it.query_length(), 1u);
    if constexpr (!TestFixture::is_bi_fm_index)
    {
        EXPECT_TRUE(it.suffix_array_interval() == (seqan3::suffix_array_interval{1u, 5u}));
    }

    EXPECT_TRUE(it.extend_right(seqan3::views::slice(this->text1, 1, 2))); // "C"
    EXPECT_EQ(seqan3::uniquify(it.locate()), (std::vector<std::pair<uint64_t, uint64_t>>{{0, 0}, {0, 3}}));
    EXPECT_EQ(it.query_length(), 2u);
    if constexpr (!TestFixture::is_bi_fm_index)
    {
        EXPECT_TRUE(it.suffix_array_interval() == (seqan3::suffix_array_interval{5u, 7u}));
    }

    // unsuccessful extend_right(char), it remains untouched
    TypeParam it_cpy = it;
    EXPECT_FALSE(it.extend_right(seqan3::views::slice(this->text1, 1, 2))); // "C"
    EXPECT_EQ(it, it_cpy);
}

// TODO: doesn't work with the current structure of typed tests
// TYPED_TEST_P(fm_index_cursor_collection_test, extend_right_convertible_char)
// {
//     const std::vector<seqan3::dna4_vector> text{"ANGACGNN"_dna5};
//     typename TypeParam::index_type fm{text};
//
//     // successful extend_right(char) using a different alphabet
//     TypeParam it(fm);
//     EXPECT_TRUE(it.extend_right(seqan3::views::slice(this->text1, 0, 1)));    // "A"
//     EXPECT_EQ(seqan3::uniquify(it.locate()), (std::vector<uint64_t>{0, 3}));
//     EXPECT_EQ(it.query_length(), 1);
// }

TYPED_TEST_P(fm_index_cursor_collection_test, extend_right_range_and_cycle)
{
    typename TypeParam::index_type fm{this->text_col4}; // {"ACGAACGC", "TACGATCGA"}

    // successful extend_right() and cycle_back()
    TypeParam it(fm);
    EXPECT_TRUE(it.extend_right(seqan3::views::slice(this->text1, 0, 4))); // "ACGA"
    EXPECT_EQ(it.locate(), (std::vector<std::pair<uint64_t, uint64_t>>{{0, 0}, {1, 1}}));
    EXPECT_EQ(it.query_length(), 4u);
    if constexpr (!TestFixture::is_bi_fm_index)
    {
        EXPECT_TRUE(it.suffix_array_interval() == (seqan3::suffix_array_interval{3u, 5u}));
    }

    EXPECT_TRUE(it.cycle_back());
    EXPECT_EQ(it.locate(), (std::vector<std::pair<uint64_t, uint64_t>>{{0, 4}}));
    EXPECT_EQ(it.query_length(), 4u);
    if constexpr (!TestFixture::is_bi_fm_index)
    {
        EXPECT_TRUE(it.suffix_array_interval() == (seqan3::suffix_array_interval{10u, 11u}));
    }
}

TYPED_TEST_P(fm_index_cursor_collection_test, extend_right_char_and_cycle)
{
    typename TypeParam::index_type fm{this->text_col5}; // {"ACGAACGC"_dna4, "TGCGATCGA"_dna4};

    // successful extend_right() and cycle_back()
    TypeParam it(fm);
    EXPECT_TRUE(it.extend_right(seqan3::views::slice(this->text1, 0, 1))); // "A"
    EXPECT_EQ(seqan3::uniquify(it.locate()),
              (std::vector<std::pair<uint64_t, uint64_t>>{{0, 0}, {0, 3}, {0, 4}, {1, 4}, {1, 8}}));
    EXPECT_EQ(it.query_length(), 1u);
    if constexpr (!TestFixture::is_bi_fm_index)
    {
        EXPECT_TRUE(it.suffix_array_interval() == (seqan3::suffix_array_interval{1u, 6u}));
    }

    EXPECT_TRUE(it.cycle_back());
    EXPECT_EQ(seqan3::uniquify(it.locate()),
              (std::vector<std::pair<uint64_t, uint64_t>>{{0, 1}, {0, 5}, {0, 7}, {1, 2}, {1, 6}}));
    EXPECT_EQ(it.query_length(), 1u);
    if constexpr (!TestFixture::is_bi_fm_index)
    {
        EXPECT_TRUE(it.suffix_array_interval() == (seqan3::suffix_array_interval{6u, 11u}));
    }
}

TYPED_TEST_P(fm_index_cursor_collection_test, extend_right_and_cycle)
{
    typename TypeParam::index_type fm{this->text_col2}; // {"ACGACG", "TGCGATCGA"}

    // successful extend_right() and cycle_back()
    TypeParam it(fm);
    EXPECT_TRUE(it.extend_right());
    EXPECT_EQ(seqan3::uniquify(it.locate()),
              (std::vector<std::pair<uint64_t, uint64_t>>{{0, 0}, {0, 3}, {1, 4}, {1, 8}}));
    EXPECT_EQ(it.query_length(), 1u);
    if constexpr (!TestFixture::is_bi_fm_index)
    {
        EXPECT_TRUE(it.suffix_array_interval() == (seqan3::suffix_array_interval{1u, 5u}));
    }

    EXPECT_TRUE(it.cycle_back());
    EXPECT_EQ(seqan3::uniquify(it.locate()),
              (std::vector<std::pair<uint64_t, uint64_t>>{{0, 1}, {0, 4}, {1, 2}, {1, 6}}));
    EXPECT_EQ(it.query_length(), 1u);
    if constexpr (!TestFixture::is_bi_fm_index)
    {
        EXPECT_TRUE(it.suffix_array_interval() == (seqan3::suffix_array_interval{5u, 9u}));
    }
    EXPECT_TRUE(it.extend_right());
    EXPECT_EQ(seqan3::uniquify(it.locate()),
              (std::vector<std::pair<uint64_t, uint64_t>>{{0, 1}, {0, 4}, {1, 2}, {1, 6}}));
    EXPECT_EQ(it.query_length(), 2u);
    if constexpr (!TestFixture::is_bi_fm_index)
    {
        EXPECT_TRUE(it.suffix_array_interval() == (seqan3::suffix_array_interval{9u, 13u}));
    }

    // unsuccessful cycle_back(), it remains untouched
    TypeParam it_cpy = it;
    EXPECT_FALSE(it.cycle_back());
    EXPECT_EQ(it, it_cpy);

    // unsuccessful extend_right(), it remains untouched
    it = TypeParam(fm);
    EXPECT_TRUE(it.extend_right(this->text1)); // "ACGACG"
    it_cpy = it;
    EXPECT_FALSE(it.extend_right());
    EXPECT_EQ(it, it_cpy);

    // cycle_back() cannot be called on the root node
    it = TypeParam(fm);
#ifndef NDEBUG
    EXPECT_DEATH(it.cycle_back(), "");
#endif
    EXPECT_EQ(it, TypeParam(fm));
}

TYPED_TEST_P(fm_index_cursor_collection_test, query)
{
    typename TypeParam::index_type fm{this->text_col2}; // {"ACGACG", "TGCGATCGA"}

    // query()
    TypeParam it(fm);
    EXPECT_TRUE(it.extend_right(seqan3::views::slice(this->text1, 0, 3)));                    // "ACG"
    EXPECT_RANGE_EQ(it.path_label(this->text_col2), seqan3::views::slice(this->text1, 0, 3)); // "ACG"
}

TYPED_TEST_P(fm_index_cursor_collection_test, last_rank)
{
    typename TypeParam::index_type fm{this->text_col2}; // {"ACGACG", "TGCGATCGA"}

    // last_rank()
    TypeParam it(fm);
    EXPECT_TRUE(it.extend_right(seqan3::views::slice(this->text1, 0, 3))); // "ACG"
    bool a = it.last_rank() == seqan3::to_rank(this->text1[2]);            // 'G'
    EXPECT_TRUE(a);
}

TYPED_TEST_P(fm_index_cursor_collection_test, incomplete_alphabet)
{
    using alphabet_type = typename TestFixture::alphabet_type;

    // search a char that does not occur in the text (higher rank than largest char occurring in text)
    {
        typename TypeParam::index_type fm{this->text_col1}; // {"ACGACG", "ACGACG"}
        TypeParam it = TypeParam(fm);
        EXPECT_FALSE(it.extend_right(seqan3::assign_char_to('T', alphabet_type{}))); // 'T'
        EXPECT_EQ(it, TypeParam(fm));
    }

    // search a char that does not occur in the text (smaller rank than smallest char occurring in text)
    {
        typename TypeParam::index_type fm{this->text_col6}; // {"CGTCGT", "CGTCGT"}
        TypeParam it = TypeParam(fm);
        EXPECT_FALSE(it.extend_right(seqan3::assign_rank_to(0, alphabet_type{}))); // 'A' or letter with smallest rank
        EXPECT_EQ(it, TypeParam(fm));
    }

    // search a char that does not occur in the text
    // (some rank that is neither the smallest nor the highest occurring in text)
    {
        typename TypeParam::index_type fm{this->text_col7}; // {"ATATAT", "ATATAT"}

        TypeParam it = TypeParam(fm);
        // get rank which is neither the smallest nor the highest:
        uint8_t middle_rank = std::round((seqan3::to_rank(this->text4[1]) + // 'T' and
                                          seqan3::to_rank(this->text4[0]))
                                         / 2);                                               // 'A'
        EXPECT_FALSE(it.extend_right(seqan3::assign_rank_to(middle_rank, alphabet_type{}))); // 'C'
        EXPECT_FALSE(it.extend_right(this->text1[2]));                                       // 'G'
        EXPECT_FALSE(it.extend_right(seqan3::views::slice(this->text7, 0, 4)));              // "ACGT"
        EXPECT_FALSE(it.extend_right(seqan3::views::slice(this->text1, 2, 3)));              // "G"
        EXPECT_EQ(it, TypeParam(fm));

        EXPECT_TRUE(it.extend_right(this->text4[0])); // 'A'
        EXPECT_TRUE(it.cycle_back());
        EXPECT_RANGE_EQ(it.path_label(this->text_col7), seqan3::views::slice(this->text4, 1, 2)); // "T"
    }
}

TYPED_TEST_P(fm_index_cursor_collection_test, lazy_locate)
{
    typename TypeParam::index_type fm{this->text_col8}; // {"ACGTACGT", "TGCGATACGA"}

    TypeParam it = TypeParam(fm);
    it.extend_right(seqan3::views::slice(this->text1, 0, 3)); // "ACG"

    EXPECT_RANGE_EQ(it.locate(), it.lazy_locate());
}

TYPED_TEST_P(fm_index_cursor_collection_test, extend_const_char_pointer)
{
    using alphabet_type = typename TestFixture::alphabet_type;

    // test case for https://github.com/seqan/seqan3/issues/1473
    if constexpr (std::is_same<alphabet_type, char>::value)
    {
        typename TypeParam::index_type fm{this->text_col1}; // {"ACGACG", "ACGACG"}
        char const * cg = "CG";

        // extend_right()
        TypeParam it1 = TypeParam(fm);
        TypeParam it2 = TypeParam(fm);

        it1.extend_right(cg);
        it2.extend_right(seqan3::views::slice(this->text1, 1, 3)); // "CG"

        EXPECT_RANGE_EQ(it1.locate(), it2.locate()); // [(0,1),(0,4),(1,4),(1,1)]
    }
}

TYPED_TEST_P(fm_index_cursor_collection_test, serialisation)
{
    typename TypeParam::index_type fm{this->text_col4};

    TypeParam it(fm);
    it.extend_right(seqan3::views::slice(this->text1, 0, 3));

    seqan3::test::do_serialisation(it);
}

REGISTER_TYPED_TEST_SUITE_P(fm_index_cursor_collection_test,
                            ctr,
                            begin,
                            extend_right_range,
                            extend_right_range_empty_text,
                            extend_right_char,
                            extend_right_range_and_cycle,
                            extend_right_char_and_cycle,
                            extend_right_and_cycle,
                            query,
                            last_rank,
                            incomplete_alphabet,
                            lazy_locate,
                            extend_const_char_pointer,
                            serialisation);
