#include <catch.hpp>

#include <pub_intl.hh>

#include <memory>
#include <type_traits>
#include <vector>

namespace
{
    static_assert(
        !std::is_copy_constructible< pub_sub::detail::MessageBlock >::value,
        "MessageBlocks must not be copyable" );
    static_assert(
        !std::is_move_constructible< pub_sub::detail::MessageBlock >::value,
        "MessageBlocks must not be movable" );

    pub_sub::Message
    make_msg( unsigned char fill, std::size_t length = 8 )
    {
        pub_sub::DataPtr data( new unsigned char[ length ] );
        std::fill( data.get( ), data.get( ) + length, fill );
        return pub_sub::Message( std::move( data ), length );
    }
} // namespace

TEST_CASE( "A MessageHistory honours its capacity and evicts in order" )
{
    pub_sub::detail::MessageHistory history( 3 );
    REQUIRE( history.empty( ) );
    REQUIRE( history.size( ) == 0 );
    REQUIRE( history.capacity( ) == 3 );
    REQUIRE( history.newest( ) == nullptr );

    for ( pub_sub::KeyType key = 1; key <= 3; ++key )
    {
        auto block = history.push(
            key, make_msg( static_cast< unsigned char >( key ) ) );
        REQUIRE( block != nullptr );
        REQUIRE( block->key( ) == key );
        REQUIRE( history.size( ) == key );
        REQUIRE( history.newest( ) == block );
    }
    REQUIRE( !history.empty( ) );

    /* pushing past capacity evicts the oldest entries */
    history.push( 4, make_msg( 4 ) );
    history.push( 5, make_msg( 5 ) );
    REQUIRE( history.size( ) == 3 );
    REQUIRE( history.find( 1 ) == nullptr );
    REQUIRE( history.find( 2 ) == nullptr );
    REQUIRE( history.find( 3 ) != nullptr );
    REQUIRE( history.newest( )->key( ) == 5 );
}

TEST_CASE( "MessageHistory lookups work at the boundaries" )
{
    pub_sub::detail::MessageHistory history( 4 );

    REQUIRE( history.first_after( 0 ) == nullptr );
    REQUIRE( history.find( 1 ) == nullptr );

    /* keys need not be contiguous, only increasing */
    history.push( 2, make_msg( 2 ) );
    history.push( 4, make_msg( 4 ) );
    history.push( 6, make_msg( 6 ) );

    REQUIRE( history.first_after( 0 )->key( ) == 2 );
    REQUIRE( history.first_after( 1 )->key( ) == 2 );
    REQUIRE( history.first_after( 2 )->key( ) == 4 );
    REQUIRE( history.first_after( 3 )->key( ) == 4 );
    REQUIRE( history.first_after( 5 )->key( ) == 6 );
    REQUIRE( history.first_after( 6 ) == nullptr );
    REQUIRE( history.first_after( pub_sub::NEXT_PUB_MSG( ) ) == nullptr );

    REQUIRE( history.find( 2 )->key( ) == 2 );
    REQUIRE( history.find( 3 ) == nullptr );
    REQUIRE( history.find( 6 )->key( ) == 6 );
}

TEST_CASE( "MessageHistory tracks gaps after eviction" )
{
    pub_sub::detail::MessageHistory history( 3 );

    /* nothing evicted yet, so there are no gaps */
    REQUIRE( !history.has_gap_after( 0 ) );
    history.push( 1, make_msg( 1 ) );
    history.push( 2, make_msg( 2 ) );
    history.push( 3, make_msg( 3 ) );
    REQUIRE( !history.has_gap_after( 0 ) );
    REQUIRE( !history.has_gap_after( 3 ) );

    /* evict key 1; only consumers behind key 1 have a gap */
    history.push( 4, make_msg( 4 ) );
    REQUIRE( history.has_gap_after( 0 ) );
    REQUIRE( !history.has_gap_after( 1 ) );
    REQUIRE( !history.has_gap_after( 2 ) );

    history.push( 5, make_msg( 5 ) );
    REQUIRE( history.has_gap_after( 1 ) );
    REQUIRE( !history.has_gap_after( 2 ) );
}

TEST_CASE( "Holding a MessageBlock pins exactly that block" )
{
    const std::size_t depth = 4;

    pub_sub::detail::MessageHistory                 history( depth );
    std::vector< std::weak_ptr< unsigned char[] > > payloads;
    pub_sub::detail::MessageBlockPtr                retained{ nullptr };
    pub_sub::DataPtr                                retained_payload{ nullptr };

    for ( pub_sub::KeyType key = 1; key <= depth; ++key )
    {
        pub_sub::DataPtr data( new unsigned char[ 16 ] );
        std::fill( data.get( ),
                   data.get( ) + 16,
                   static_cast< unsigned char >( key ) );
        payloads.emplace_back( data );
        auto block = history.push( key, pub_sub::Message( data, 16 ) );
        if ( key == 2 )
        {
            retained = block;
            retained_payload = std::move( data );
        }
    }

    /* push until every original block has been evicted */
    for ( pub_sub::KeyType key = depth + 1; key <= 2 * depth; ++key )
    {
        history.push( key, make_msg( static_cast< unsigned char >( key ) ) );
    }

    /* the retained block keeps only its own payload alive */
    for ( std::size_t i = 0; i < payloads.size( ); ++i )
    {
        if ( i == 1 )
        {
            /* one reference in the test, one in the retained block */
            REQUIRE( payloads[ i ].use_count( ) == 2 );
        }
        else
        {
            REQUIRE( payloads[ i ].expired( ) );
        }
    }

    retained.reset( );
    /* only the test's reference remains */
    REQUIRE( payloads[ 1 ].use_count( ) == 1 );
    retained_payload = nullptr;
    REQUIRE( payloads[ 1 ].expired( ) );
}
