// 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 <iostream>
#include <iterator>
#include <string>
#include <vector>

#include <stdlib.h>

#include "catch.hpp"

#include <pstreams/pstream.h>

extern std::string test_program_name;

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_;
};

void
run_test( const std::vector< std::string >& input,
          const std::vector< std::string >& extra_args,
          const std::vector< std::string >& expected )
{
    std::vector< std::string > args;
    temporary_fd               db( "." );
    db.add_data( input );

    args.emplace_back( test_program_name );
    args.emplace_back( "-d" );
    args.emplace_back( db.filename( ) );
    std::copy(
        extra_args.begin( ), extra_args.end( ), std::back_inserter( args ) );
    redi::ipstream prog( test_program_name, args );
    REQUIRE( prog.is_open( ) );

    std::string                line;
    std::vector< std::string > choices;
    while ( std::getline( prog, line ) )
    {
        if ( !line.empty( ) )
        {
            choices.push_back( line );
            std::cerr << line << "\n";
        }
    }

    prog.close( );
    REQUIRE( prog.rdbuf( )->exited( ) );
    REQUIRE( prog.rdbuf( )->status( ) == 0 );
    REQUIRE( choices == expected );
}
void
run_search_test( const std::vector< std::string >& input,
                 const std::string&                key,
                 const std::vector< std::string >& expected )
{
    run_test( input, { key }, expected );
}

void
run_sort_test( const std::vector< std::string >& input,
               const std::vector< std::string >& expected )
{
    run_test( input, { "-r" }, expected );
}

TEST_CASE( "You can run the test program" )
{
    std::vector< std::string > args;
    args.push_back( test_program_name );
    args.emplace_back( "H1:" );
    redi::ipstream prog( test_program_name, args );
    REQUIRE( prog.is_open( ) );

    std::string                line;
    std::vector< std::string > choices;
    while ( std::getline( prog, line ) )
    {
        if ( !line.empty( ) )
        {
            choices.push_back( line );
            std::cerr << line << "\n";
        }
    }
    prog.close( );
    REQUIRE( prog.rdbuf( )->exited( ) );
    REQUIRE( prog.rdbuf( )->status( ) == 0 );
    REQUIRE( choices.empty( ) );
}

TEST_CASE( "Basic completion behavior with an specified database no input" )
{
    std::vector< std::string > input = {
        "H0:VAC-SYSTEM_1", "H0:VAC-SYSTEM_2", "H0:VAC-SYSTEM_3",
        "H1:SYS-SUB_A",    "H1:SYS-SUB_B",    "H1:SYS-SUB_C",
    };
    std::vector< std::string > expected = {
        "H0:VAC", "H1:SYS",
    };
    run_search_test( input, "", expected );
}

TEST_CASE( "Basic completion behavior with an specified database first step" )
{
    std::vector< std::string > input = {
        "H0:VAC-SYSTEM_1", "H0:VAC-SYSTEM_2", "H0:VAC-SYSTEM_3",
        "H1:SYS-SUB_A",    "H1:SYS-SUB_B",    "H1:SYS-SUB_C",
    };
    std::vector< std::string > expected = {
        "H1:SYS-SUB_A", "H1:SYS-SUB_B", "H1:SYS-SUB_C",
    };
    run_search_test( input, "H1:SY", expected );
}

TEST_CASE( "Basic completion behavior with an specified database with spaces "
           "first step" )
{
    std::vector< std::string > input = {
        "H0:VAC-SYSTEM_1 foo", "H0:VAC-SYSTEM_1a bar", "H0:VAC-SYSTEM_2",
        "H0:VAC-SYSTEM_3",     "H1:SYS-SUB_A",         "H1:SYS-SUB_B",
        "H1:SYS-SUB_C",
    };
    std::vector< std::string > expected = {
        "H0:VAC-SYSTEM_1", "H0:VAC-SYSTEM_1a",
    };
    run_search_test( input, "H0:VAC-SYSTEM_1", expected );
}

TEST_CASE( "Completions should never remove user input" )
{
    std::vector< std::string > input = {
        "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",
    };
    run_search_test( input, "H0:VAC-SYSTEM_1", expected );
}

TEST_CASE( "Basic sorting with a specified input database" )
{
    std::vector< std::string > input = {
        "H0:VAC-SYSTEM_3", "H0:VAC-SYSTEM_1", "H1:SYS-SUB_C",
        "H0:VAC-SYSTEM_2", "H1:SYS-SUB_A",    "H1:SYS-SUB_B",
    };
    std::vector< std::string > expected = {
        "H0:VAC-SYSTEM_1", "H0:VAC-SYSTEM_2", "H0:VAC-SYSTEM_3",
        "H1:SYS-SUB_A",    "H1:SYS-SUB_B",    "H1:SYS-SUB_C",
    };
    run_sort_test( input, expected );
}

TEST_CASE( "Basic sorting with a specified input database with spaces" )
{
    std::vector< std::string > input = {
        "H0:VAC-SYSTEM_3 a", "H0:VAC-SYSTEM_1 b", "H1:SYS-SUB_C",
        "H0:VAC-SYSTEM_2",   "H1:SYS-SUB_A",      "H1:SYS-SUB_B",
        "H0:VAC-SYSTEM 1"
    };
    std::vector< std::string > expected = {
        "H0:VAC-SYSTEM 1",   "H0:VAC-SYSTEM_1 b", "H0:VAC-SYSTEM_2",
        "H0:VAC-SYSTEM_3 a", "H1:SYS-SUB_A",      "H1:SYS-SUB_B",
        "H1:SYS-SUB_C",
    };
    run_sort_test( input, expected );
}

TEST_CASE( "Calling the program w/o database should not give output" )
{
    std::vector< std::string > args;

    args.emplace_back( test_program_name );
    args.emplace_back( "-d" );
    redi::ipstream prog( test_program_name, args );
    REQUIRE( prog.is_open( ) );

    std::string                line;
    std::vector< std::string > choices;
    while ( std::getline( prog, line ) )
    {
        if ( !line.empty( ) )
        {
            choices.push_back( line );
            std::cerr << line << "\n";
        }
    }

    prog.close( );
    REQUIRE( prog.rdbuf( )->exited( ) );
    REQUIRE( choices.empty( ) );
}

TEST_CASE( "Calling the program w/o database but with a search key should not "
           "give output" )
{
    std::vector< std::string > args;

    args.emplace_back( test_program_name );
    args.emplace_back( "key" );
    args.emplace_back( "-d" );
    redi::ipstream prog( test_program_name, args );
    REQUIRE( prog.is_open( ) );

    std::string                line;
    std::vector< std::string > choices;
    while ( std::getline( prog, line ) )
    {
        if ( !line.empty( ) )
        {
            choices.push_back( line );
            std::cerr << line << "\n";
        }
    }

    prog.close( );
    REQUIRE( prog.rdbuf( )->exited( ) );
    REQUIRE( choices.empty( ) );
}

TEST_CASE( "Calling the program an empty database name should not give output" )
{
    std::vector< std::string > args;

    args.emplace_back( test_program_name );
    args.emplace_back( "-d" );
    args.emplace_back( "" );
    redi::ipstream prog( test_program_name, args );
    REQUIRE( prog.is_open( ) );

    std::string                line;
    std::vector< std::string > choices;
    while ( std::getline( prog, line ) )
    {
        if ( !line.empty( ) )
        {
            choices.push_back( line );
            std::cerr << line << "\n";
        }
    }

    prog.close( );
    REQUIRE( prog.rdbuf( )->exited( ) );
    REQUIRE( choices.empty( ) );
}

TEST_CASE( "Calling the program an empty database name and a key should not "
           "give output" )
{
    std::vector< std::string > args;

    args.emplace_back( test_program_name );
    args.emplace_back( "key" );
    args.emplace_back( "-d" );
    args.emplace_back( "" );
    redi::ipstream prog( test_program_name, args );
    REQUIRE( prog.is_open( ) );

    std::string                line;
    std::vector< std::string > choices;
    while ( std::getline( prog, line ) )
    {
        if ( !line.empty( ) )
        {
            choices.push_back( line );
            std::cerr << line << "\n";
        }
    }

    prog.close( );
    REQUIRE( prog.rdbuf( )->exited( ) );
    REQUIRE( choices.empty( ) );
}