/******************************************************************************
*
* Copyright (C) 2004-2017  Brian Berlin
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
******************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#ifdef _WIN32
#include <windows.h>
#else
#include <unistd.h>
#endif
#include "../src/interface/util.h"
#include "../src/kat5200.h"

static int failure = 0;

static void check_result ( int condition, const char *name )
{
    printf ( "%s: %s\n", condition ? "PASS" : "FAIL", name );
    if ( !condition )
        failure = 1;
}

static void make_dir ( const char *path )
{
    util_mkdir ( path, UTIL_DEFAULT_DIR_MODE );
}

static char * make_path ( const char *dir, const char *file )
{
    return util_make_path_alloc ( (char *)dir, file );
}

static int write_text ( const char *path, const char *text )
{
    FILE *fp = fopen ( path, "w" );
    if ( !fp )
        return -1;
    fputs ( text, fp );
    fclose ( fp );
    return 0;
}

static char * read_text ( const char *path )
{
    FILE *fp = fopen ( path, "rb" );
    long size;
    char *buffer;

    if ( !fp )
        return NULL;
    fseek ( fp, 0, SEEK_END );
    size = ftell ( fp );
    fseek ( fp, 0, SEEK_SET );
    buffer = malloc ( (size_t)size + 1 );
    if ( !buffer ) {
        fclose ( fp );
        return NULL;
    }
    if ( fread(buffer, 1, (size_t)size, fp) != (size_t)size ) {
        free ( buffer );
        fclose ( fp );
        return NULL;
    }
    buffer[size] = '\0';
    fclose ( fp );
    return buffer;
}

static int file_contains ( const char *path, const char *needle )
{
    char *text = read_text ( path );
    int found = text && strstr(text, needle);
    free ( text );
    return found;
}

static int file_equals ( const char *path, const char *expected )
{
    char *text = read_text ( path );
    int equal = text && !strcmp(text, expected);
    free ( text );
    return equal;
}

static int file_exists ( const char *path )
{
    struct stat st;
    return stat(path, &st) == 0;
}

static int add_zip_entry ( zipFile zf, const char *name, const char *text )
{
    zip_fileinfo zi;
    memset ( &zi, 0, sizeof(zi) );

    if ( zipOpenNewFileInZip ( zf, name, &zi, NULL, 0, NULL, 0, NULL,
                0, 0 ) != ZIP_OK )
        return -1;

    if ( text && zipWriteInFileInZip ( zf, text, (unsigned)strlen(text) ) != ZIP_OK ) {
        zipCloseFileInZip ( zf );
        return -1;
    }

    return zipCloseFileInZip ( zf ) == ZIP_OK ? 0 : -1;
}

static int create_seed_zip ( const char *path )
{
    zipFile zf = zipOpen ( path, APPEND_STATUS_CREATE );
    int status = 0;

    if ( !zf )
        return -1;

    status |= add_zip_entry ( zf, "profiles.5200/", NULL );
    status |= add_zip_entry ( zf, "profiles.5200/new.conf",
            "title = \"New Profile\"\n" );
    status |= add_zip_entry ( zf, "profiles.5200/existing.conf",
            "title = \"Seed Should Not Win\"\n" );
    status |= add_zip_entry ( zf, "profiles.5200/list.csv",
            "\"Existing Seed\", 0x0000000a\n"
            "\"New Seed\", 0x0000000b\n"
            "\"Case Width Seed\", 0x0000000c\n"
            "\"Long Duplicate Seed\", 0x0000000d\n" );
    status |= add_zip_entry ( zf, "profiles.5200/collection.csv",
            "\"Only In Seed\", 0x0000000e\n" );
    status |= add_zip_entry ( zf, "profiles.5200/favorites.csv",
            "\"Seed Favorite\", 0x0000000f\n" );
    status |= add_zip_entry ( zf, "profiles.8bit/dual.conf",
            "title = \"Dual 8bit\"\n" );
    status |= add_zip_entry ( zf, "profiles.8bit/list.csv",
            "\"Dual 8bit\", 0x00000010\n" );

    if ( zipClose ( zf, NULL ) != ZIP_OK )
        status = -1;

    return status ? -1 : 0;
}

static int create_extra_zip ( const char *path )
{
    zipFile zf = zipOpen ( path, APPEND_STATUS_CREATE );
    int status = 0;

    if ( !zf )
        return -1;

    status |= add_zip_entry ( zf, "README.txt", "extra\n" );
    status |= add_zip_entry ( zf, "profiles.5200/extra.conf",
            "title = \"Extra Profile\"\n" );

    if ( zipClose ( zf, NULL ) != ZIP_OK )
        status = -1;

    return status ? -1 : 0;
}

static int create_unsafe_zip ( const char *path, const char *entry )
{
    zipFile zf = zipOpen ( path, APPEND_STATUS_CREATE );
    int status;

    if ( !zf )
        return -1;
    status = add_zip_entry ( zf, entry, "bad\n" );
    if ( zipClose ( zf, NULL ) != ZIP_OK )
        status = -1;
    return status;
}

static char * make_existing_list_csv ( void )
{
    const size_t long_title_len = 1200;
    const char *prefix = "\"User Title\", 0xA\n\"";
    const char *suffix =
        "\", 0xD\n\"Case Width User\", 0X0000000C";
    char *text = malloc ( strlen(prefix) + long_title_len + strlen(suffix) + 1 );

    if ( !text )
        return NULL;

    strcpy ( text, prefix );
    memset ( text + strlen(prefix), 'L', long_title_len );
    strcpy ( text + strlen(prefix) + long_title_len, suffix );
    return text;
}

static void run_merge_tests ( const char *root )
{
    char *zip_path = make_path ( root, "profiles.zip" );
    char *dir_5200 = make_path ( root, "live5200" );
    char *dir_800 = make_path ( root, "live800" );
    char *dir_xl = make_path ( root, "livexl" );
    char *path;
    t_profile_seed_target targets[3];

    make_dir ( dir_5200 );
    make_dir ( dir_800 );
    make_dir ( dir_xl );
    check_result ( create_seed_zip(zip_path) == 0, "create seed zip" );

    path = make_path ( dir_5200, "existing.conf" );
    write_text ( path, "title = \"User Edited\"\n" );
    free ( path );

    path = make_path ( dir_5200, "list.csv" );
    {
        char *csv = make_existing_list_csv();
        write_text ( path, csv );
        free ( csv );
    }
    free ( path );

    path = make_path ( dir_5200, "favorites.csv" );
    write_text ( path, "\"User Favorite\", 0x0000000f\n" );
    free ( path );

    targets[0].seed_tree = "profiles.5200";
    targets[0].dest_dir = dir_5200;
    targets[1].seed_tree = "profiles.8bit";
    targets[1].dest_dir = dir_800;
    targets[2].seed_tree = "profiles.8bit";
    targets[2].dest_dir = dir_xl;

    check_result ( util_merge_seed_profiles_from_zip(zip_path, targets, 3) == 0,
            "merge seed zip" );

    path = make_path ( dir_5200, "new.conf" );
    check_result ( file_contains(path, "New Profile"), "missing profile copied" );
    free ( path );

    path = make_path ( dir_5200, "existing.conf" );
    check_result ( file_equals(path, "title = \"User Edited\"\n"),
            "existing profile preserved" );
    free ( path );

    path = make_path ( dir_5200, "list.csv" );
    check_result ( file_contains(path, "\"User Title\", 0xA"),
            "existing CSV row preserved" );
    check_result ( file_contains(path, "\"New Seed\", 0x0000000b"),
            "new CSV CRC appended" );
    check_result ( !file_contains(path, "\"Existing Seed\", 0x0000000a"),
            "numeric duplicate CRC skipped" );
    check_result ( !file_contains(path, "\"Case Width Seed\", 0x0000000c"),
            "case and width duplicate CRC skipped" );
    check_result ( !file_contains(path, "\"Long Duplicate Seed\", 0x0000000d"),
            "overlong destination row CRC skipped" );
    check_result ( file_contains(path, "0X0000000C\n\"New Seed\""),
            "append repairs missing trailing newline" );
    free ( path );

    path = make_path ( dir_5200, "collection.csv" );
    check_result ( file_equals(path, "\"Only In Seed\", 0x0000000e\n"),
            "missing CSV copied" );
    free ( path );

    path = make_path ( dir_5200, "favorites.csv" );
    check_result ( file_equals(path, "\"User Favorite\", 0x0000000f\n"),
            "favorites CSV preserved" );
    free ( path );

    path = make_path ( dir_800, "dual.conf" );
    check_result ( file_contains(path, "Dual 8bit"),
            "8-bit profile copied to first target" );
    free ( path );

    path = make_path ( dir_xl, "dual.conf" );
    check_result ( file_contains(path, "Dual 8bit"),
            "8-bit profile copied to second target" );
    free ( path );

    free ( zip_path );
    free ( dir_5200 );
    free ( dir_800 );
    free ( dir_xl );
}

static void run_extra_entry_tests ( const char *root )
{
    char *zip_path = make_path ( root, "extra.zip" );
    char *dest = make_path ( root, "extra-live" );
    char *path;
    t_profile_seed_target target;

    make_dir ( dest );
    target.seed_tree = "profiles.5200";
    target.dest_dir = dest;

    check_result ( create_extra_zip(zip_path) == 0, "create extra-entry zip" );
    check_result ( util_merge_seed_profiles_from_zip(zip_path, &target, 1) == 0,
            "safe unmatched zip entry skipped" );

    path = make_path ( dest, "extra.conf" );
    check_result ( file_contains(path, "Extra Profile"),
            "profile still copied after extra entry" );
    free ( path );
    free ( zip_path );
    free ( dest );
}

static void run_unsafe_tests ( const char *root )
{
    char *zip_path = make_path ( root, "unsafe.zip" );
    char *dest = make_path ( root, "unsafe-live" );
    t_profile_seed_target target;

    make_dir ( dest );
    target.seed_tree = "profiles.5200";
    target.dest_dir = dest;

    check_result ( create_unsafe_zip(zip_path, "profiles.5200/../bad.conf") == 0,
            "create traversal zip" );
    check_result ( util_merge_seed_profiles_from_zip(zip_path, &target, 1) != 0,
            "reject traversal zip entry" );

    remove ( zip_path );
    check_result ( create_unsafe_zip(zip_path, "profiles.5200\\bad.conf") == 0,
            "create backslash zip" );
    check_result ( util_merge_seed_profiles_from_zip(zip_path, &target, 1) != 0,
            "reject backslash zip entry" );

    remove ( zip_path );
    check_result ( create_unsafe_zip(zip_path, "profiles.5200/subdir\\bad.conf") == 0,
            "create mid-segment backslash zip" );
    check_result ( util_merge_seed_profiles_from_zip(zip_path, &target, 1) != 0,
            "reject mid-segment backslash zip entry" );

    remove ( zip_path );
    {
        char *long_entry = malloc ( KAT_PATH_MAX + 32 );
        strcpy ( long_entry, "profiles.5200/" );
        memset ( long_entry + strlen("profiles.5200/"), 'a',
                KAT_PATH_MAX + 16 - strlen("profiles.5200/") );
        long_entry[KAT_PATH_MAX + 16] = '\0';
        check_result ( create_unsafe_zip(zip_path, long_entry) == 0,
                "create overlong-name zip" );
        check_result ( util_merge_seed_profiles_from_zip(zip_path, &target, 1) != 0,
                "reject overlong zip entry name" );
        free ( long_entry );
    }

    free ( zip_path );
    free ( dest );
}

static void run_schedule_tests ( const char *root )
{
    char program_path[KAT_PATH_MAX];
    char *zip_path = make_path ( root, "schedule.zip" );
    char *dest = make_path ( root, "schedule-live" );
    char *stamp = make_path ( root, ".profiles_seed_version" );
    t_profile_seed_target target;
    char *empty_path;

    make_dir ( dest );
    check_result ( create_seed_zip(zip_path) == 0, "create schedule zip" );

    strncpy ( program_path, root, sizeof(program_path) - 1 );
    program_path[sizeof(program_path) - 1] = '\0';
    util_set_program_path ( program_path );
    empty_path = util_create_program_path_alloc ( "" );
    check_result ( empty_path && !strcmp(empty_path, ""),
            "empty program-relative path allocation safe" );
    free ( empty_path );

    target.seed_tree = "profiles.5200";
    target.dest_dir = dest;

    write_text ( stamp, VERSION "\n" );
    check_result ( util_schedule_missing_profile_seed_sync(zip_path, &target, 1) == 0,
            "current stamp schedule succeeds" );
    check_result ( !util_first_use_seed_pending(),
            "current stamp skips pending worker" );

    write_text ( stamp, "old\n" );
    check_result ( util_schedule_missing_profile_seed_sync(zip_path, &target, 1) == 0,
            "stale stamp schedule succeeds" );
    check_result ( util_first_use_seed_pending(),
            "stale stamp arms pending worker" );
    util_first_use_seed_worker ( NULL );
    check_result ( util_first_use_seed_done() && !util_first_use_seed_error(),
            "scheduled worker completes" );
    check_result ( file_equals(stamp, VERSION "\n"),
            "scheduled worker writes current stamp" );

    free ( zip_path );
    free ( dest );
    free ( stamp );
}

int main ( void )
{
    char root[KAT_PATH_MAX];

#ifdef _WIN32
    snprintf ( root, sizeof(root), "profile_seed_test_%lu",
            (unsigned long)GetCurrentProcessId() );
#else
    snprintf ( root, sizeof(root), "profile_seed_test_%ld", (long)getpid() );
#endif
    make_dir ( root );

    run_merge_tests ( root );
    run_extra_entry_tests ( root );
    run_unsafe_tests ( root );
    run_schedule_tests ( root );

    return failure ? EXIT_FAILURE : EXIT_SUCCESS;
}
