#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
