/* mbed Microcontroller Library
 * Copyright (c) 2017-2017 ARM Limited
 *
 * 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.
 */


/**
 * This file contains code which performs various atomic operations using
 * littlefs. It is intended for use in tests and test applications to
 * validate that the defined behavior below is being met.
 *
 * # Defined behavior
 * - A file rename is atomic (Note - rename can be used to replace a file)
 *      - Atomic file rename tested by setup/perform/check_file_rename
 *      - Atomic file replace tested by setup/perform/check_file_rename_replace
 * - A directory rename is atomic (Note - rename can be used to replace an empty directory)
 *      - Tested by setup/perform/check_directory_rename
 * - Directory create is atomic
 * - Directory delete is atomic
 * - File create is atomic
 * - File delete is atomic
 * - File contents are atomically written on close
 *      - Tested by setup/perform/check_file_change_contents
 */


#include "mbed.h"
#include "greentea-client/test_env.h"
#include "unity.h"
#include "utest.h"
#include <stdlib.h>
#include <errno.h>

#include "ObservingBlockDevice.h"
#include "ExhaustibleBlockDevice.h"
#include "FileSystem.h"

#include "atomic_usage.h"

// test configuration
#ifndef MBED_TEST_FILESYSTEM
#define MBED_TEST_FILESYSTEM LittleFileSystem
#endif

#ifndef MBED_TEST_FILESYSTEM_DECL
#define MBED_TEST_FILESYSTEM_DECL MBED_TEST_FILESYSTEM fs("fs")
#endif


// declarations
#define STRINGIZE(x) STRINGIZE2(x)
#define STRINGIZE2(x) #x
#define INCLUDE(x) STRINGIZE(x.h)

#include INCLUDE(MBED_TEST_FILESYSTEM)


#define DEBUG(...)
#define DEBUG_CHECK(...)
#define BUFFER_SIZE             64
// Version is written to a file and is used
// to determine if a reformat is required
#define ATOMIC_USAGE_VERSION    1

#define ARRAY_LENGTH(array)     (sizeof(array) / sizeof(array[0]))

#define TEST_ASSERT_OR_EXIT(condition)              \
        TEST_ASSERT(condition); if (!(condition)) {error("Assert failed");}

#define TEST_ASSERT_EQUAL_OR_EXIT(expected, actual) \
        TEST_ASSERT_EQUAL(expected, actual); if ((int64_t)(expected) != (int64_t)(actual)) {error("Assert failed");}

using namespace utest::v1;

typedef void (*test_function_t)(FileSystem *fs);
typedef bool (*test_function_bool_t)(FileSystem *fs);

struct TestEntry {
    const char *name;
    test_function_t setup;
    test_function_bool_t perform;
    test_function_t check;
};

/**
 * Write data to the file while checking for error conditions
 *
 * @param file File to write to
 * @param data Data to write
 * @param size Size of data to write
 * @return true if flash has been exhausted, false otherwise
 */
static bool file_write(File *file, uint8_t *data, uint32_t size)
{
    int res = file->write(data, size);
    if (-ENOSPC  == res) {
        return true;
    }
    TEST_ASSERT_EQUAL_OR_EXIT(size, res);
    return false;
}

/**
 * Write padding data of the given size
 *
 * @param file Pointer to the file to write to
 * @param padding Value to pad
 * @param size Size to pad
 * @return true if flash has been exhausted, false otherwise
 */
static bool file_pad(File *file, char padding, uint32_t size)
{
    uint8_t buf[BUFFER_SIZE];
    memset(buf, padding, sizeof(buf));

    while (size > 0) {
        uint32_t write_size = sizeof(buf) <= size ? sizeof(buf) : size;
        if (file_write(file, buf, write_size)) {
            return true;
        }
        size -= write_size;
    }

    return false;
}

/*
 * Similar to fscanf but uses and mbed file
 *
 * @param file File to scan from
 * @param format Format string of values to read
 * @return the number of arguments read
 */
static int file_scanf(File *file, const char *format, ...)
{
    uint8_t buf[BUFFER_SIZE];
    va_list args;
    memset(buf, 0, sizeof(buf));

    int res = file->read(buf, sizeof(buf) - 1);
    TEST_ASSERT_OR_EXIT(res >= 0);

    va_start(args, format);
    int count = vsscanf((char *)buf, format, args);
    va_end(args);
    TEST_ASSERT_OR_EXIT(count >= 0);

    return count;
}

/*
 * Similar to fprintf but uses and mbed file
 *
 * @param file File to print to
 * @param format Format string of values to write
 * @return size written to file or -1 on out of space
 */
static int file_printf(File *file, const char *format, ...)
{
    uint8_t buf[BUFFER_SIZE];
    va_list args;
    va_start(args, format);
    int size = vsprintf((char *)buf, format, args);
    va_end(args);
    TEST_ASSERT_OR_EXIT((size >= 0) && (size <= (int)sizeof(buf)));

    if (file_write(file, buf, size)) {
        return -1;
    }

    return size;
}


static const char FILE_RENAME_A[] = "file_to_rename_a.txt";
static const char FILE_RENAME_B[] = "file_to_rename_b.txt";
static const char FILE_RENAME_CONTENTS[] = "Test contents for the file to be renamed";
static const int FILE_RENAME_LEN = strlen(FILE_RENAME_CONTENTS);

/**
 * Setup for the file rename test
 *
 * Create file FILE_RENAME_A with contents FILE_RENAME_CONTENTS.
 */
static void setup_file_rename(FileSystem *fs)
{
    DEBUG("setup_file_rename()\n");

    File file;

    int res = file.open(fs, FILE_RENAME_A, O_WRONLY | O_CREAT);
    DEBUG("  open result %i\n", res);
    TEST_ASSERT_EQUAL_OR_EXIT(0, res);

    res = file.write(FILE_RENAME_CONTENTS, FILE_RENAME_LEN);
    DEBUG("  write result %i\n", res);
    TEST_ASSERT_EQUAL_OR_EXIT(FILE_RENAME_LEN, res);
}

/**
 * Change the file name to either FILE_RENAME_A or FILE_RENAME_B
 */
static bool perform_file_rename(FileSystem *fs)
{
    DEBUG("perform_file_rename()\n");

    struct stat st;
    int res = fs->stat(FILE_RENAME_A, &st);
    const char *src = (res == 0) ? FILE_RENAME_A : FILE_RENAME_B;
    const char *dst = (res == 0) ? FILE_RENAME_B : FILE_RENAME_A;

    DEBUG("  stat result  %i\n", res);
    TEST_ASSERT_OR_EXIT((res == -ENOENT) || (res == 0));

    DEBUG("  Renaming %s to %s\n", src, dst);
    res = fs->rename(src, dst);
    if (-ENOSPC  == res) {
        return true;
    }
    TEST_ASSERT_EQUAL_OR_EXIT(0, res);
    return false;
}

/**
 * Check that the file rename is in a good state
 *
 * Check that there is only one file and that file contains the correct
 * contents.
 *
 * Allowed states:
 * - File FILE_RENAME_A exists with contents and FILE_RENAME_B does not
 * - File FILE_RENAME_B exists with contents and FILE_RENAME_A does not
 *
 */
static void check_file_rename(FileSystem *fs)
{

    int files = 0;
    int valids = 0;
    const char *const filenames[] = {FILE_RENAME_A, FILE_RENAME_B};

    for (int i = 0; i < 2; i++) {
        File file;
        if (file.open(fs, filenames[i], O_RDONLY) == 0) {
            uint8_t buf[BUFFER_SIZE];
            files++;
            memset(buf, 0, sizeof(buf));
            int res = file.read(buf, FILE_RENAME_LEN);
            if (res != FILE_RENAME_LEN) {
                break;
            }
            if (memcmp(buf, FILE_RENAME_CONTENTS, FILE_RENAME_LEN) != 0) {
                break;
            }
            valids++;
        }
    }

    TEST_ASSERT_EQUAL_OR_EXIT(1, files);
    TEST_ASSERT_EQUAL_OR_EXIT(1, valids);
}


static const char  FILE_RENAME_REPLACE[] = "rename_replace_file.txt";
static const char  FILE_RENAME_REPLACE_NEW[] = "new_rename_replace_file.txt";
static const char  FILE_RENAME_REPLACE_FMT[] = "file replace count: %lu\n";

/**
 * Create the file FILE_RENAME_REPLACE with initial contents
 *
 * Create an write an initial count of 0 to the file.
 */
static void setup_file_rename_replace(FileSystem *fs)
{
    DEBUG("setup_file_rename_replace()\n");
    File file;

    // Write out initial count

    int res = file.open(fs, FILE_RENAME_REPLACE, O_WRONLY | O_CREAT);
    TEST_ASSERT_EQUAL_OR_EXIT(0, res);

    uint32_t count = 0;
    uint8_t buf[BUFFER_SIZE];
    memset(buf, 0, sizeof(buf));
    const int length = sprintf((char *)buf, FILE_RENAME_REPLACE_FMT, count);
    TEST_ASSERT_OR_EXIT(length > 0);

    res = file.write(buf, length);
    DEBUG("  write result %i\n", res);
    TEST_ASSERT_EQUAL_OR_EXIT(length, res);
}

/**
 * Atomically increment the count in FILE_RENAME_REPLACE using a rename
 */
bool perform_file_rename_replace(FileSystem *fs)
{
    DEBUG("perform_file_rename_replace()\n");
    File file;

    // Read in previous count

    int res = file.open(fs, FILE_RENAME_REPLACE, O_RDONLY);
    TEST_ASSERT_EQUAL_OR_EXIT(0, res);

    uint64_t count;
    int args_read = file_scanf(&file, FILE_RENAME_REPLACE_FMT, &count);
    TEST_ASSERT_EQUAL_OR_EXIT(1, args_read);

    res = file.close();
    if (-ENOSPC  == res) {
        return true;
    }
    TEST_ASSERT_EQUAL(0, res);

    // Write out new count

    count++;

    res = file.open(fs, FILE_RENAME_REPLACE_NEW, O_WRONLY | O_CREAT);
    if (-ENOSPC  == res) {
        return true;
    }
    TEST_ASSERT_EQUAL_OR_EXIT(0, res);

    if (file_printf(&file, FILE_RENAME_REPLACE_FMT, count) <= 0) {
        return true;
    }

    res = file.close();
    if (-ENOSPC  == res) {
        return true;
    }
    TEST_ASSERT_EQUAL(0, res);

    // Rename file

    res = fs->rename(FILE_RENAME_REPLACE_NEW, FILE_RENAME_REPLACE);
    if (-ENOSPC  == res) {
        return true;
    }
    TEST_ASSERT_EQUAL_OR_EXIT(0, res);
    DEBUG("  count %llu -> %llu\n", count - 1, count);

    return false;
}

/**
 * Check that FILE_RENAME_REPLACE always has a valid count
 *
 * Allowed states:
 * - FILE_RENAME_REPLACE exists with valid contents
 */
static void check_file_rename_replace(FileSystem *fs)
{
    DEBUG_CHECK("check_file_rename_replace()\n");
    File file;

    // Read in previous count

    int res = file.open(fs, FILE_RENAME_REPLACE, O_RDONLY);
    TEST_ASSERT_EQUAL_OR_EXIT(0, res);

    uint64_t count;
    int args_read = file_scanf(&file, FILE_RENAME_REPLACE_FMT, &count);
    TEST_ASSERT_EQUAL_OR_EXIT(1, args_read);
    DEBUG_CHECK("  count %llu\n", count);
}


static const char DIRECTORY_RENAME_A[] = "dir_a";
static const char DIRECTORY_RENAME_B[] = "dir_b";

/**
 * Create DIRECTORY_RENAME_A with initial contents
 */
static void setup_directory_rename(FileSystem *fs)
{
    DEBUG("setup_directory_rename()\n");

    int res = fs->mkdir(DIRECTORY_RENAME_A, 0777);
    TEST_ASSERT_EQUAL_OR_EXIT(0, res);
}

/*
 * Change the directory name from either DIRECTORY_RENAME_A or DIRECTORY_RENAME_B to the other
 */
static bool perform_directory_rename(FileSystem *fs)
{
    DEBUG("perform_directory_rename()\n");

    struct stat st;
    int res = fs->stat(DIRECTORY_RENAME_A, &st);
    const char *src = (res == 0) ? DIRECTORY_RENAME_A : DIRECTORY_RENAME_B;
    const char *dst = (res == 0) ? DIRECTORY_RENAME_B : DIRECTORY_RENAME_A;

    DEBUG("  stat result  %i\n", res);
    TEST_ASSERT_OR_EXIT((res == -ENOENT) || (res == 0));

    DEBUG("  Renaming %s to %s\n", src, dst);
    res = fs->rename(src, dst);
    if (-ENOSPC  == res) {
        return true;
    }
    TEST_ASSERT_EQUAL_OR_EXIT(0, res);
    return false;
}

/*
 * Change the directory name from either DIRECTORY_RENAME_A or DIRECTORY_RENAME_B to the other
 *
 * Allowed states:
 * - DIRECTORY_RENAME_A exists with valid contents and DIRECTORY_RENAME_B does not exist
 * - DIRECTORY_RENAME_B exists with valid contents and DIRECTORY_RENAME_A does not exist
 */
static void check_directory_rename(FileSystem *fs)
{
    DEBUG_CHECK("check_directory_rename()\n");

    static const char *directory_names[] = {
        DIRECTORY_RENAME_A,
        DIRECTORY_RENAME_B
    };

    size_t directories = 0;
    for (size_t i = 0; i < ARRAY_LENGTH(directory_names); i++) {
        Dir dir;
        int res = dir.open(fs, directory_names[i]);
        TEST_ASSERT_OR_EXIT((res == -ENOENT) || (res == 0));
        if (res == 0) {
            directories++;
        }
    }
    TEST_ASSERT_EQUAL_OR_EXIT(1, directories);
}


static const char CHANGE_CONTENTS_NAME[] = "file_changing_contents.txt";
static const char CHANGE_CONTENTS_FILL = ' ';
static const uint32_t BLOCK_SIZE = 512;

/**
 * Create file CHANGE_CONTENTS_NAME with initial contents
 *
 * File contains three blocks of data each which start
 * with a count.
 */
static void setup_file_change_contents(FileSystem *fs)
{
    DEBUG("setup_file_change_contents()\n");

    File file;
    int res = file.open(fs, CHANGE_CONTENTS_NAME, O_WRONLY | O_CREAT);
    TEST_ASSERT_EQUAL_OR_EXIT(0, res);

    for (int count = 1; count <= 3; count++) {
        int size = file_printf(&file, "%lu\n", count);
        TEST_ASSERT_OR_EXIT(size >= 0);

        bool dead = file_pad(&file, CHANGE_CONTENTS_FILL, BLOCK_SIZE - size);
        TEST_ASSERT_EQUAL_OR_EXIT(false, dead);
    }
}

/**
 * Atomically increment the counts in the file CHANGE_CONTENTS_NAME
 *
 * Read in the current counts, increment them and then write them
 * back in non-sequential order.
 */
static bool perform_file_change_contents(FileSystem *fs)
{
    DEBUG("perform_file_change_contents()\n");
    File file;

    int res = file.open(fs, CHANGE_CONTENTS_NAME,  O_RDWR);
    TEST_ASSERT_EQUAL_OR_EXIT(0, res);

    // Read in values
    uint32_t values[3];
    for (int i = 0; i < 3; i++) {
        file.seek(i * BLOCK_SIZE);
        int args_read = file_scanf(&file, "%lu\n", &values[i]);
        TEST_ASSERT_EQUAL_OR_EXIT(1, args_read);
    }

    // Increment values
    for (int i = 0; i < 3; i++) {
        values[i]++;
    }

    // Write values out of order
    int i;
    i = 0;
    file.seek(i * BLOCK_SIZE);
    if (file_printf(&file, "%lu\n", values[i]) <= 0) {
        return true;
    }
    DEBUG("    value[%i]: %lu -> %lu\n", i, values[i] - 1, values[i]);

    i = 2;
    file.seek(i * BLOCK_SIZE);
    if (file_printf(&file, "%lu\n", values[i]) <= 0) {
        return true;
    }
    DEBUG("    value[%i]: %lu -> %lu\n", i, values[i] - 1, values[i]);

    i = 1;
    file.seek(i * BLOCK_SIZE);
    if (file_printf(&file, "%lu\n", values[i]) <= 0) {
        return true;
    }
    DEBUG("    value[%i]: %lu -> %lu\n", i, values[i] - 1, values[i]);

    res = file.close();
    if (-ENOSPC  == res) {
        return true;
    }
    TEST_ASSERT_EQUAL(0, res);

    return false;
}

/*
 * Change the directory name from either DIRECTORY_RENAME_A or DIRECTORY_RENAME_B to the other
 *
 * Allowed states:
 * - CHANGE_CONTENTS_NAME exists and contains 3 counts which are in order
 */
static void check_file_change_contents(FileSystem *fs)
{
    DEBUG_CHECK("check_file_change_contents()\n");
    File file;

    int res = file.open(fs, CHANGE_CONTENTS_NAME,  O_RDONLY);
    TEST_ASSERT_EQUAL_OR_EXIT(0, res);

    // Read in values
    uint32_t values[3];
    for (int i = 0; i < 3; i++) {
        file.seek(i * BLOCK_SIZE);
        int args_read = file_scanf(&file, "%lu\n", &values[i]);
        TEST_ASSERT_EQUAL_OR_EXIT(1, args_read);
        DEBUG_CHECK("    value[%i]: %lu\n", i, values[i]);
    }

    TEST_ASSERT_EQUAL_OR_EXIT(values[0] + 1, values[1]);
    TEST_ASSERT_EQUAL_OR_EXIT(values[1] + 1, values[2]);
}


static const TestEntry atomic_test_entries[] = {
    {"File rename", setup_file_rename, perform_file_rename, check_file_rename},
    {"File rename replace", setup_file_rename_replace, perform_file_rename_replace, check_file_rename_replace},
    {"Directory rename", setup_directory_rename, perform_directory_rename, check_directory_rename},
    {"File change contents", setup_file_change_contents, perform_file_change_contents, check_file_change_contents},
};

static const char  FILE_SETUP_COMPLETE[] = "setup_complete.txt";
static const char  FILE_SETUP_COMPLETE_FMT[] = "Test version: %lu\n";

static bool format_required(BlockDevice *bd)
{
    MBED_TEST_FILESYSTEM_DECL;

    if (fs.mount(bd) != 0) {
        return true;
    }

    // Check if setup complete file exists
    File file;
    int res = file.open(&fs, FILE_SETUP_COMPLETE,  O_RDONLY);
    if (res != 0) {
        return true;
    }

    // Read contents of setup complete file
    uint8_t buf[BUFFER_SIZE];
    memset(buf, 0, sizeof(buf));
    int size_read = file.read(buf, sizeof(buf) - 1);
    if (size_read <= 0) {
        return true;
    }

    // Get the test version
    uint32_t version = 0;
    res = sscanf((char *)buf, FILE_SETUP_COMPLETE_FMT, &version);
    if (res != 1) {
        return true;
    }

    if (ATOMIC_USAGE_VERSION != version) {
        return true;
    }

    // Setup file exists and is the correct version
    return false;
}

static void format(BlockDevice *bd)
{
    MBED_TEST_FILESYSTEM_DECL;

    int res = fs.format(bd);
    TEST_ASSERT_EQUAL_OR_EXIT(0, res);

    res = fs.mount(bd);
    TEST_ASSERT_EQUAL_OR_EXIT(0, res);

    for (size_t i = 0; i < ARRAY_LENGTH(atomic_test_entries); i++) {
        atomic_test_entries[i].setup(&fs);
    }

    File file;
    res = file.open(&fs, FILE_SETUP_COMPLETE, O_CREAT | O_WRONLY);
    TEST_ASSERT_EQUAL_OR_EXIT(0, res);

    int size = file_printf(&file, FILE_SETUP_COMPLETE_FMT, (uint32_t)ATOMIC_USAGE_VERSION);
    TEST_ASSERT_OR_EXIT(size >= 0);
}

static int64_t get_cycle_count(FileSystem *fs)
{
    File file;

    int res = file.open(fs, FILE_RENAME_REPLACE, O_RDONLY);
    TEST_ASSERT_EQUAL_OR_EXIT(0, res);

    uint64_t count = 0;
    int args_read = file_scanf(&file, FILE_RENAME_REPLACE_FMT, &count);
    TEST_ASSERT_EQUAL_OR_EXIT(1, args_read);

    file.close();

    return (int64_t)count;
}

bool setup_atomic_operations(BlockDevice *bd, bool force_rebuild)
{
    if (force_rebuild || format_required(bd)) {
        format(bd);
        TEST_ASSERT_EQUAL_OR_EXIT(false, format_required(bd));
        return true;
    }
    return false;
}

int64_t perform_atomic_operations(BlockDevice *bd)
{
    MBED_TEST_FILESYSTEM_DECL;
    bool out_of_space = false;

    fs.mount(bd);

    for (size_t i = 0; i < ARRAY_LENGTH(atomic_test_entries); i++) {
        out_of_space |= atomic_test_entries[i].perform(&fs);
    }

    int64_t cycle_count = get_cycle_count(&fs);

    fs.unmount();

    if (out_of_space) {
        return -1;
    } else  {
        return cycle_count;
    }
}

void check_atomic_operations(BlockDevice *bd)
{
    MBED_TEST_FILESYSTEM_DECL;
    fs.mount(bd);

    for (size_t i = 0; i < ARRAY_LENGTH(atomic_test_entries); i++) {
        atomic_test_entries[i].check(&fs);
    }

    fs.unmount();
}
