/* io-test.c --- tests for some i/o functions
 *
 * ====================================================================
 *    Licensed to the Apache Software Foundation (ASF) under one
 *    or more contributor license agreements.  See the NOTICE file
 *    distributed with this work for additional information
 *    regarding copyright ownership.  The ASF licenses this file
 *    to you under the Apache License, Version 2.0 (the
 *    "License"); you may not use this file except in compliance
 *    with the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing,
 *    software distributed under the License is distributed on an
 *    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *    KIND, either express or implied.  See the License for the
 *    specific language governing permissions and limitations
 *    under the License.
 * ====================================================================
 */

#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <stdio.h>

#include <apr.h>

#include "svn_pools.h"
#include "svn_string.h"
#include "private/svn_skel.h"

#include "../svn_test.h"
#include "../svn_test_fs.h"


/* Helpers to create the test data directory. */

#define TEST_DIR "io-test-temp"

/* The definition for the test data files. */
struct test_file_definition_t
  {
    /* The name of the test data file. */
    const char* const name;

    /* The string needs to contain up to 5 bytes, they
     * are interpreded as:
     * - first byte
     * - filler between first and medium byte
     * - medium byte (the byte in the middle of the file)
     * - filler between medium and last byte
     * - last byte.
     * If the string is shorter than the file length,
     * the test will fail. */
    const char* const data;

    /* The size of the file actually to create. */
    const apr_off_t size;

    /* The created path of the file. Will be filled in
     * by create_test_file() */
    char* created_path;
  };

struct test_file_definition_t test_file_definitions[] =
  {
    {"empty",                 "",      0},
    {"single_a",              "a",     1},
    {"single_b",              "b",     1},
    {"hundred_a",             "aaaaa", 100},
    {"hundred_b",             "bbbbb", 100},
    {"hundred_b1",            "baaaa", 100},
    {"hundred_b2",            "abaaa", 100},
    {"hundred_b3",            "aabaa", 100},
    {"hundred_b4",            "aaaba", 100},
    {"hundred_b5",            "aaaab", 100},
    {"chunk_minus_one_a",     "aaaaa", SVN__STREAM_CHUNK_SIZE - 1},
    {"chunk_minus_one_b1",    "baaaa", SVN__STREAM_CHUNK_SIZE - 1},
    {"chunk_minus_one_b2",    "abaaa", SVN__STREAM_CHUNK_SIZE - 1},
    {"chunk_minus_one_b3",    "aabaa", SVN__STREAM_CHUNK_SIZE - 1},
    {"chunk_minus_one_b4",    "aaaba", SVN__STREAM_CHUNK_SIZE - 1},
    {"chunk_minus_one_b5",    "aaaab", SVN__STREAM_CHUNK_SIZE - 1},
    {"chunk_a",               "aaaaa", SVN__STREAM_CHUNK_SIZE},
    {"chunk_b1",              "baaaa", SVN__STREAM_CHUNK_SIZE},
    {"chunk_b2",              "abaaa", SVN__STREAM_CHUNK_SIZE},
    {"chunk_b3",              "aabaa", SVN__STREAM_CHUNK_SIZE},
    {"chunk_b4",              "aaaba", SVN__STREAM_CHUNK_SIZE},
    {"chunk_b5",              "aaaab", SVN__STREAM_CHUNK_SIZE},
    {"chunk_plus_one_a",      "aaaaa", SVN__STREAM_CHUNK_SIZE + 1},
    {"chunk_plus_one_b1",     "baaaa", SVN__STREAM_CHUNK_SIZE + 1},
    {"chunk_plus_one_b2",     "abaaa", SVN__STREAM_CHUNK_SIZE + 1},
    {"chunk_plus_one_b3",     "aabaa", SVN__STREAM_CHUNK_SIZE + 1},
    {"chunk_plus_one_b4",     "aaaba", SVN__STREAM_CHUNK_SIZE + 1},
    {"chunk_plus_one_b5",     "aaaab", SVN__STREAM_CHUNK_SIZE + 1},
    {"twochunk_minus_one_a",  "aaaaa", SVN__STREAM_CHUNK_SIZE*2 - 1},
    {"twochunk_minus_one_b1", "baaaa", SVN__STREAM_CHUNK_SIZE*2 - 1},
    {"twochunk_minus_one_b2", "abaaa", SVN__STREAM_CHUNK_SIZE*2 - 1},
    {"twochunk_minus_one_b3", "aabaa", SVN__STREAM_CHUNK_SIZE*2 - 1},
    {"twochunk_minus_one_b4", "aaaba", SVN__STREAM_CHUNK_SIZE*2 - 1},
    {"twochunk_minus_one_b5", "aaaab", SVN__STREAM_CHUNK_SIZE*2 - 1},
    {"twochunk_a",            "aaaaa", SVN__STREAM_CHUNK_SIZE*2},
    {"twochunk_b1",           "baaaa", SVN__STREAM_CHUNK_SIZE*2},
    {"twochunk_b2",           "abaaa", SVN__STREAM_CHUNK_SIZE*2},
    {"twochunk_b3",           "aabaa", SVN__STREAM_CHUNK_SIZE*2},
    {"twochunk_b4",           "aaaba", SVN__STREAM_CHUNK_SIZE*2},
    {"twochunk_b5",           "aaaab", SVN__STREAM_CHUNK_SIZE*2},
    {"twochunk_plus_one_a",   "aaaaa", SVN__STREAM_CHUNK_SIZE*2 + 1},
    {"twochunk_plus_one_b1",  "baaaa", SVN__STREAM_CHUNK_SIZE*2 + 1},
    {"twochunk_plus_one_b2",  "abaaa", SVN__STREAM_CHUNK_SIZE*2 + 1},
    {"twochunk_plus_one_b3",  "aabaa", SVN__STREAM_CHUNK_SIZE*2 + 1},
    {"twochunk_plus_one_b4",  "aaaba", SVN__STREAM_CHUNK_SIZE*2 + 1},
    {"twochunk_plus_one_b5",  "aaaab", SVN__STREAM_CHUNK_SIZE*2 + 1},
    {0},
  };

/* Function to prepare a single test file */

static svn_error_t *
create_test_file(struct test_file_definition_t* definition,
                 apr_pool_t *pool,
                 apr_pool_t *scratch_pool)
{
  apr_status_t status = 0;
  apr_file_t *file_h;
  apr_off_t midpos = definition->size / 2;
  svn_error_t *err = NULL;
  int i;

  if (definition->size < 5)
    SVN_ERR_ASSERT(strlen(definition->data) >= (apr_size_t)definition->size);
  else
    SVN_ERR_ASSERT(strlen(definition->data) >= 5);


  definition->created_path = svn_dirent_join(TEST_DIR,
                                  definition->name,
                                  pool);

  SVN_ERR(svn_io_file_open(&file_h,
                           definition->created_path,
                           (APR_WRITE | APR_CREATE | APR_EXCL | APR_BUFFERED),
                           APR_OS_DEFAULT,
                           scratch_pool));

  for (i=1; i <= definition->size; i += 1)
    {
      char c;
      if (i == 1)
        c = definition->data[0];
      else if (i < midpos)
        c = definition->data[1];
      else if (i == midpos)
        c = definition->data[2];
      else if (i < definition->size)
        c = definition->data[3];
      else
        c = definition->data[4];

      status = apr_file_putc(c, file_h);

      if (status)
        break;
    }

  if (status)
    err = svn_error_wrap_apr(status, "Can't write to file '%s'",
                              definition->name);

  return svn_error_compose_create(err,
                        svn_io_file_close(file_h, scratch_pool));
}

/* Function to prepare the whole set of on-disk files to be compared. */
static svn_error_t *
create_comparison_candidates(apr_pool_t *scratch_pool)
{
  svn_node_kind_t kind;
  apr_pool_t *iterpool = svn_pool_create(scratch_pool);
  struct test_file_definition_t *candidate;
  svn_error_t *err = SVN_NO_ERROR;

  /* If there's already a directory named io-test-temp, delete it.
     Doing things this way means that repositories stick around after
     a failure for postmortem analysis, but also that tests can be
     re-run without cleaning out the repositories created by prior
     runs.  */
  SVN_ERR(svn_io_check_path(TEST_DIR, &kind, scratch_pool));

  if (kind == svn_node_dir)
    SVN_ERR(svn_io_remove_dir2(TEST_DIR, TRUE, NULL, NULL, scratch_pool));
  else if (kind != svn_node_none)
    return svn_error_createf(SVN_ERR_TEST_FAILED, NULL,
                             "There is already a file named '%s'",
                             TEST_DIR);

  SVN_ERR(svn_io_dir_make(TEST_DIR, APR_OS_DEFAULT, scratch_pool));

  svn_test_add_dir_cleanup(TEST_DIR);

  for (candidate = test_file_definitions;
       candidate->name != NULL;
       candidate += 1)
    {
      svn_pool_clear(iterpool);
      err = create_test_file(candidate, scratch_pool, iterpool);
      if (err)
        break;
    }

  svn_pool_destroy(iterpool);

  return err;
}


/* Functions to check the 2-way and 3-way file comparison functions.  */

/* Test 2-way file size checking */
static svn_error_t *
test_two_file_size_comparison(apr_pool_t *scratch_pool)
{
  struct test_file_definition_t *inner, *outer;
  svn_boolean_t actual;
  svn_boolean_t expected;
  svn_error_t *err = SVN_NO_ERROR;
  svn_error_t *cmp_err;
  apr_pool_t *iterpool = svn_pool_create(scratch_pool);

  SVN_ERR(create_comparison_candidates(scratch_pool));

  for (outer = test_file_definitions; outer->name != NULL; outer += 1)
    {
#ifdef SVN_IO_TEST_ALL_PERMUTATIONS
      inner = test_file_definitions;
#else
      inner = outer;
#endif
      for (; inner->name != NULL; inner += 1)
        {
          svn_pool_clear(iterpool);

          expected = inner->size != outer->size;

          cmp_err = svn_io_filesizes_different_p(&actual,
                                                 inner->created_path,
                                                 outer->created_path,
                                                 iterpool);

          if (cmp_err)
            {
              err = svn_error_compose_create(err, cmp_err);
            }
          else if (expected != actual)
            {
              err = svn_error_compose_create(err,
                  svn_error_createf(SVN_ERR_TEST_FAILED, NULL,
                                   "size comparison problem: '%s' and '%s'",
                                   inner->created_path,
                                   outer->created_path));
            }
        }
    }

  svn_pool_destroy(iterpool);
  return err;
}


/* Test 2-way file content checking */
static svn_error_t *
test_two_file_content_comparison(apr_pool_t *scratch_pool)
{
  struct test_file_definition_t *inner, *outer;
  svn_boolean_t actual;
  svn_boolean_t expected;
  svn_error_t *err = SVN_NO_ERROR;
  svn_error_t *cmp_err;
  apr_pool_t *iterpool = svn_pool_create(scratch_pool);

  SVN_ERR(create_comparison_candidates(scratch_pool));

  for (outer = test_file_definitions; outer->name != NULL; outer += 1)
    {
#ifdef SVN_IO_TEST_ALL_PERMUTATIONS
      inner = test_file_definitions;
#else
      inner = outer;
#endif
      for (; inner->name != NULL; inner += 1)
        {
          svn_pool_clear(iterpool);

          expected = inner->size == outer->size
            && strcmp(inner->data, outer->data) == 0;

          cmp_err = svn_io_files_contents_same_p(&actual,
                                                 inner->created_path,
                                                 outer->created_path,
                                                 iterpool);

          if (cmp_err)
            {
              err = svn_error_compose_create(err, cmp_err);
            }
          else
            {
              if (expected != actual)
                  err = svn_error_compose_create(err,
                      svn_error_createf(SVN_ERR_TEST_FAILED, NULL,
                                 "content comparison problem: '%s' and '%s'",
                                 inner->created_path,
                                 outer->created_path));
            }
        }
    }

  svn_pool_destroy(iterpool);
  return err;
}


/* Test 3-way file size checking */
static svn_error_t *
test_three_file_size_comparison(apr_pool_t *scratch_pool)
{
  struct test_file_definition_t *inner, *middle, *outer;
  svn_boolean_t actual12, actual23, actual13;
  svn_boolean_t expected12, expected23, expected13;
  svn_error_t *err = SVN_NO_ERROR;
  svn_error_t *cmp_err;
  apr_pool_t *iterpool = svn_pool_create(scratch_pool);

  SVN_ERR(create_comparison_candidates(scratch_pool));

  for (outer = test_file_definitions; outer->name != NULL; outer += 1)
    {
#ifdef SVN_IO_TEST_ALL_PERMUTATIONS
      middle = test_file_definitions;
#else
      middle = outer;
#endif
      for (; middle->name != NULL; middle += 1)
        {
#ifdef SVN_IO_TEST_ALL_PERMUTATIONS
          inner = test_file_definitions;
#else
          inner = middle;
#endif
          for (; inner->name != NULL; inner += 1)
            {
              svn_pool_clear(iterpool);

              expected12 = inner->size != middle->size;
              expected23 = middle->size != outer->size;
              expected13 = inner->size != outer->size;

              cmp_err = svn_io_filesizes_three_different_p(&actual12,
                                &actual23,
                                &actual13,
                                inner->created_path,
                                middle->created_path,
                                outer->created_path,
                                iterpool);

              if (cmp_err)
                {
                  err = svn_error_compose_create(err, cmp_err);
                }
              else
                {
                  if (expected12 != actual12)
                      err = svn_error_compose_create(err,
                          svn_error_createf(SVN_ERR_TEST_FAILED, NULL,
                                     "size comparison problem: '%s' and '%s'",
                                     inner->created_path,
                                     middle->created_path));

                  if (expected23 != actual23)
                      err = svn_error_compose_create(err,
                          svn_error_createf(SVN_ERR_TEST_FAILED, NULL,
                                     "size comparison problem: '%s' and '%s'",
                                     middle->created_path,
                                     outer->created_path));

                  if (expected13 != actual13)
                      err = svn_error_compose_create(err,
                          svn_error_createf(SVN_ERR_TEST_FAILED, NULL,
                                     "size comparison problem: '%s' and '%s'",
                                     inner->created_path,
                                     outer->created_path));
                }
            }
        }
    }

  svn_pool_destroy(iterpool);

  return err;
}


/* Test 3-way file content checking */
static svn_error_t *
test_three_file_content_comparison(apr_pool_t *scratch_pool)
{
  struct test_file_definition_t *inner, *middle, *outer;
  svn_boolean_t actual12, actual23, actual13;
  svn_boolean_t expected12, expected23, expected13;
  svn_error_t *err = SVN_NO_ERROR;
  svn_error_t *cmp_err;
  apr_pool_t *iterpool = svn_pool_create(scratch_pool);

  SVN_ERR(create_comparison_candidates(scratch_pool));

  for (outer = test_file_definitions; outer->name != NULL; outer += 1)
    {
#ifdef SVN_IO_TEST_ALL_PERMUTATIONS
      middle = test_file_definitions;
#else
      middle = outer;
#endif
      for (; middle->name != NULL; middle += 1)
        {
#ifdef SVN_IO_TEST_ALL_PERMUTATIONS
          inner = test_file_definitions;
#else
          inner = middle;
#endif
          for (; inner->name != NULL; inner += 1)
            {
              svn_pool_clear(iterpool);

              expected12 = outer->size == middle->size
                && strcmp(outer->data, middle->data) == 0;
              expected23 = middle->size == inner->size
                && strcmp(middle->data, inner->data) == 0;
              expected13 = outer->size == inner->size
                && strcmp(outer->data, inner->data) == 0;

              cmp_err = svn_io_files_contents_three_same_p(&actual12,
                                &actual23,
                                &actual13,
                                outer->created_path,
                                middle->created_path,
                                inner->created_path,
                                iterpool);

              if (cmp_err)
                {
                  err = svn_error_compose_create(err, cmp_err);
                }
              else
                {
                  if (expected12 != actual12)
                      err = svn_error_compose_create(err,
                          svn_error_createf(SVN_ERR_TEST_FAILED, NULL,
                                     "size comparison problem: '%s' and '%s'",
                                     inner->created_path,
                                     middle->created_path));

                  if (expected23 != actual23)
                      err = svn_error_compose_create(err,
                          svn_error_createf(SVN_ERR_TEST_FAILED, NULL,
                                     "size comparison problem: '%s' and '%s'",
                                     middle->created_path,
                                     outer->created_path));

                  if (expected13 != actual13)
                      err = svn_error_compose_create(err,
                          svn_error_createf(SVN_ERR_TEST_FAILED, NULL,
                                     "size comparison problem: '%s' and '%s'",
                                     inner->created_path,
                                     outer->created_path));
                }
            }
        }
    }

  return err;
}

static svn_error_t *
read_length_line_shouldnt_loop(apr_pool_t *pool)
{
  const char *tmp_dir;
  const char *tmp_file;
  char buffer[4];
  apr_size_t buffer_limit = sizeof(buffer);
  apr_file_t *f;

  SVN_ERR(svn_dirent_get_absolute(&tmp_dir, "read_length_tmp", pool));
  SVN_ERR(svn_io_remove_dir2(tmp_dir, TRUE, NULL, NULL, pool));
  SVN_ERR(svn_io_make_dir_recursively(tmp_dir, pool));
  svn_test_add_dir_cleanup(tmp_dir);

  SVN_ERR(svn_io_write_unique(&tmp_file, tmp_dir, "1234\r\n", 6,
                              svn_io_file_del_on_pool_cleanup, pool));

  SVN_ERR(svn_io_file_open(&f, tmp_file, APR_READ, APR_OS_DEFAULT, pool));

  SVN_TEST_ASSERT_ERROR(svn_io_read_length_line(f, buffer, &buffer_limit,
                                                pool), SVN_ERR_MALFORMED_FILE);
  SVN_TEST_ASSERT(buffer_limit == 4);

  return SVN_NO_ERROR;
}


static svn_error_t *
test_apr_trunc_workaround(apr_pool_t *pool)
{
  const char *tmp_dir;
  const char *tmp_file;
  apr_file_t *f;
  apr_size_t len;
  apr_off_t offset;
  char dummy;

  /* create a temp folder & schedule it for automatic cleanup */
  SVN_ERR(svn_dirent_get_absolute(&tmp_dir, "test_apr_trunc_workaround",
                                  pool));
  SVN_ERR(svn_io_remove_dir2(tmp_dir, TRUE, NULL, NULL, pool));
  SVN_ERR(svn_io_make_dir_recursively(tmp_dir, pool));
  svn_test_add_dir_cleanup(tmp_dir);

  /* create an r/w file */
  tmp_file = svn_dirent_join(tmp_dir, "file", pool);
  SVN_ERR(svn_io_file_open(&f, tmp_file,
                           APR_READ | APR_WRITE | APR_BUFFERED | APR_CREATE |
                              APR_TRUNCATE,
                           APR_OS_DEFAULT, pool));

  /* write some content and put it internally into read mode */
  len = 10;
  SVN_ERR(svn_io_file_write(f, "0123456789", &len, pool));

  offset = 0;
  SVN_ERR(svn_io_file_seek(f, APR_SET, &offset, pool));
  SVN_ERR(svn_io_file_getc(&dummy, f, pool));

  /* clear the file and write some new content */
  SVN_ERR(svn_io_file_trunc(f, 0, pool));
  len = 3;
  SVN_ERR(svn_io_file_write(f, "abc", &len, pool));

  /* we should now be positioned at the end of the new content */
  offset = 0;
  SVN_ERR(svn_io_file_seek(f, APR_CUR, &offset, pool));
  SVN_TEST_ASSERT(offset == (int)len);

  return SVN_NO_ERROR;  
}

/* The test table.  */

struct svn_test_descriptor_t test_funcs[] =
  {
    SVN_TEST_NULL,
    SVN_TEST_PASS2(test_two_file_size_comparison,
                   "two file size comparison"),
    SVN_TEST_PASS2(test_two_file_content_comparison,
                   "two file content comparison"),
    SVN_TEST_PASS2(test_three_file_size_comparison,
                   "three file size comparison"),
    SVN_TEST_PASS2(test_three_file_content_comparison,
                   "three file content comparison"),
    SVN_TEST_PASS2(read_length_line_shouldnt_loop,
                   "svn_io_read_length_line() shouldn't loop"),
    SVN_TEST_PASS2(test_apr_trunc_workaround,
                   "test workaround for APR in svn_io_file_trunc"),
    SVN_TEST_NULL
  };
