////////////////////////////////////////////////////////////////////////////////
//
// Copyright 2020, Shaun Ruffell, Thomas Lauf.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// 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 OR COPYRIGHT HOLDERS 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.
//
// https://www.opensource.org/licenses/mit-license.php
//
////////////////////////////////////////////////////////////////////////////////

#include <sstream>
#include <memory>
#include <string>
#include <unistd.h>
#include <iostream>
#include <cstdlib>
#include <fstream>
#include <cmake.h>
#include <timew.h>
#include <test.h>
#include <AtomicFile.h>
#include <FS.h>

#ifdef FIU_ENABLE

#include <fiu.h>
#include <fiu-control.h>

#else

#define fiu_init(flags) 0
#define fiu_fail(name) 0
#define fiu_failinfo() NULL
#define fiu_do_on(name, action)
#define fiu_exit_on(name)
#define fiu_return_on(name, retval)

#endif /* FIU_ENABLE */

////////////////////////////////////////////////////////////////////////////////
class TempDir
{
public:
  TempDir ();
  ~TempDir ();

  bool is_empty () const;
  void clear ();
  std::vector <std::string> file_names () const;

private:
  std::string tmpName {};
  std::string oldDir {};
};

////////////////////////////////////////////////////////////////////////////////
TempDir::TempDir ()
{
  oldDir = Directory::cwd ();

  char template_name[] = "atomic_XXXXXX";
  const char *cwd = ::mkdtemp (template_name);
  if (cwd == nullptr)
  {
    throw std::string ("Failed to create temp directory");
  }

  tmpName = cwd;

  if (::chdir (tmpName.c_str ()))
  {
    throw std::string ("Failed to change to temporary directory");
  }
}

////////////////////////////////////////////////////////////////////////////////
TempDir::~TempDir ()
{
  try
  {
    clear ();
    ::chdir (oldDir.c_str ());

    if (!Directory(tmpName).remove ())
    {
      std::cerr << "Failed to remove temp dir " << tmpName << '\n';
    }
  }
  catch (...)
  {
    std::cerr << "Unhandled exception in " << __func__ << '\n';
  }
}

////////////////////////////////////////////////////////////////////////////////
bool TempDir::is_empty () const
{
  return file_names().empty ();
}

////////////////////////////////////////////////////////////////////////////////
std::vector <std::string> TempDir::file_names () const
{
  return Path (tmpName).glob ("*");
}

////////////////////////////////////////////////////////////////////////////////
void TempDir::clear ()
{
  for (const auto& file : file_names ())
  {
    File::remove (file);
  }
}

#ifdef FIU_ENABLE

// This class is a helper to make sure we turn off libfiu after the test so that
// we can properly write to stdout / stderr.
class FIU
{
public:
  FIU()
  {
    enable();
  }

  FIU(const FIU&) = delete;
  FIU& operator= (const FIU&) = delete;

  ~FIU()
  {
    disable ();
    std::cout << cbuffer.str();
    std::stringstream().swap(cbuffer);
  }

  void enable ()
  {
    for (auto test_point : test_points)
    {
      fiu_enable_external(test_point, 1, NULL, 0, fiu_cb);
    }
  }

  void disable ()
  {
    for (auto test_point : test_points)
    {
      fiu_disable(test_point);
    }
  }

private:
  static const std::vector <const char *> test_points;
  static std::stringstream cbuffer;

  static int external_cb_was_called;

  static int fiu_cb(const char *name, int *failnum,
                    void **failinfo, unsigned int *flags)
  {
    (void)name;
    (void)flags;
    external_cb_was_called++;

    // For debugging the tests themselves...
    // cbuffer << "fiu_cb called for " << name << '\n';

    *failinfo = (void *) EIO;

    return *failnum;
  }
};

const std::vector <const char *> FIU::test_points {
  "posix/stdio/gp/fputs",
  "posix/io/rw/write",
};

std::stringstream FIU::cbuffer;
int FIU::external_cb_was_called = 0;

//////////////////////////////////////////////////////////////////////////////
// Since AtomicFile is primarily for keeping the database consistent in the
// presence of filesystem errors, these tests use libfiu to ensure that
// AtomicFile behaves as intended when the underlying system calls fail.
int fiu_test (UnitTest& t)
{
  TempDir tempDir;
  fiu_init (0);

  try { FIU fiu; AtomicFile ("test.txt").write_raw("This is  test"); t.fail ("AtomicFile::write_raw throws on error"); }
  catch (...) {                                                      t.pass ("AtomicFile::write_raw throws on error"); }  

  try { FIU fiu; AtomicFile::finalize_all ();                        t.fail ("AtomicFile::finalize_all() throws on error"); }
  catch (...) {                                                      t.pass ("AtomicFile::finalize_all() throws on error"); }

  try { FIU fiu; AtomicFile::reset (); AtomicFile::finalize_all ();  t.pass ("AtomicFile::reset clears failure state"); }
  catch (...) {                                                      t.fail ("AtomicFile::reset clears failure state"); }

  File::write ("test.txt", "line1\n");
  {
    AtomicFile file ("test.txt");
    try { FIU fiu; file.append ("append1\n");                        t.fail ("AtomicFile::append throws on error"); }
    catch (...) {                                                    t.pass ("AtomicFile::append throws on error"); }

    std::string contents {"should-not-see-this"};
    file.read (contents);
    t.is (contents, "", "AtomicFile::append did not partially fill the file.");
  }

  try { FIU fiu; AtomicFile::finalize_all ();                       t.fail ("AtomicFile::append failures prevent finalization"); }
  catch (...) {                                                     t.pass ("AtomicFile::append failures prevent finalization"); }

  return 0;
}
#else
int fiu_test (UnitTest& t)
{
  t.skip ("AtomicFile::write_raw throws on error");
  t.skip ("AtomicFile::finalize_all() throws on error");
  t.skip ("AtomicFile::reset clears failure state");
  t.skip ("AtomicFile::append throws on error");
  t.skip ("AtomicFile::append did not partially fill the file.");
  t.skip ("AtomicFile::append failures prevent finalization");
  return 0;
}
#endif // FIU_ENABLE

//////////////////////////////////////////////////////////////////////////////
int test (UnitTest& t)
{
  std::string test_name;

  // This will create and change to a temporary directory that will be cleaned
  // up when the destructor is run
  TempDir tempDir;

  std::string goldenText = "1\n";
  std::string contents;
  std::string expected;

  Path firstFilename ("test-1.txt");
  Path secondFilename ("test-2.txt");

  {
    AtomicFile file (firstFilename);
    file.write_raw (goldenText);
    file.close ();
  }

  t.is (firstFilename.exists (), false, "Shall not exists before finalize");
  AtomicFile::finalize_all ();
  t.is (firstFilename.exists (), true, "Shall exists after finalize");
  File::read(firstFilename, contents);
  t.is (contents == goldenText, true, "Shall have the correct data");

  tempDir.clear ();

  {
    AtomicFile first (firstFilename);
    AtomicFile second (secondFilename);

    first.write_raw ("first\n");
    second.write_raw ("second\n");
    first.close ();
    second.close ();
  }

  t.is (firstFilename.exists () || secondFilename.exists (), false, "Neither shall exist before finalize");

  AtomicFile::finalize_all ();

  t.is (firstFilename.exists () && secondFilename.exists (), true, "Both shall exists after finalize");

  File::read(firstFilename, contents);
  test_name = "First file shall contain the correct data";
  expected = "first\n";
  if (contents == expected)
  {
    t.pass (test_name);
  }
  else
  {
    t.fail (test_name);
    t.diag (std::string ("Expected '" + expected + "' read '" + contents + "'"));
  }

  File::read(secondFilename, contents);
  test_name = "Second file shall contain the correct data";
  if (contents == std::string("second\n"))
  {
    t.pass (test_name);
  }
  else
  {
    t.fail (test_name);
    t.diag (std::string ("Expected 'second\n' read '" + contents + "'"));
  }

  // Make sure appending works

  test_name = "Appending does not update original before finalize";
  expected = "first\n";

  {
    AtomicFile first (firstFilename);

    first.append("append 1\n");
    first.append("append 2\n");
    first.close ();
  }

  File::read (firstFilename, contents);
  if (contents == expected)
  {
    t.pass (test_name);
  }
  else
  {
    t.fail (test_name);
    t.diag (std::string ("Expected '" + expected + "' read '" + contents + "'"));
  }

  test_name = "Finalizing updates the appended data";
  expected = "first\nappend 1\nappend 2\n";
  AtomicFile::finalize_all ();
  File::read (firstFilename, contents);
  if (contents == expected)
  {
    t.pass (test_name);
  }
  else
  {
    t.fail (test_name);
    t.diag (std::string ("Expected '" + expected + "' read '" + contents + "'"));
  }

  test_name = "Read from Atomicfile";
  // We do not want to update the expected
  {
    AtomicFile::read (firstFilename, contents);
  }

  if (contents == expected)
  {
    t.pass (test_name);
  }
  else
  {
    t.fail (test_name);
    t.diag (std::string ("Expected '" + expected + "' read '" + contents + "'"));
  }

  // If we read from an atomic file before finalizing, we should get the data
  // that was written to the temporary file and not the 'real' file.

  test_name = "Read from Atomicfile should read unfinalized data";
  expected += "expected\n";
  AtomicFile::write (firstFilename, expected);
  AtomicFile::read (firstFilename, contents);
  if (contents == expected)
  {
    t.pass (test_name);
  }
  else
  {
    t.fail (test_name);
    t.diag (std::string ("Expected '" + expected + "' read '" + contents + "'"));
  }

  AtomicFile::finalize_all ();
  {
    AtomicFile first (firstFilename);
    AtomicFile second (firstFilename);

    first.write_raw("first\n");
    second.append ("second\n");

    AtomicFile::finalize_all ();

    first.append ("third\n");
    second.append ("fourth\n");
    first.append ("fifth\n");

  }

  test_name = "Two AtomicFiles should access same temp file (part 1)";

  // The atomic files, which were closed above should return all the strings
  // written since it was not yet finalized.

  expected = "first\nsecond\nthird\nfourth\nfifth\n";
  AtomicFile::read (firstFilename, contents);
  if (contents == expected)
  {
    t.pass (test_name);
  }
  else
  {
    t.fail (test_name);
    t.diag (std::string ("Expected '" + expected + "' read '" + contents + "'"));
  }

  // But since the file was not yet finalized, the "real" file should only
  // contain the data present since before the finalize_all () call.

  test_name = "Two AtomicFiles should access same temp file (part 2)";
  expected = "first\nsecond\n";
  File::read (firstFilename, contents);
  if (contents == expected)
  {
    t.pass (test_name);
  }
  else
  {
    t.fail (test_name);
    t.diag (std::string ("Expected '" + expected + "' read '" + contents + "'"));
  }

  // After we finalize the data, the AtomicFile and the File should now both
  // return the same information

  test_name = "Two AtomicFiles should access same temp file (part 3)";
  AtomicFile::finalize_all ();
  File::read (firstFilename, expected);
  AtomicFile::read (firstFilename, contents);
  if (contents == expected)
  {
    t.pass (test_name);
  }
  else
  {
    t.fail (test_name);
    t.diag (std::string ("Expected '" + expected + "' read '" + contents + "'"));
  }

  {
    AtomicFile test ("test");
  }

  AtomicFile::reset ();
  return 0;
}

int main (int, char**)
{
  UnitTest t (20);
  try
  {
    int ret = test (t);
    int fiu_ret = fiu_test (t);
    return (ret == 0) ? fiu_ret : ret;
  }
  catch (const std::string& error)
  {
    std::cerr << "Test threw exception: " << error << '\n';
  }
  catch (...)
  {
    std::cerr << "Uncaught exception.\n";
  }
}

////////////////////////////////////////////////////////////////////////////////

