#ifndef TUT_FORK_H_GUARD
#define TUT_FORK_H_GUARD

#if defined(TUT_USE_POSIX)
#include <errno.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <sys/types.h>

#include <cstring>
#include <cstdlib>
#include <map>
#include <iterator>
#include <functional>

#include "tut_result.hpp"
#include "tut_assert.hpp"
#include "tut_runner.hpp"

namespace tut
{

template<typename, int>
class test_group;

template<typename T>
class test_object;

class test_group_posix
{
private:
    template<typename, int>
    friend class test_group;

    template<typename T>
    void send_result_(const T *obj, const test_result &tr)
    {
        if(obj->get_pipe_() == -1)
        {
            return;
        }

        if(tr.result != test_result::ok)
        {
            std::stringstream ss;
            ss << int(tr.result) << "\n"
                << tr.group << "\n"
                << tr.test << "\n"
                << tr.name << "\n"
                << tr.exception_typeid << "\n";
            std::copy( tr.message.begin(), tr.message.end(), std::ostreambuf_iterator<char>(ss.rdbuf()) );

            int size = static_cast<int>(ss.str().length());
            int w = static_cast<int>(write(obj->get_pipe_(), ss.str().c_str(), size));
            ensure_errno("write() failed", w == size);
        }
    }
};

template<typename T>
struct tut_posix
{
    pid_t fork()
    {
        test_object<T> *self = dynamic_cast< tut::test_object<T>* >(this);
        ensure("trying to call 'tut_fork' in ctor of test object", self != nullptr);

        return self->fork_();
    }

    pid_t waitpid(pid_t pid, int *status, int flags = 0)
    {
        test_object<T> *self = dynamic_cast< tut::test_object<T>* >(this);
        ensure("trying to call 'tut_waitpid' in ctor of test object", self != nullptr);

        return self->waitpid_(pid, status, flags);
    }

    void ensure_child_exit(pid_t pid, int exit_status = 0)
    {
        test_object<T> *self = dynamic_cast< tut::test_object<T>* >(this);
        ensure("trying to call 'ensure_child_exit' in ctor of test object", self != nullptr);

        int status;
        self->waitpid_(pid, &status);

        self->ensure_child_exit_(status, exit_status);
    }


    void ensure_child_signal(pid_t pid, int signal = SIGTERM)
    {
        test_object<T> *self = dynamic_cast< tut::test_object<T>* >(this);
        ensure("trying to call 'ensure_child_signal' in ctor of test object", self != nullptr);

        int status;
        self->waitpid_(pid, &status);

        self->ensure_child_signal_(status, signal);
    }

    std::set<pid_t> get_pids() const
    {
        using namespace std;

        const test_object<T> *self = dynamic_cast< const tut::test_object<T>* >(this);
        ensure("trying to call 'get_pids' in ctor of test object", self != nullptr);

        return self->get_pids_();
    }

    virtual ~tut_posix()
    {
    }

};

class test_object_posix
{
public:
    typedef std::map<pid_t, int> pid_map;

    /**
     * Default constructor
     */
    test_object_posix()
        : pipe_(-1)
    {
    }


    virtual ~test_object_posix()
    {
        // we have forked
        if(pipe_ != -1)
        {
            // in child, force exit
            std::exit(0);
        }

        if(!pids_.empty())
        {
            std::stringstream ss;

            // in parent, reap children
            for(std::map<pid_t, int>::iterator i = pids_.begin(); i != pids_.end(); ++i)
            {
                try {
                    kill_child_(i->first);
                } catch(const rethrown &ex) {
                    ss << std::endl << "child " << ex.tr.pid << " has thrown an exception: " << ex.what();
                } catch(const failure &ex) {
                    ss << std::endl << ex.what();
                }
            }

            if(!ss.str().empty())
            {
                fail(ss.str().c_str());
            }
        }
    }

private:
    template<typename T>
    friend struct tut_posix;

    friend class test_group_posix;

    int get_pipe_() const
    {
        return pipe_;
    }


    pid_t fork_()
    {
        // create pipe
        int fds[2];
        ensure_errno("pipe() failed", ::pipe(fds) == 0);

        pid_t pid = ::fork();

        ensure_errno("fork() failed", pid >= 0);

        if(pid != 0)
        {
            // in parent, register pid
            ensure("duplicated child", pids_.insert( std::make_pair(pid, fds[0]) ).second);

            // close writing side
            close(fds[1]);
        }
        else
        {
            // in child, shutdown reporter
            tut::runner.get().clear_callbacks();

            // close reading side
            close(fds[0]);
            pipe_ = fds[1];
        }

        return pid;
    }

    void kill_child_(pid_t pid)
    {
        int status;

        if(waitpid_(pid, &status, WNOHANG) == pid)
        {
            ensure_child_exit_(status, 0);
            return;
        }

        if(::kill(pid, SIGTERM) != 0)
        {
            if(errno == ESRCH)
            {
                // no such process
                return;
            }
            else
            {
                // cannot kill, we are in trouble
                std::stringstream ss;
                char e[1024];
                ss << "child " << pid << " could not be killed with SIGTERM, " << strerror_r(errno, e, sizeof(e)) << std::endl;
                fail(ss.str());
            }
        }

        if(waitpid_(pid, &status, WNOHANG) == pid)
        {
            // child killed, check signal
            ensure_child_signal_(status, SIGTERM);

            ensure_equals("child process exists after SIGTERM", ::kill(pid, 0), -1);
            return;
        }

        // child seems to be still exiting, give it some time
        sleep(2);

        if(waitpid_(pid, &status, WNOHANG) != pid)
        {
            // child is still running, kill it
            if(::kill(pid, SIGKILL) != 0)
            {
                if(errno == ESRCH)
                {
                    // no such process
                    return;
                }
                else
                {
                    std::stringstream ss;
                    char e[1024];
                    ss << "child " << pid << " could not be killed with SIGKILL, " << strerror_r(errno, e, sizeof(e)) << std::endl;
                    fail(ss.str());
                }
            }

            ensure_equals("wait after SIGKILL", waitpid_(pid, &status), pid);
            ensure_child_signal_(status, SIGKILL);

            ensure_equals("child process exists after SIGKILL", ::kill(pid, 0), -1);

            std::stringstream ss;
            ss << "child " << pid << " had to be killed with SIGKILL";
            fail(ss.str());
        }
    }

    test_result receive_result_(std::istream &ss, pid_t pid)
    {
        test_result tr;

        int type;
        ss >> type;
        tr.result = test_result::result_type(type);
        ss.ignore(1024, '\n');

        std::getline(ss, tr.group);
        ss >> tr.test;
        ss.ignore(1024, '\n');
        std::getline(ss, tr.name);
        std::getline(ss, tr.exception_typeid);
        std::copy( std::istreambuf_iterator<char>(ss.rdbuf()),
                   std::istreambuf_iterator<char>(),
                   std::back_inserter(tr.message) );

        tr.pid = pid;

        return tr;
    }

    struct fdclose
    {
        fdclose(int fd): fd_(fd) { }
        ~fdclose()
        {
            close(fd_);
        }
    private:
        int fd_;
    };

    pid_t waitpid_(pid_t pid, int *status, int flags = 0)
    {

        ensure("trying to wait for unknown pid", pids_.count(pid) > 0);

        pid_t p = ::waitpid(pid, status, flags);
        if( (flags & WNOHANG) && (p != pid) )
        {
            return p;
        }

        // read child result from pipe
        fd_set fdset;
        timeval tv;
        tv.tv_sec = 0;
        tv.tv_usec = 0;

        FD_ZERO(&fdset);

        int pipe = pids_[pid];
        fdclose guard(pipe);

        FD_SET(pipe, &fdset);

        int result = select(pipe+1, &fdset, nullptr, nullptr, &tv);
        ensure_errno("sanity check on select() failed", result >= 0);

        if(result > 0)
        {
            ensure("sanity check on FD_ISSET() failed", FD_ISSET(pipe, &fdset) );

            std::stringstream ss;

            //TODO: max failure length
            char buffer[1024];
            int r = static_cast<int>(read(pipe, buffer, sizeof(buffer)));
            ensure_errno("sanity check on read() failed", r >= 0);

            if(r > 0)
            {
                ss.write(buffer, r);
                throw rethrown( receive_result_(ss, pid) );
            }
        }

        return pid;
    }

    void ensure_child_exit_(int status, int exit_status)
    {
        if(WIFSIGNALED(status))
        {
            std::stringstream ss;
            ss << "child killed by signal " << WTERMSIG(status)
                << ": expected exit with code " << exit_status;

            throw failure(ss.str().c_str());
        }

        if(WIFEXITED(status))
        {
            if(WEXITSTATUS(status) != exit_status)
            {
                std::stringstream ss;
                ss << "child exited, expected '"
                    << exit_status
                    << "' actual '"
                    << WEXITSTATUS(status)
                    << '\'';

                throw failure(ss.str().c_str());
            }
        }

        if(WIFSTOPPED(status))
        {
            std::stringstream ss;
            ss << "child stopped by signal " << WTERMSIG(status)
                << ": expected exit with code " << exit_status;
            throw failure(ss.str().c_str());
        }
    }

    void ensure_child_signal_(int status, int signal)
    {
        if(WIFSIGNALED(status))
        {
            if(WTERMSIG(status) != signal)
            {
                std::stringstream ss;
                ss << "child killed by signal, expected '"
                    << signal
                    << "' actual '"
                    << WTERMSIG(status)
                    << '\'';
                throw failure(ss.str().c_str());
            }
        }

        if(WIFEXITED(status))
        {
            std::stringstream ss;
            ss << "child exited with code " << WEXITSTATUS(status)
                << ": expected signal " << signal;

            throw failure(ss.str().c_str());
        }

        if(WIFSTOPPED(status))
        {
            std::stringstream ss;
            ss << "child stopped by signal " << WTERMSIG(status)
                << ": expected kill by signal " << signal;

            throw failure(ss.str().c_str());
        }
    }

    std::set<pid_t> get_pids_() const
    {
        using namespace std;

        set<pid_t> pids;
        for(pid_map::const_iterator i = pids_.begin(); i != pids_.end(); ++i)
        {
            pids.insert( i->first );
        }

        return pids;
    }

    pid_map         pids_;
    int             pipe_;
};

} // namespace tut

#else

namespace tut
{

struct test_object_posix
{
};

struct test_group_posix
{
    template<typename T>
    void send_result_(const T*, const test_result &)
    {
    }
};

} // namespace tut

#endif


#endif

