// file      : build/test/rule.cxx -*- C++ -*-
// copyright : Copyright (c) 2014-2015 Code Synthesis Ltd
// license   : MIT; see accompanying LICENSE file

#include <build/test/rule>

#include <butl/process>

#include <build/scope>
#include <build/target>
#include <build/algorithm>
#include <build/diagnostics>

#include <build/config/utility> // add_options()

using namespace std;
using namespace butl;

namespace build
{
  namespace test
  {
    match_result rule::
    match (action a, target& t, const std::string&) const
    {
      // First determine if this is a test. This is controlled by
      // the test target variable and text.<tt> scope variables.
      // Also, it feels redundant to specify, say, "test = true"
      // and "test.output = test.out" -- the latter already says
      // this is a test. So take care of that as well.
      //
      bool r (false);
      lookup<const value> l;

      // @@ This logic doesn't take into account target type/pattern-
      // specific variables.
      //
      // @@ Perhaps a find_any(<list-of-vars>)?
      //
      for (auto p (t.vars.find_namespace ("test"));
           p.first != p.second;
           ++p.first)
      {
        const variable& var (p.first->first);
        const value& val (p.first->second);

        // If we have test, then always use that.
        //
        if (var.name == "test")
        {
          l = lookup<const value> (val, t);
          break;
        }

        // Otherwise check for variables that would indicate this
        // is a test.
        //
        if (var.name == "test.input"     ||
            var.name == "test.output"    ||
            var.name == "test.roundtrip" ||
            var.name == "test.options"   ||
            var.name == "test.arguments")
        {
          r = true;
          break;
        }
      }

      if (!r)
      {
        // See if there is a scope variable.
        //
        if (!l.defined ())
          l = t.base_scope ()[
            variable_pool.find (string("test.") + t.type ().name, bool_type)];

        r = l && as<bool> (*l);
      }

      // If this is the update pre-operation, then all we really need to
      // do is say we are not a match and the standard matching machinery
      // will (hopefully) find the rule to update this target.
      //
      // There is one thing that compilates this simple approach: test
      // input/output. While normally they will be existing (in src_base)
      // files, they could also be auto-generated. In fact, they could
      // only be needed for testing, which means the normall update won't
      // even know about them (nor clean, for that matter; this is why we
      // need cleantest).
      //
      // To make generated input/output work we will have to cause their
      // update ourselves. I other words, we may have to do some actual
      // work for (update, test), and not simply "guide" (update, 0) as
      // to which targets need updating. For how exactly we are going to
      // do it, see apply() below.
      //
      match_result mr (t, r);

      // If this is the update pre-operation, change the recipe action
      // to (update, 0) (i.e., "unconditional update").
      //
      if (r && a.operation () == update_id)
        mr.recipe_action = action (a.meta_operation (), update_id);

      return mr;
    }

    recipe rule::
    apply (action a, target& t, const match_result& mr) const
    {
      tracer trace ("test::rule::apply");

      if (!mr.bvalue) // Not a test.
        return noop_recipe;

      // Ok, if we are here, then this means:
      //
      // 1. This target is a test.
      // 2. The action is either
      //    a. (perform, test, 0) or
      //    b. (*, update, install)
      //
      // In both cases, the next step is to see if we have test.{input,
      // output,roundtrip}.
      //

      // First check the target-specific vars since they override any
      // scope ones.
      //
      auto il (t.vars["test.input"]);
      auto ol (t.vars["test.output"]);
      auto rl (t.vars["test.roundtrip"]);
      auto al (t.vars["test.arguments"]); // Should be input or arguments.

      if (al)
      {
        if (il)
          fail << "both test.input and test.arguments specified for "
               << "target " << t;

        if (rl)
          fail << "both test.roundtrip and test.arguments specified for "
               << "target " << t;
      }

      scope& bs (t.base_scope ());

      if (!il && !ol && !rl)
      {
        string n ("test.");
        n += t.type ().name;

        const variable& in (variable_pool.find (n + ".input", name_type));
        const variable& on (variable_pool.find (n + ".output", name_type));
        const variable& rn (variable_pool.find (n + ".roundtrip", name_type));

        // We should only keep value(s) that were specified together
        // in the innermost scope.
        //
        for (scope* s (&bs); s != nullptr; s = s->parent_scope ())
        {
          ol = s->vars[on];

          if (!al) // Not overriden at target level by test.arguments?
          {
            il = s->vars[in];
            rl = s->vars[rn];
          }

          if (il || ol || rl)
            break;
        }
      }

      const name* in;
      const name* on;

      // Reduce the roundtrip case to input/output.
      //
      if (rl)
      {
        if (il || ol)
          fail << "both test.roundtrip and test.input/output specified "
               << "for target " << t;

        in = on = &as<name> (*rl);
      }
      else
      {
        in = il ? &as<name> (*il) : nullptr;
        on = ol ? &as<name> (*ol) : nullptr;
      }

      // Resolve them to targets, which normally would be existing files
      // but could also be targets that need updating.
      //
      target* it (in != nullptr ? &search (*in, bs) : nullptr);
      target* ot (on != nullptr ? in == on ? it : &search (*on, bs) : nullptr);

      if (a.operation () == update_id)
      {
        // First see if input/output are existing, up-to-date files. This
        // is a common case optimization.
        //
        if (it != nullptr)
        {
          build::match (a, *it);

          if (it->state () == target_state::unchanged)
          {
            unmatch (a, *it);
            it = nullptr;
          }
        }

        if (ot != nullptr)
        {
          if (in != on)
          {
            build::match (a, *ot);

            if (ot->state () == target_state::unchanged)
            {
              unmatch (a, *ot);
              ot = nullptr;
            }
          }
          else
            ot = it;
        }


        // Find the "real" update rule, that is, the rule that would
        // have been found if we signalled that we do not match from
        // match() above.
        //
        recipe d (match_delegate (a, t).first);

        // If we have no input/output that needs updating, then simply
        // redirect to it.
        //
        if (it == nullptr && ot == nullptr)
          return d;

        // Ok, time to handle the worst case scenario: we need to
        // cause update of input/output targets and also delegate
        // to the real update.
        //
        return [it, ot, dr = move (d)] (action a, target& t) -> target_state
        {
          // Do the general update first.
          //
          target_state r (execute_delegate (dr, a, t));

          if (it != nullptr)
            r |= execute (a, *it);

          if (ot != nullptr)
            r |= execute (a, *ot);

          return r;
        };
      }
      else
      {
        // Cache the targets in our prerequsite targets lists where they
        // can be found by perform_test(). If we have either or both,
        // then the first entry is input and the second -- output (either
        // can be NULL).
        //
        if (it != nullptr || ot != nullptr)
        {
          auto& pts (t.prerequisite_targets);
          pts.resize (2, nullptr);
          pts[0] = it;
          pts[1] = ot;
        }

        return &perform_test;
      }
    }

    static void
    add_arguments (cstrings& args, const target& t, const char* n)
    {
      string var ("test.");
      var += n;

      auto l (t.vars[var]);

      if (!l)
      {
        var.resize (5);
        var += t.type ().name;
        var += '.';
        var += n;
        l = t.base_scope ()[variable_pool.find (var, strings_type)];
      }

      if (l)
        config::append_options (args, as<strings> (*l));
    }

    // The format of args shall be:
    //
    // name1 arg arg ... nullptr
    // name2 arg arg ... nullptr
    // ...
    // nameN arg arg ... nullptr nullptr
    //
    static bool
    run_test (target& t,
              diag_record& dr,
              char const** args,
              process* prev = nullptr)
    {
      // Find the next process, if any.
      //
      char const** next (args);
      for (next++; *next != nullptr; next++) ;
      next++;

      // Redirect stdout to a pipe unless we are last, in which
      // case redirect it to stderr.
      //
      int out (*next == nullptr ? 2 : -1);
      bool pr, wr;

      try
      {
        if (prev == nullptr)
        {
          // First process.
          //
          process p (args, 0, out);
          pr = *next == nullptr || run_test (t, dr, next, &p);
          wr = p.wait ();
        }
        else
        {
          // Next process.
          //
          process p (args, *prev, out);
          pr = *next == nullptr || run_test (t, dr, next, &p);
          wr = p.wait ();
        }
      }
      catch (const process_error& e)
      {
        error << "unable to execute " << args[0] << ": " << e.what ();

        if (e.child ())
          exit (1);

        throw failed ();
      }

      if (!wr)
      {
        if (pr) // First failure?
          dr << fail << "test " << t << " failed"; // Multi test: test 1.

        dr << error << "non-zero exit status: ";
        print_process (dr, args);
      }

      return pr && wr;
    }

    target_state rule::
    perform_test (action, target& t)
    {
      // @@ Would be nice to print what signal/core was dumped.
      //
      // @@ Doesn't have to be a file target if we have test.cmd.
      //

      file& ft (static_cast<file&> (t));
      assert (!ft.path ().empty ()); // Should have been assigned by update.

      cstrings args {ft.path ().string ().c_str ()};

      // Do we have options?
      //
      add_arguments (args, t, "options");

      // Do we have input?
      //
      auto& pts (t.prerequisite_targets);
      if (pts.size () != 0 && pts[0] != nullptr)
      {
        file& it (static_cast<file&> (*pts[0]));
        assert (!it.path ().empty ()); // Should have been assigned by update.
        args.push_back (it.path ().string ().c_str ());
      }
      // Maybe arguments then?
      //
      else
        add_arguments (args, t, "arguments");

      args.push_back (nullptr);

      // Do we have output?
      //
      if (pts.size () != 0 && pts[1] != nullptr)
      {
        file& ot (static_cast<file&> (*pts[1]));
        assert (!ot.path ().empty ()); // Should have been assigned by update.

        args.push_back ("diff");
        args.push_back ("-u");
        args.push_back (ot.path ().string ().c_str ());
        args.push_back ("-");
        args.push_back (nullptr);
      }

      args.push_back (nullptr); // Second.

      if (verb >= 2)
        print_process (args);
      else if (verb)
        text << "test " << t;

      {
        diag_record dr;

        if (!run_test (t, dr, args.data ()))
        {
          dr << info << "test command line: ";
          print_process (dr, args);
        }
      }

      return target_state::changed;
    }
  }
}