// file      : tests/process/driver.cxx -*- C++ -*-
// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd
// license   : MIT; see accompanying LICENSE file

#include <stdlib.h> // getenv(), setenv(), _putenv()

#include <ios>
#include <string>
#include <vector>
#include <cassert>
#include <iostream>
#include <iterator>  // istreambuf_iterator, ostream_iterator
#include <algorithm> // copy()

#include <libbutl/path.hxx>
#include <libbutl/process.hxx>
#include <libbutl/fdstream.hxx>

using namespace std;
using namespace butl;

static const char* envvars[] = {"ABC=1", "DEF", nullptr};

static bool
exec (const path& p,
      vector<char> in = vector<char> (),
      bool out = false,
      bool err = false,
      bool pipeline = false,
      bool bin = true,           // Set binary mode for file descriptors.
      dir_path wd = dir_path (), // Set the working dir for the child process.
      bool env = false)          // Set environment variables for the child.
{
  using cstrings = vector<const char*>;

  assert (!in.empty () || (!out && !err)); // Nothing to output if no input.
  assert (!pipeline || out); // To pipeline need to output something.

  const char* cwd (!wd.empty () ? wd.string ().c_str () : nullptr);

  cstrings args {p.string ().c_str (), "-c"};

  if (bin)
    args.push_back ("-b");

  if (env)
  {
    args.push_back ("-e");

    // Here we set the environment variables for the current process to make
    // sure that the child process will not see the variable that is requested
    // to be unset, and will see the other one unaffected.
    //
    // Note that we don't support (un)setting environment variables for the
    // child process on Windows.
    //
#ifndef _WIN32
    assert (setenv ("DEF", "2", 1) == 0);
    assert (setenv ("XYZ", "3", 1) == 0);
#else
    assert (false);
#endif
  }

  if (cwd != nullptr)
    args.push_back (cwd);

  args.push_back (nullptr);

  try
  {
    bool r (true);

    // If both o and e are true, then redirect STDERR to STDOUT, so both can be
    // read from the same stream.
    //
    process pr (args.data (),
                !in.empty () ? -1 : -2,
                out ? -1 : -2,
                err ? (out ? 1 : -1) : -2,
                cwd,
                env ? envvars : nullptr);

    try
    {
      if (!in.empty ())
      {
        bool s;
        r = !pr.try_wait (s); // Couldn't exit as waiting for the input.

        auto bin_mode = [bin](auto_fd fd) -> auto_fd
        {
          if (bin)
            fdmode (fd.get (), fdstream_mode::binary);

          return fd;
        };

        ofdstream os (bin_mode (move (pr.out_fd)));
        copy (in.begin (), in.end (), ostream_iterator<char> (os));
        os.close ();

        if (out)
        {
          vector<char> o;

          if (pipeline)
          {
            // Here we test both passing process output fd as an input for
            // another process (pr2.in = pr.out), as well as passing process
            // input fd as an output for another one (pr2.out = pr3.in). The
            // overall pipeline looks like 'os -> pr -> pr2 -> pr3 -> is'.
            //
            process pr3 (args.data (),
                         -1, -1, -2,
                         cwd,
                         env ? envvars : nullptr);

            process pr2 (args.data (),
                         pr, bin_mode (move (pr3.out_fd)).get (), -2,
                         cwd,
                         env ? envvars : nullptr);

            ifdstream is (bin_mode (move (pr3.in_ofd)));
            o = is.read_binary ();

            r = pr2.wait () && r;
            r = pr3.wait () && r;
          }
          else
          {
            ifdstream is (bin_mode (move (pr.in_ofd)));
            o = is.read_binary ();
          }

          if (err)
          {
            // If STDERR is redirected to STDOUT then output will be
            // duplicated.
            //
            vector<char> v (in);
            in.reserve (in.size () * 2);
            in.insert (in.end (), v.begin (), v.end ());
          }

          r = in == o && r;
        }

        if (err && !out)
        {
          ifdstream is (bin_mode (move (pr.in_efd)));
          vector<char> e (is.read_binary ());

          r = in == e && r;
        }
      }
    }
    catch (const ios_base::failure&)
    {
      r = false;
    }

    bool s;
    return pr.wait () && pr.try_wait (s) && s && r;
  }
  catch (const process_error& e)
  {
    //cerr << args[0] << ": " << e << endl;

    if (e.child)
      exit (1);

    return false;
  }
}

static bool
exec (const path& p,
      const string& i,
      bool o = false,
      bool e = false,
      bool pipeline = false,
      dir_path wd = dir_path (),
      bool env = false)
{
  return exec (
    p, vector<char> (i.begin (), i.end ()), o, e, pipeline, false, wd, env);
}

int
main (int argc, const char* argv[])
{
  bool child (false);
  bool bin (false);
  dir_path wd;      // Working directory.
  bool env (false); // Check the environment variables.

  assert (argc > 0);

  int i (1);
  for (; i != argc; ++i)
  {
    string v (argv[i]);

    if (v == "-c")
      child = true;
    else if (v == "-b")
      bin = true;
    else if (v == "-e")
      env = true;
    else
    {
      if (!wd.empty ())
        break;

      try
      {
        wd = dir_path (v);
      }
      catch (const invalid_path&)
      {
        break;
      }
    }
  }

  if (i != argc)
  {
    if (!child)
      cerr << "usage: " << argv[0] << " [-c] [-b] [<dir>]" << endl;

    return 1;
  }

  path p;

  try
  {
    p = path (argv[0]);
  }
  catch (const invalid_path&)
  {
    if (child)
      return 1;

    assert (false);
  }

  if (child)
  {
    // Child process. Check if the working directory argument matches the
    // current directory if specified. Read input data if requested, optionally
    // write it to cout and/or cerr.
    //

    if (!wd.empty () && wd.realize () != dir_path::current_directory ())
      return 1;

    if (env)
    {
      // Check that the ABC variable is set, the DEF is unset and the XYZ is
      // left unchanged.
      //
      const char* v;
      if ((v = getenv ("ABC")) == nullptr || string ("1") != v ||
          getenv ("DEF") != nullptr ||
          (v = getenv ("XYZ")) == nullptr || string ("3") != v)
        return 1;
    }

    try
    {
      if (bin)
      {
        stdin_fdmode  (fdstream_mode::binary);
        stdout_fdmode (fdstream_mode::binary);
        stderr_fdmode (fdstream_mode::binary);
      }

      vector<char> data
        ((istreambuf_iterator<char> (cin)), istreambuf_iterator<char> ());

      cout.exceptions (istream::badbit);
      copy (data.begin (), data.end (), ostream_iterator<char> (cout));

      cerr.exceptions (istream::badbit);
      copy (data.begin (), data.end (), ostream_iterator<char> (cerr));
    }
    catch (const ios_base::failure&)
    {
      return 1;
    }

    return 0;
  }

  dir_path owd (dir_path::current_directory ());

  // Test processes created as "already terminated".
  //
  {
    process p;
    assert (!p.wait ()); // "Terminated" abnormally.
  }

  {
    // Note that if to create as just process(0) then the
    // process(const char* args[], int=0, int=1, int=2) ctor is being called.
    //
    process p (process_exit (0));
    assert (p.wait ()); // "Exited" successfully.
  }

  {
    process p (process_exit (1));
    assert (!p.wait ()); // "Exited" with an error.
  }

  const char* s ("ABC\nXYZ");

  assert (exec (p));
  assert (exec (p, s));
  assert (exec (p, s, true));
  assert (exec (p, s, true, false, true)); // Same but with piping.
  assert (exec (p, s, false, true));
  assert (exec (p, s, true, true));
  assert (exec (p, s, true, true, true)); // Same but with piping.

  // Passing environment variables to the child process.
  //
  // Note that we don't support (un)setting environment variables for the
  // child process on Windows.
  //
#ifndef _WIN32
  assert (exec (p, string (), false, false, false, dir_path (), true));
#endif

  // Transmit large binary data through the child.
  //
  vector<char> v;
  v.reserve (5000 * 256);
  for (size_t i (0); i < 5000; ++i)
  {
    char c (numeric_limits<char>::min ());

    do
    {
      v.push_back (c);
    }
    while (c++ != numeric_limits<char>::max ());
  }

  assert (exec (p, v, true, true));
  assert (exec (p, v, true, true, true)); // Same as above but with piping.

  // Execute the child using the full path.
  //
  path fp (p);
  fp.complete ();
  assert (exec (fp));

  // Execute the child using the relative path.
  //
  dir_path::current_directory (fp.directory ());

  assert (exec (dir_path (".") / fp.leaf ()));

  // Fail for non-existent file path.
  //
  assert (!exec (dir_path (".") / path ("dr")));

  // Execute the child using file name having PATH variable being properly set.
  //
  string paths (fp.directory ().string ());

  if (char const* s = getenv ("PATH"))
    paths += string (1, path::traits::path_separator) + s;

#ifndef _WIN32
  assert (setenv ("PATH", paths.c_str (), 1) == 0);
#else
  assert (_putenv (("PATH=" + paths).c_str ()) == 0);
#endif

  dir_path::current_directory (fp.directory () / dir_path (".."));

  assert (exec (fp.leaf ()));

  // Same as above but also with changing the child current directory.
  //
  assert (exec (
    fp.leaf (), vector<char> (), false, false, false, true, fp.directory ()));

#ifndef _WIN32
  // Check that wait() works properly if the underlying low-level wait
  // operation fails.
  //
  process pr;
  pr.handle = reinterpret_cast<process::handle_type> (-1);
  assert (!pr.wait (true) && !pr.wait (false));
#endif

  // Test execution of Windows batch files. The test file is in the original
  // working directory.
  //
#ifdef _WIN32
  {
    assert (exec (owd / "test.bat"));
    assert (exec (owd / "test"));

    paths = owd.string () + path::traits::path_separator + paths;
    assert (_putenv (("PATH=" + paths).c_str ()) == 0);

    assert (exec (path ("test.bat")));
    assert (exec (path ("test")));
    assert (!exec (path ("testX.bat")));
  }
#endif
}