// Copyright 2019 California Institute of Technology.

// You should have received a copy of the licensing terms for this
// software included in the file “LICENSE” located in the top-level
// directory of this package. If you did not, you can view a copy at
// http://dcc.ligo.org/M1500244/LICENSE.txt

#include <sstream>
#include "completion.hh"

#include "catch.hpp"

class temporary_fd
{
public:
    explicit temporary_fd( std::string temp_dir ) : fd_( -1 ), name_( )
    {
        std::string path( std::move( temp_dir ) );
        if ( !path.empty( ) )
        {
            if ( path.back( ) != '/' )
            {
                path.push_back( '/' );
            }
        }
        path += "tmpfileXXXXXX";
        std::vector< char > tmp;
        tmp.reserve( path.size( ) + 1 );
        std::copy( path.begin( ), path.end( ), std::back_inserter( tmp ) );
        tmp.push_back( '\0' );
        name_.clear( );
        name_.reserve( tmp.size( ) );
        fd_ = mkstemp( tmp.data( ) );
        if ( fd_ >= 0 )
        {
            name_.append( tmp.data( ) );
        }
    }
    temporary_fd( const temporary_fd& other ) = delete;
    temporary_fd( temporary_fd&& other ) noexcept
        : fd_( other.fd_ ),
          name_( std::move( other.name_ ) )
    {
        other.fd_ = -1;
    }
    temporary_fd operator=( const temporary_fd& other ) = delete;
    temporary_fd&
    operator=( temporary_fd&& other ) noexcept
    {
        unlink( );
        fd_ = other.fd_;
        name_ = std::move( other.name_ );
        other.fd_ = -1;
        other.name_.clear( );
        return *this;
    };

    ~temporary_fd( )
    {
        unlink( );
    }

    void
    add_data( const std::string& data )
    {
        if ( *this )
        {
            size_t remaining = data.size( );
            size_t copied = 0;
            while ( remaining )
            {
                ssize_t count = write( fd_, data.data( ) + copied, remaining );
                if ( count >= 0 )
                {
                    remaining -= static_cast< size_t >( count );
                    copied += static_cast< size_t >( count );
                }
                else
                {
                    auto err = errno;
                    if ( err == EAGAIN || err == EINTR )
                    {
                        continue;
                    }
                    throw std::runtime_error(
                        "Error writing data to temp file" );
                }
            }
        }
    }

    void
    add_data( const std::vector< std::string >& data )
    {
        for ( const auto& cur : data )
        {
            add_data( cur );
            add_data( "\n" );
        }
    }

    void
    close( )
    {
        if ( fd_ >= 0 )
        {
            ::close( fd_ );
        }
        fd_ = -1;
    }

    std::string
    filename( ) const
    {
        return name_;
    }

    operator bool( ) const
    {
        return fd_ >= 0;
    }

private:
    void
    unlink( )
    {
        close( );
        if ( !name_.empty( ) )
        {
            ::unlink( name_.c_str( ) );
        }
        name_.clear( );
    }
    int         fd_;
    std::string name_;
};

TEST_CASE( "Load empty database" )
{

    temporary_fd file( "." );
    file.add_data( std::vector< std::string >{} );
    auto db = completion::load_database( file.filename( ) );
    REQUIRE( db.channels.size( ) == 0 );
}

TEST_CASE( "Load database with no spaces" )
{

    temporary_fd file( "." );
    file.add_data( std::vector< std::string >{ "abc", "def" } );
    auto db = completion::load_database( file.filename( ) );
    auto expected =
        std::vector< completion::channel >{ { "abc", "" }, { "def", "" } };
    REQUIRE( db.channels.size( ) == expected.size( ) );
    for ( int i = 0; i < db.channels.size( ); ++i )
    {
        REQUIRE( db.channels[ i ].name == expected[ i ].name );
        REQUIRE( db.channels[ i ].extra == expected[ i ].extra );
    }
}

TEST_CASE( "Load database with spaces" )
{

    temporary_fd file( "." );
    file.add_data( std::vector< std::string >{
        "abc", "def", "ghi jkl", "mno ", "pqr stu" } );
    auto db = completion::load_database( file.filename( ) );
    auto expected = std::vector< completion::channel >{ { "abc", "" },
                                                        { "def", "" },
                                                        { "ghi", "jkl" },
                                                        { "mno", "" },
                                                        { "pqr", "stu" } };
    REQUIRE( db.channels.size( ) == expected.size( ) );
    for ( int i = 0; i < db.channels.size( ); ++i )
    {
        REQUIRE( db.channels[ i ].name == expected[ i ].name );
        REQUIRE( db.channels[ i ].extra == expected[ i ].extra );
    }
}

TEST_CASE( "Basic completion behavior" )
{
    completion::Database db;
    db.channels = {
        { "H0:VAC-SYSTEM_1" }, { "H0:VAC-SYSTEM_2" }, { "H0:VAC-SYSTEM_3" },
        { "H1:SYS-SUB_A" },    { "H1:SYS-SUB_B" },    { "H1:SYS-SUB_C" },
    };

    {
        completion::string_list results =
            completion::search( db.channels.begin( ), db.channels.end( ), "" );
        REQUIRE( results.size( ) == 2 );
        REQUIRE( results[ 0 ] == "H0:VAC" );
        REQUIRE( results[ 1 ] == "H1:SYS" );
    }

    {
        completion::string_list results = completion::search(
            db.channels.begin( ), db.channels.end( ), "H1:SY" );
        REQUIRE( results.size( ) == 1 );
        REQUIRE( results[ 0 ] == "H1:SYS" );
    }

    {
        completion::string_list results = completion::search(
            db.channels.begin( ), db.channels.end( ), "H1:SYS" );
        REQUIRE( results.size( ) == 1 );
        REQUIRE( results[ 0 ] == "H1:SYS-SUB" );
    }

    {
        completion::string_list results = completion::search(
            db.channels.begin( ), db.channels.end( ), "H1:SYS-SUB" );
        REQUIRE( results.size( ) == 3 );
        REQUIRE( results[ 0 ] == "H1:SYS-SUB_A" );
        REQUIRE( results[ 1 ] == "H1:SYS-SUB_B" );
        REQUIRE( results[ 2 ] == "H1:SYS-SUB_C" );
    }
}

TEST_CASE( "Channel completion should not remove user input" )
{
    completion::Database db;
    db.channels = {
        { "H0:VAC-SYSTEM" },     { "H0:VAC-SYSTEM_1_a" },
        { "H0:VAC-SYSTEM_1_b" }, { "H0:VAC-SYSTEM_1_c" },
        { "H0:VAC-SYSTEM_1_d" }, { "H0:VAC-SYSTEM_1_e" },
        { "H0:VAC-SYSTEM_1_f" },
    };
    {
        std::vector< std::string > expected = {
            "H0:VAC-SYSTEM_1_a", "H0:VAC-SYSTEM_1_b", "H0:VAC-SYSTEM_1_c",
            "H0:VAC-SYSTEM_1_d", "H0:VAC-SYSTEM_1_e", "H0:VAC-SYSTEM_1_f",
        };

        completion::string_list results = completion::search(
            db.channels.begin( ), db.channels.end( ), "H0:VAC-SYSTEM_1" );
        REQUIRE( results == expected );
    }
}

TEST_CASE( "Basic DB sorting" )
{
    completion::Database input;
    input.channels = {
        { "H0:VAC-SYSTEM_3" }, { "H0:VAC-SYSTEM_1" }, { "H1:SYS-SUB_C" },
        { "H0:VAC-SYSTEM_2" }, { "H1:SYS-SUB_A" },    { "H1:SYS-SUB_B" },
    };
    completion::Database expected;
    expected.channels = {
        { "H0:VAC-SYSTEM_1" }, { "H0:VAC-SYSTEM_2" }, { "H0:VAC-SYSTEM_3" },
        { "H1:SYS-SUB_A" },    { "H1:SYS-SUB_B" },    { "H1:SYS-SUB_C" },
    };

    completion::sort( input );
    REQUIRE( input.channels.size( ) == expected.channels.size( ) );
    for ( int i = 0; i < input.channels.size( ); ++i )
    {
        REQUIRE( input.channels[ i ].name == expected.channels[ i ].name );
    }
}

TEST_CASE( "Serialization of channel with an extra portion" )
{
    std::ostringstream  os;
    completion::channel a( "abc", "def" );
    os << a;
    REQUIRE( os.str( ) == "abc def" );
}

TEST_CASE( "Serialization of channel without an extra portion" )
{
    std::ostringstream  os;
    completion::channel a( "abc" );
    os << a;
    REQUIRE( os.str( ) == "abc" );
}