//===-- EditlineTest.cpp ----------------------------------------*- C++ -*-===//
//
//                     The LLVM Compiler Infrastructure
//
// This file is distributed under the University of Illinois Open Source
// License. See LICENSE.TXT for details.
//
//===----------------------------------------------------------------------===//

#ifndef LLDB_DISABLE_LIBEDIT

#define EDITLINE_TEST_DUMP_OUTPUT 0

#include <stdio.h>
#include <unistd.h>

#include <memory>
#include <thread>

#include "gtest/gtest.h"

#include "lldb/Core/Error.h"
#include "lldb/Core/StringList.h"
#include "lldb/Host/Editline.h"
#include "lldb/Host/Pipe.h"
#include "lldb/Utility/PseudoTerminal.h"

namespace
{
    const size_t TIMEOUT_MILLIS = 5000;
}

class FilePointer
{
public:

    FilePointer () = delete;

    FilePointer (const FilePointer&) = delete;

    FilePointer (FILE *file_p)
    : _file_p (file_p)
    {
    }

    ~FilePointer ()
    {
        if (_file_p != nullptr)
        {
            const int close_result = fclose (_file_p);
            EXPECT_EQ(0, close_result);
        }
    }

    operator FILE* ()
    {
        return _file_p;
    }

private:

    FILE *_file_p;

};

/**
 Wraps an Editline class, providing a simple way to feed
 input (as if from the keyboard) and receive output from Editline.
 */
class EditlineAdapter
{
public:

    EditlineAdapter ();

    void
    CloseInput ();

    bool
    IsValid () const
    {
        return _editline_sp.get () != nullptr;
    }

    lldb_private::Editline&
    GetEditline ()
    {
        return *_editline_sp;
    }

    bool
    SendLine (const std::string &line);

    bool
    SendLines (const std::vector<std::string> &lines);

    bool
    GetLine (std::string &line, bool &interrupted, size_t timeout_millis);

    bool
    GetLines (lldb_private::StringList &lines, bool &interrupted, size_t timeout_millis);

    void
    ConsumeAllOutput ();

private:

    static bool
    IsInputComplete (
        lldb_private::Editline * editline,
        lldb_private::StringList & lines,
        void * baton);

    std::unique_ptr<lldb_private::Editline> _editline_sp;

    lldb_utility::PseudoTerminal _pty;
    int _pty_master_fd;
    int _pty_slave_fd;

    std::unique_ptr<FilePointer> _el_slave_file;
};

EditlineAdapter::EditlineAdapter () :
    _editline_sp (),
    _pty (),
    _pty_master_fd (-1),
    _pty_slave_fd (-1),
    _el_slave_file ()
{
    lldb_private::Error error;

    // Open the first master pty available.
    char error_string[256];
    error_string[0] = '\0';
    if (!_pty.OpenFirstAvailableMaster (O_RDWR, error_string, sizeof (error_string)))
    {
        fprintf(stderr, "failed to open first available master pty: '%s'\n", error_string);
        return;
    }

    // Grab the master fd.  This is a file descriptor we will:
    // (1) write to when we want to send input to editline.
    // (2) read from when we want to see what editline sends back.
    _pty_master_fd = _pty.GetMasterFileDescriptor();

    // Open the corresponding slave pty.
    if (!_pty.OpenSlave (O_RDWR, error_string, sizeof (error_string)))
    {
        fprintf(stderr, "failed to open slave pty: '%s'\n", error_string);
        return;
    }
    _pty_slave_fd = _pty.GetSlaveFileDescriptor();

    _el_slave_file.reset (new FilePointer (fdopen (_pty_slave_fd, "rw")));
    EXPECT_FALSE (nullptr == *_el_slave_file);
    if (*_el_slave_file == nullptr)
        return;

    // Create an Editline instance.
    _editline_sp.reset (new lldb_private::Editline("gtest editor", *_el_slave_file, *_el_slave_file, *_el_slave_file, false));
    _editline_sp->SetPrompt ("> ");

    // Hookup our input complete callback.
    _editline_sp->SetIsInputCompleteCallback(IsInputComplete, this);
}

void
EditlineAdapter::CloseInput ()
{
    if (_el_slave_file != nullptr)
        _el_slave_file.reset (nullptr);
}

bool
EditlineAdapter::SendLine (const std::string &line)
{
    // Ensure we're valid before proceeding.
    if (!IsValid ())
        return false;

    // Write the line out to the pipe connected to editline's input.
    ssize_t input_bytes_written =
        ::write (_pty_master_fd,
                 line.c_str(),
                 line.length() * sizeof (std::string::value_type));

    const char *eoln = "\n";
    const size_t eoln_length = strlen(eoln);
    input_bytes_written =
        ::write (_pty_master_fd,
                 eoln,
                 eoln_length * sizeof (char));

    EXPECT_NE(-1, input_bytes_written) << strerror(errno);
    EXPECT_EQ (eoln_length * sizeof (char), size_t(input_bytes_written));
    return eoln_length * sizeof (char) == size_t(input_bytes_written);
}

bool
EditlineAdapter::SendLines (const std::vector<std::string> &lines)
{
    for (auto &line : lines)
    {
#if EDITLINE_TEST_DUMP_OUTPUT
        printf ("<stdin> sending line \"%s\"\n", line.c_str());
#endif
        if (!SendLine (line))
            return false;
    }
    return true;
}

// We ignore the timeout for now.
bool
EditlineAdapter::GetLine (std::string &line, bool &interrupted, size_t /* timeout_millis */)
{
    // Ensure we're valid before proceeding.
    if (!IsValid ())
        return false;

    _editline_sp->GetLine (line, interrupted);
    return true;
}

bool
EditlineAdapter::GetLines (lldb_private::StringList &lines, bool &interrupted, size_t /* timeout_millis */)
{
    // Ensure we're valid before proceeding.
    if (!IsValid ())
        return false;
    
    _editline_sp->GetLines (1, lines, interrupted);
    return true;
}

bool
EditlineAdapter::IsInputComplete (
        lldb_private::Editline * editline,
        lldb_private::StringList & lines,
        void * baton)
{
    // We'll call ourselves complete if we've received a balanced set of braces.
    int start_block_count = 0;
    int brace_balance = 0;

    for (size_t i = 0; i < lines.GetSize (); ++i)
    {
        for (auto ch : lines[i])
        {
            if (ch == '{')
            {
                ++start_block_count;
                ++brace_balance;
            }
            else if (ch == '}')
                --brace_balance;
        }
    }

    return (start_block_count > 0) && (brace_balance == 0);
}

void
EditlineAdapter::ConsumeAllOutput ()
{
    FilePointer output_file (fdopen (_pty_master_fd, "r"));

    int ch;
    while ((ch = fgetc(output_file)) != EOF)
    {
#if EDITLINE_TEST_DUMP_OUTPUT
        char display_str[] = { 0, 0, 0 };
        switch (ch)
        {
            case '\t':
                display_str[0] = '\\';
                display_str[1] = 't';
                break;
            case '\n':
                display_str[0] = '\\';
                display_str[1] = 'n';
                break;
            case '\r':
                display_str[0] = '\\';
                display_str[1] = 'r';
                break;
            default:
                display_str[0] = ch;
                break;
        }
        printf ("<stdout> 0x%02x (%03d) (%s)\n", ch, ch, display_str);
        // putc(ch, stdout);
#endif
    }
}

class EditlineTestFixture : public ::testing::Test
{
private:
    EditlineAdapter _el_adapter;
    std::shared_ptr<std::thread> _sp_output_thread;

public:
    void SetUp()
    {
        // We need a TERM set properly for editline to work as expected.
        setenv("TERM", "vt100", 1);

        // Validate the editline adapter.
        EXPECT_TRUE(_el_adapter.IsValid());
        if (!_el_adapter.IsValid())
            return;

        // Dump output.
        _sp_output_thread.reset(new std::thread([&] { _el_adapter.ConsumeAllOutput(); }));
    }

    void TearDown()
    {
        _el_adapter.CloseInput();
        if (_sp_output_thread)
            _sp_output_thread->join();
    }

    EditlineAdapter &GetEditlineAdapter() { return _el_adapter; }
};

TEST_F(EditlineTestFixture, EditlineReceivesSingleLineText)
{
    // Send it some text via our virtual keyboard.
    const std::string input_text ("Hello, world");
    EXPECT_TRUE(GetEditlineAdapter().SendLine(input_text));

    // Verify editline sees what we put in.
    std::string el_reported_line;
    bool input_interrupted = false;
    const bool received_line = GetEditlineAdapter().GetLine(el_reported_line, input_interrupted, TIMEOUT_MILLIS);

    EXPECT_TRUE (received_line);
    EXPECT_FALSE (input_interrupted);
    EXPECT_EQ (input_text, el_reported_line);
}

TEST_F(EditlineTestFixture, EditlineReceivesMultiLineText)
{
    // Send it some text via our virtual keyboard.
    std::vector<std::string> input_lines;
    input_lines.push_back ("int foo()");
    input_lines.push_back ("{");
    input_lines.push_back ("printf(\"Hello, world\");");
    input_lines.push_back ("}");
    input_lines.push_back ("");

    EXPECT_TRUE(GetEditlineAdapter().SendLines(input_lines));

    // Verify editline sees what we put in.
    lldb_private::StringList el_reported_lines;
    bool input_interrupted = false;

    EXPECT_TRUE(GetEditlineAdapter().GetLines(el_reported_lines, input_interrupted, TIMEOUT_MILLIS));
    EXPECT_FALSE (input_interrupted);

    // Without any auto indentation support, our output should directly match our input.
    EXPECT_EQ (input_lines.size (), el_reported_lines.GetSize ());
    if (input_lines.size () == el_reported_lines.GetSize ())
    {
        for (size_t i = 0; i < input_lines.size(); ++i)
            EXPECT_EQ (input_lines[i], el_reported_lines[i]);
    }
}

#endif
