/* -*- mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
// vim: expandtab:ts=8:sw=4:softtabstop=4:
#include "test.h"
#include <sys/wait.h>

// Verify that if we prepare a transaction, then commit a bunch more transactions so that the logs may have been rotated, then the transaction can commit or abort properly on recovery.

static void clean_env (const char *envdir) {
    const int len = strlen(envdir)+100;
    char cmd[len];
    snprintf(cmd, len, "rm -rf %s", envdir);
    system(cmd);
    CKERR(toku_os_mkdir(envdir, S_IRWXU+S_IRWXG+S_IRWXO));
}

static void setup_env (DB_ENV **envp, const char *envdir) {
    CHK(db_env_create(envp, 0));
    (*envp)->set_errfile(*envp, stderr);
#ifdef TOKUDB
    CHK((*envp)->set_redzone(*envp, 0));
#endif
    CHK((*envp)->open(*envp, envdir, DB_INIT_LOCK|DB_INIT_LOG|DB_INIT_MPOOL|DB_INIT_TXN|DB_CREATE|DB_PRIVATE|DB_RECOVER, S_IRWXU+S_IRWXG+S_IRWXO));
}

#define NTXNS 6

static void setup_env_and_prepare (DB_ENV **envp, const char *envdir) {
    DB *db;
    clean_env(envdir);
    setup_env(envp, envdir);
    CKERR(db_create(&db, *envp, 0));
    CKERR(db->open(db, NULL, "foo.db", 0, DB_BTREE, DB_CREATE | DB_AUTO_COMMIT, S_IRWXU+S_IRWXG+S_IRWXO));

    {
	DB_TXN *txn;
	CKERR((*envp)->txn_begin(*envp, 0, &txn, 0));
	for (int tnum=0; tnum<NTXNS; tnum++) {
	    for (int k=0; k<26; k++) {
		#define DSIZE 200
		char data[DSIZE];
		memset(data, ' ', DSIZE);
		data[0]='a'+tnum;
		data[1]='a'+k;
		data[DSIZE-1]=0;
		DBT key={.size=DSIZE, .data=data};
		CKERR(db->put(db, txn, &key, &key, 0));
	    }
	}
	CKERR(txn->commit(txn, 0));
    }

    for (int tnum=0; tnum<NTXNS; tnum++) {
	DB_TXN *txn;
	CKERR((*envp)->txn_begin(*envp, 0, &txn, 0));
	char data[3]={'a'+tnum,'_',0};
	DBT key={.size=3, .data=data};
	CKERR(db->put(db, txn, &key, &key, 0));
	u_int8_t gid[DB_GID_SIZE];
	memset(gid, 0, DB_GID_SIZE);
	gid[0]='a'+tnum;
	CKERR(txn->prepare(txn, gid));
	// Drop txn on the ground, since we will commit or abort it after recovery
	if (tnum==0) {
	    //printf("commit %d\n", tnum);
	    CKERR(txn->commit(txn, 0));
	} else if (tnum==1) {
	    //printf("abort %d\n", tnum);
	    CKERR(txn->abort(txn));
	} else {
	    //printf("prepare %d\n", tnum);
	}
    }
    CHK(db->close(db, 0));
}

enum prepared_state {
    COMMITTED,
    ABORTED,
    MAYBE_COMMITTED,
    MAYBE_ABORTED,
    PREPARED};
    

static void check_prepared_list (enum prepared_state ps[NTXNS], long count, DB_PREPLIST *l) {
    int count_prepared=0;
    int count_maybe_prepared=0;
    for (int j=0; j<NTXNS; j++) {
	switch (ps[j]) {
	case COMMITTED:
	case ABORTED:
	    goto next;
	case PREPARED:
	    count_prepared++;
	case MAYBE_COMMITTED:
	case MAYBE_ABORTED:
	    count_maybe_prepared++;
	    goto next;
	}
	assert(0);
    next:;
    }
	    
    assert(count>=count_prepared && count<=count_maybe_prepared);

    bool found[NTXNS];
    for (int j=0; j<NTXNS; j++) {
	found[j] = (ps[j]!=PREPARED);
    }

    // now found[j] is false on those transactions that I hope to find in the prepared list.
    for (int j=0; j<count; j++) {
	int num = l[j].gid[0]-'a';
	assert(num>=0 && num<NTXNS);
	switch (ps[num]) {
	case PREPARED:
	    assert(!found[num]);
	    found[num]=true;
	    break;
	default:;
	}
	for (int i=1; i<DB_GID_SIZE; i++) {
		assert(l[j].gid[i]==0);
	}
    }
}

static void get_prepared (DB_ENV *env, long *count, DB_PREPLIST *l) {
    CKERR(env->txn_recover(env, l, NTXNS, count, DB_FIRST));
    //printf("%s:%d count=%ld\n", __FILE__, __LINE__, *count);
    assert(*count>=0);
}

static void check_prepared_txns (DB_ENV *env, enum prepared_state ps[NTXNS]) {
    DB_PREPLIST l[NTXNS];
    long count=-1;
    get_prepared(env, &count, l);
    check_prepared_list(ps, count, l);
}

static void check_state_after_full_recovery (DB_ENV *env) {
    DB *db;
    CKERR(db_create(&db, env, 0));
    CKERR(db->open(db, NULL, "foo.db", 0, DB_BTREE, DB_CREATE | DB_AUTO_COMMIT, S_IRWXU+S_IRWXG+S_IRWXO));

    for (int tnum=0; tnum<NTXNS; tnum++) {
	DB_TXN *txn;
	CKERR(env->txn_begin(env, 0, &txn, 0));
	char data[3]={'a'+tnum,'_',0};
	DBT key = {.size=3, .data=data};
	DBT dbt_data = {.size=0, .data=0};
	int r = db->get(db, txn, &key, &dbt_data, 0);
	if (tnum%2==0) {
	    assert(r==0);
	    assert(dbt_data.size==3 && memcmp(dbt_data.data, data, 3)==0);
	} else {
	    assert(r==DB_NOTFOUND);
	}
	CKERR(txn->commit(txn, 0));
    }
    CKERR(db->close(db, 0));
}

static void waitfor (pid_t pid) {
    int status;
    pid_t pid2 = wait(&status);
    assert(pid2==pid);
    assert(WIFEXITED(status) && WEXITSTATUS(status)==0);
}

static void abort_number(int num, int count, DB_PREPLIST *l) {
    for (int j=0; j<count; j++) {
	if (l[j].gid[0]=='a'+num) {
	    CKERR(l[j].txn->abort(l[j].txn));
	    return;
	}
    }
    assert(0);
}
static void commit_number(int num, int count, DB_PREPLIST *l) {
    for (int j=0; j<count; j++) {
	if (l[j].gid[0]=='a'+num) {
	    CKERR(l[j].txn->commit(l[j].txn, 0));
	    return;
	}
    }
    assert(0);
}

static void test (void) {
    pid_t pid;

    if (0==(pid=fork())) {
	DB_ENV *env;
	setup_env_and_prepare(&env, ENVDIR);
	enum prepared_state prepared[NTXNS]={COMMITTED,ABORTED,PREPARED,PREPARED,PREPARED,PREPARED};
	check_prepared_txns(env, prepared);
	exit(0);
    }
    waitfor(pid);
    // Now run recovery and crash on purpose.
    if (0==(pid=fork())) {
	DB_ENV *env;
	setup_env(&env, ENVDIR);
	enum prepared_state prepared[NTXNS]={COMMITTED,ABORTED,PREPARED,PREPARED,PREPARED,PREPARED};
	check_prepared_txns(env, prepared);
	exit(0);
    }
    waitfor(pid);

    // Now see if recovery works the second time.
    if (0==(pid=fork())) {
	DB_ENV *env;
	setup_env(&env, ENVDIR);
	enum prepared_state prepared[NTXNS]={COMMITTED,ABORTED,PREPARED,PREPARED,PREPARED,PREPARED};
	check_prepared_txns(env, prepared);
	exit(0);
    }
    waitfor(pid);

    // Now see if recovery works the third time.
    if (0==(pid=fork())) {
	DB_ENV *env;
	setup_env(&env, ENVDIR);
	enum prepared_state prepared[NTXNS]={COMMITTED,ABORTED,PREPARED,PREPARED,PREPARED,PREPARED};
	DB_PREPLIST l[NTXNS];
	long count=-1;
	get_prepared(env, &count, l);
	check_prepared_list(prepared, count, l);
	abort_number(3, count, l);
	commit_number(2, count, l); // do the commit second so it will make it to disk.
	exit(0);
    }
    waitfor(pid);
    // Now see if recovery works a third time, with number 2 and 3 no longer in the prepared state.
    if (0==(pid=fork())) {
	DB_ENV *env;
	setup_env(&env, ENVDIR);
	enum prepared_state prepared[NTXNS]={COMMITTED,ABORTED,MAYBE_COMMITTED,MAYBE_ABORTED,PREPARED,PREPARED};
	DB_PREPLIST l[NTXNS];
	long count=-1;
	//printf("%s:%d count=%ld\n", __FILE__, __LINE__, count); // it's a little bit funky that the committed transactions in BDB (from commit_number(2,...) above) don't stay committed.  But whatever...
	get_prepared(env, &count, l);
	check_prepared_list(prepared, count, l);
	exit(0);
    }
    waitfor(pid);
    // Now see if recovery works a fourth time, with number 2 and 3 no longer in the prepared state.
    // This time we'll do get_prepared with a short count.
    if (0==(pid=fork())) {
	DB_ENV *env;
	setup_env(&env, ENVDIR);
	//printf("%s:%d count=%ld\n", __FILE__, __LINE__, count); // it's a little bit funky that the committed transactions in BDB (from commit_number(2,...) above) don't stay committed.  But whatever...

	long actual_count=0;

	for (int recover_num=0; 1; recover_num++) {
	    long count=-1;
	    DB_PREPLIST *MALLOC_N(1, l); // use malloc so that valgrind might notice a problem
	    CKERR(env->txn_recover(env, l, 1, &count, recover_num==0 ? DB_FIRST : DB_NEXT));
	    //printf("recover_num %d count=%ld\n", recover_num,count);
	    if (count==0) break;
	    actual_count++;
	    if ((l[0].gid[0]-'a')%2==0) {
		CKERR(l[0].txn->commit(l[0].txn, 0));
	    } else {
		CKERR(l[0].txn->abort(l[0].txn));
	    }
	    toku_free(l);
	}
	//printf("actual_count=%ld\n", actual_count);

	// Now let's see what the state is.
	check_state_after_full_recovery(env);

	CKERR(env->close(env, 0));
	exit(0);
    }
    waitfor(pid);
    // Now we should end up with nothing in the recovery list.
    {
	DB_ENV *env;
	setup_env(&env, ENVDIR);
	long count=-1;
	DB_PREPLIST l[1];
	CKERR(env->txn_recover(env, l, 1, &count, DB_FIRST));
	assert(count==0);
	check_state_after_full_recovery(env);
	CKERR(env->close(env, 0));
    }	
	

}

int test_main (int argc, char *const argv[]) {
    default_parse_args(argc, argv);
    // first test: open an environment, a db, a txn, and do a prepare.   Then do txn_prepare (without even closing the environment).
    test();
    
    return 0;
}
