// Broken - Lightweight unit testing for C++
//
// This is free and unencumbered software released into the public domain.
//
// Anyone is free to copy, modify, publish, use, compile, sell, or
// distribute this software, either in source code form or as a compiled
// binary, for any purpose, commercial or non-commercial, and by any
// means.
//
// In jurisdictions that recognize copyright laws, the author or authors
// of this software dedicate any and all copyright interest in the
// software to the public domain. We make this dedication for the benefit
// of the public at large and to the detriment of our heirs and
// successors. We intend this dedication to be an overt act of
// relinquishment in perpetuity of all present and future rights to this
// software under copyright law.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
//
// For more information, please refer to <http://unlicense.org>

#include "broken.h"
#include <stdarg.h>

// Broken - Globals
// ================

// Zero initialized globals.
struct BrokenGlobal {
  // Application arguments.
  int _argc;
  const char** _argv;

  // Output file.
  FILE* _file;

  // Unit tests.
  BrokenAPI::Unit* _unit_list;
  BrokenAPI::Unit* _unit_running;

  bool has_arg(const char* a) const noexcept {
    for (int i = 1; i < _argc; i++)
      if (strcmp(_argv[i], a) == 0)
        return true;
    return false;
  }

  inline FILE* file() const noexcept { return _file ? _file : stdout; }
};
static BrokenGlobal _broken_global;

// Broken - API
// ============

// Get whether the string `a` starts with string `b`.
static bool BrokenAPI_startsWith(const char* a, const char* b) noexcept {
  for (size_t i = 0; ; i++) {
    if (b[i] == '\0') return true;
    if (a[i] != b[i]) return false;
  }
}

//! Compares names and priority of two unit tests.
static int BrokenAPI_compareUnits(const BrokenAPI::Unit* a, const BrokenAPI::Unit* b) noexcept {
  if (a->priority == b->priority)
    return strcmp(a->name, b->name);
  else
    return a->priority > b->priority ? 1 : -1;
}

// Get whether the strings `a` and `b` are equal, ignoring case and treating
// `-` as `_`.
static bool BrokenAPI_matchesFilter(const char* a, const char* b) noexcept {
  for (size_t i = 0; ; i++) {
    int ca = (unsigned char)a[i];
    int cb = (unsigned char)b[i];

    // If filter is defined as wildcard the rest automatically matches.
    if (cb == '*')
      return true;

    if (ca == '-') ca = '_';
    if (cb == '-') cb = '_';

    if (ca >= 'A' && ca <= 'Z') ca += 'a' - 'A';
    if (cb >= 'A' && cb <= 'Z') cb += 'a' - 'A';

    if (ca != cb)
      return false;

    if (ca == '\0')
      return true;
  }
}

static bool BrokenAPI_canRun(BrokenAPI::Unit* unit) noexcept {
  BrokenGlobal& global = _broken_global;

  int i, argc = global._argc;
  const char** argv = global._argv;

  const char* unit_name = unit->name;
  bool has_filter = false;

  for (i = 1; i < argc; i++) {
    const char* arg = argv[i];

    if (BrokenAPI_startsWith(arg, "--run-") && strcmp(arg, "--run-all") != 0) {
      has_filter = true;

      if (BrokenAPI_matchesFilter(unit_name, arg + 6))
        return true;
    }
  }

  // If no filter has been specified the default is to run.
  return !has_filter;
}

static void BrokenAPI_runUnit(BrokenAPI::Unit* unit) noexcept {
  BrokenAPI::info("Running %s", unit->name);

  _broken_global._unit_running = unit;
  unit->entry();
  _broken_global._unit_running = NULL;
}

static void BrokenAPI_runAll() noexcept {
  BrokenAPI::Unit* unit = _broken_global._unit_list;

  bool has_units = unit != NULL;
  size_t count = 0;
  int current_priority = 0;

  while (unit != NULL) {
    if (BrokenAPI_canRun(unit)) {
      if (current_priority != unit->priority) {
        if (count)
          INFO("");
        INFO("[[Priority=%d]]", unit->priority);
      }

      current_priority = unit->priority;
      BrokenAPI_runUnit(unit);
      count++;
    }
    unit = unit->next;
  }

  if (count) {
    INFO("\nSuccess:");
    INFO("  All tests passed!");
  }
  else {
    INFO("\nWarning:");
    INFO("  No units %s!", has_units ? "matched the filter" : "defined");
  }
}

static void BrokenAPI_listAll() noexcept {
  BrokenAPI::Unit* unit = _broken_global._unit_list;

  if (unit != NULL) {
    INFO("Units:");
    do {
      INFO("  %s [priority=%d]", unit->name, unit->priority);
      unit = unit->next;
    } while (unit != NULL);
  }
  else {
    INFO("Warning:");
    INFO("  No units defined!");
  }
}

bool BrokenAPI::has_arg(const char* name) noexcept {
  return _broken_global.has_arg(name);
}

void BrokenAPI::add_unit(Unit* unit) noexcept {
  Unit** pPrev = &_broken_global._unit_list;
  Unit* current = *pPrev;

  // C++ static initialization doesn't guarantee anything. We sort all units by
  // name so the execution will always happen in deterministic order.
  while (current != NULL) {
    if (BrokenAPI_compareUnits(current, unit) >= 0)
      break;

    pPrev = &current->next;
    current = *pPrev;
  }

  *pPrev = unit;
  unit->next = current;
}

void BrokenAPI::set_output_file(FILE* file) noexcept {
  BrokenGlobal& global = _broken_global;

  global._file = file;
}

int BrokenAPI::run(int argc, const char* argv[], Entry on_before_run, Entry on_after_run) {
  BrokenGlobal& global = _broken_global;

  global._argc = argc;
  global._argv = argv;

  if (global.has_arg("--help")) {
    INFO("Options:");
    INFO("  --help    - print this usage");
    INFO("  --list    - list all tests");
    INFO("  --run-... - run a test(s), trailing wildcards supported");
    INFO("  --run-all - run all tests (default)");
    return 0;
  }

  if (global.has_arg("--list")) {
    BrokenAPI_listAll();
    return 0;
  }

  if (on_before_run)
    on_before_run();

  // We don't care about filters here, it's implemented by `run_all`.
  BrokenAPI_runAll();

  if (on_after_run)
    on_after_run();

  return 0;
}

static void BrokenAPI_printMessage(const char* prefix, const char* fmt, va_list ap) noexcept {
  BrokenGlobal& global = _broken_global;
  FILE* dst = global.file();

  if (!fmt || fmt[0] == '\0') {
    fprintf(dst, "\n");
  }
  else {
    // This looks scary, but we really want to use only a single call to vfprintf()
    // in multithreaded code. So we change the format a bit if necessary.
    enum : unsigned { kBufferSize = 512 };
    char static_buffer[512];

    size_t fmt_size = strlen(fmt);
    size_t prefix_size = strlen(prefix);

    char* fmt_buf = static_buffer;
    if (fmt_size > kBufferSize - 2 - prefix_size)
      fmt_buf = static_cast<char*>(malloc(fmt_size + prefix_size + 2));

    if (!fmt_buf) {
      fprintf(dst, "%s_cannot allocate buffer for vfprintf()\n", prefix);
    }
    else {
      memcpy(fmt_buf, prefix, prefix_size);
      memcpy(fmt_buf + prefix_size, fmt, fmt_size);

      fmt_size += prefix_size;
      if (fmt_buf[fmt_size - 1] != '\n')
        fmt_buf[fmt_size++] = '\n';
      fmt_buf[fmt_size] = '\0';

      vfprintf(dst, fmt_buf, ap);

      if (fmt_buf != static_buffer)
        free(fmt_buf);
    }
  }

  fflush(dst);
}

void BrokenAPI::info(const char* fmt, ...) noexcept {
  BrokenGlobal& global = _broken_global;

  va_list ap;
  va_start(ap, fmt);
  BrokenAPI_printMessage(global._unit_running ? "  " : "", fmt, ap);
  va_end(ap);
}

void BrokenAPI::fail(const char* file, int line, const char* expression, const char* fmt, ...) noexcept {
  BrokenGlobal& global = _broken_global;
  FILE* dst = global.file();

  fprintf(dst, "  FAILED: %s\n", expression);

  if (fmt) {
    va_list ap;
    va_start(ap, fmt);
    BrokenAPI_printMessage("  REASON: ", fmt, ap);
    va_end(ap);
  }

  fprintf(dst, "  SOURCE: %s (Line: %d)\n", file, line);
  fflush(dst);

  abort();
}
