package migrations

import (
	"code.justin.tv/d8a/iceman/lib/queries"
	"code.justin.tv/d8a/iceman/lib/tables"
	"code.justin.tv/d8a/iceman/test_common"
	"database/sql"
	"fmt"
	_ "github.com/mattn/go-sqlite3"
	"github.com/stretchr/testify/require"
	"testing"
	"time"
)

var (
	db            *sql.DB
	rows          *sql.Rows
	testDB        test_common.TestDB
	driverQueries queries.DriverQueries
	err           error
	dbType        string
)

func init() {
	dbType, testDB = test_common.ProcessDBTestFlags()
}

// setup db and tables
func setupDB(t *testing.T) {

	fmt.Printf("Testing on the \"%v\" database.\n", dbType)

	var err error
	db, driverQueries, err = testDB.SetupDB(t)
	require.Nil(t, err)
}

// close and remove db after test is finished

// run all migrations to completion
func TestMigrations(t *testing.T) {
	// initialize db
	setupDB(t)
	defer testDB.CleanupDB()

	// applying migrations
	var names []string
	names, err = RunMigrations(db, driverQueries, &DirectoryMigrationSource{MigrationsDirectory: "test_migrations"}, true)
	require.Nil(t, err)

	// assert that the names returned are correct
	require.Equal(t, []string{"CreateUserID", "RemoveUser"}, names, "names is incorrect")

	// verify rows in iceman_migrations are correct
	rows, err = db.Query("SELECT name, created_at FROM iceman_migrations ORDER BY applied_at DESC;")
	require.Nil(t, err)

	var migrations []string
	var name string

	var times []time.Time
	var created_at time.Time

	for rows.Next() {
		err = rows.Scan(&name, &created_at)
		require.Nil(t, err)
		migrations = append(migrations, name)
		times = append(times, created_at)
	}
	rows.Close()

	var time1 time.Time
	time1, err = time.Parse("20060102150405", "20160804234143")
	require.Nil(t, err)

	var time2 time.Time
	time2, err = time.Parse("20060102150405", "20160803215857")
	require.Nil(t, err)

	require.Equal(t, []string{"RemoveUser", "CreateUserID"}, migrations, "iceman_migrations names are incorrect")
	require.Equal(t, []time.Time{time1, time2}, times, "iceman_migrations times are incorrect")

	// verify rows in my_table
	rows, err = db.Query("SELECT user_id, nom FROM my_table ORDER BY user_id;")
	require.Nil(t, err)

	var ids []int
	var user_id int

	var noms []string
	var nom string

	for rows.Next() {
		err = rows.Scan(&user_id, &nom)
		require.Nil(t, err)
		ids = append(ids, user_id)
		noms = append(noms, nom)
	}
	rows.Close()

	require.Equal(t, []string{"user_b"}, noms, "migrations applied incorrectly")
	require.Equal(t, []int{67890}, ids, "migrations applied incorrectly")

	// try to apply migrations (there won't be any) and check if correct msg is outputted
	names, err = RunMigrations(db, driverQueries, &DirectoryMigrationSource{MigrationsDirectory: "test_migrations"}, true)
	require.Nil(t, err)

	require.Empty(t, names, "names should be empty")

	// roll back one migration and re-verify everything
	names, err = RunMigrations(db, driverQueries, &DirectoryMigrationSource{MigrationsDirectory: "test_migrations"}, false)
	require.Nil(t, err)

	// assert that the names returned are correct
	require.Equal(t, []string{"RemoveUser"}, names, "names is incorrect")

	// verify rows in iceman_migrations
	rows, err = db.Query("SELECT name, created_at FROM iceman_migrations;")
	require.Nil(t, err)

	migrations = make([]string, 0)
	times = make([]time.Time, 0)

	for rows.Next() {
		err = rows.Scan(&name, &created_at)
		require.Nil(t, err)
		migrations = append(migrations, name)
		times = append(times, created_at)
	}
	rows.Close()

	var _time time.Time
	_time, err = time.Parse("20060102150405", "20160803215857")
	require.Nil(t, err)

	require.Equal(t, []string{"CreateUserID"}, migrations, "iceman_migrations names are wrong")
	require.Equal(t, []time.Time{_time}, times, "iceman_migrations times are wrong")

	// verify rows in my_table
	rows, err = db.Query("SELECT user_id, nom FROM my_table ORDER BY user_id;")
	require.Nil(t, err)

	ids = ids[:0]
	noms = noms[:0]

	for rows.Next() {
		err = rows.Scan(&user_id, &nom)
		require.Nil(t, err)
		ids = append(ids, user_id)
		noms = append(noms, nom)
	}
	rows.Close()

	require.Equal(t, []string{"user_a", "user_b"}, noms, "migrations applied incorrectly")
	require.Equal(t, []int{12345, 67890}, ids, "migrations applied incorrectly")

	// roll back another one and re-verify everything
	names, err = RunMigrations(db, driverQueries, &DirectoryMigrationSource{MigrationsDirectory: "test_migrations"}, false)
	require.Nil(t, err)

	// assert that the names returned are correct
	require.Equal(t, []string{"CreateUserID"}, names, "names are incorrect")

	// verify rows in iceman_migrations
	rows, err = db.Query("SELECT name, created_at FROM iceman_migrations;")
	require.Nil(t, err)

	migrations = make([]string, 0)
	times = make([]time.Time, 0)

	for rows.Next() {
		err = rows.Scan(&name, &created_at)
		require.Nil(t, err)
		migrations = append(migrations, name)
		times = append(times, created_at)
	}
	rows.Close()

	require.Empty(t, migrations, "there should be nothing")
	require.Empty(t, times, "there should be nothing")

	// verify rows in my_table
	rows, err = db.Query("SELECT user_id, nom FROM my_table;")

	require.NotNil(t, err, "there should be no table")
	require.True(t, tables.IsMissingTableError(err), "wrong error")

	// try to roll back again (there won't be any) and check if correct msg is outputted
	names, err = RunMigrations(db, driverQueries, &DirectoryMigrationSource{MigrationsDirectory: "test_migrations"}, false)
	require.Nil(t, err)

	require.Empty(t, names, "there should be no migrations to roll back")

}

// run all migrations to completion
func TestMigrationsFailedTxn(t *testing.T) {
	// initialize db
	setupDB(t)
	defer testDB.CleanupDB()

	// applying migrations
	var names []string
	names, err = RunMigrations(db, driverQueries, &DirectoryMigrationSource{MigrationsDirectory: "test_migrations_failed_txn"}, true)
	require.NotNil(t, err)

	// assert that the names returned are correct
	require.Equal(t, []string{}, names, "names is incorrect")

	// verify no rows in iceman_migrations
	rows, err = db.Query("SELECT name, created_at FROM iceman_migrations ORDER BY applied_at DESC;")
	require.Nil(t, err)

	migrations := []string{}
	var name string

	var times []time.Time
	var created_at time.Time

	for rows.Next() {
		err = rows.Scan(&name, &created_at)
		require.Nil(t, err)
		migrations = append(migrations, name)
		times = append(times, created_at)
	}
	rows.Close()

	require.Equal(t, []string{}, migrations, "iceman_migrations names are incorrect")

	//Verify Row does not exist in table
	rows, err := db.Query("SELECT COUNT(*) FROM my_table")
	defer rows.Close()
	require.Nil(t, err)

	var count int
	require.NotNil(t, rows.Next())
	rows.Scan(&count)

	// Should have a count of 0
	require.Equal(t, 0, count, "Insert was not rolled back")
}

// with timeout trigger
func TestMigrationsTimeout(t *testing.T) {
	// initialize db
	setupDB(t)
	defer testDB.CleanupDB()

	// applying migrations
	var names []string
	names, err = RunMigrations(db, driverQueries, &DirectoryMigrationSource{MigrationsDirectory: "test_migrations_timeout"}, true)
	require.NotNil(t, err)
	require.Contains(t, err.Error(), "Migration failed for AddUsers")

	// assert that the names returned are correct
	require.Equal(t, []string{"CreateUserID"}, names, "names is incorrect")

	// verify rows in iceman_migrations are correct
	rows, err = db.Query("SELECT name, created_at FROM iceman_migrations ORDER BY applied_at DESC;")
	require.Nil(t, err)

	var migrations []string
	var name string

	var times []time.Time
	var created_at time.Time

	for rows.Next() {
		err = rows.Scan(&name, &created_at)
		require.Nil(t, err)
		migrations = append(migrations, name)
		times = append(times, created_at)
	}
	rows.Close()

	var _time time.Time
	_time, err = time.Parse("20060102150405", "20160803215857")
	require.Nil(t, err)

	require.Equal(t, []string{"CreateUserID"}, migrations, "iceman_migrations names are incorrect")
	require.Equal(t, []time.Time{_time}, times, "iceman_migrations times are incorrect")

	// verify rows in my_table
	rows, err = db.Query("SELECT user_id, nom FROM my_table ORDER BY user_id;")
	require.Nil(t, err)

	var ids []int
	var user_id int

	var noms []string
	var nom string

	for rows.Next() {
		err = rows.Scan(&user_id, &nom)
		require.Nil(t, err)
		ids = append(ids, user_id)
		noms = append(noms, nom)
	}
	rows.Close()

	require.Equal(t, []string{"user_a", "user_b"}, noms, "migrations applied incorrectly")
	require.Equal(t, []int{12345, 67890}, ids, "migrations applied incorrectly")

	// roll back one migration and re-verify everything
	names, err = RunMigrations(db, driverQueries, &DirectoryMigrationSource{MigrationsDirectory: "test_migrations_timeout"}, false)
	require.Nil(t, err)

	// assert that the names returned are correct
	require.Equal(t, []string{"CreateUserID"}, names, "names are incorrect")

	// verify rows in iceman_migrations
	rows, err = db.Query("SELECT name, created_at FROM iceman_migrations;")
	require.Nil(t, err)

	migrations = make([]string, 0)
	times = make([]time.Time, 0)

	for rows.Next() {
		err = rows.Scan(&name, &created_at)
		require.Nil(t, err)
		migrations = append(migrations, name)
		times = append(times, created_at)
	}
	rows.Close()

	require.Empty(t, migrations, "there should be nothing")
	require.Empty(t, times, "there should be nothing")

	// verify rows in my_table
	rows, err = db.Query("SELECT user_id, nom FROM my_table;")

	require.NotNil(t, err, "there should be no table")
	require.True(t, tables.IsMissingTableError(err), "wrong error")

	// try to roll back again (there won't be any) and check if correct msg is outputted
	names, err = RunMigrations(db, driverQueries, &DirectoryMigrationSource{MigrationsDirectory: "test_migrations_timeout"}, false)
	require.Nil(t, err)

	require.Empty(t, names, "there should be no migrations to roll back")

}

// non-transactional migration test
func TestMigrationsNonTxn(t *testing.T) {
	// initialize db
	setupDB(t)
	defer testDB.CleanupDB()

	// applying migrations
	var names []string
	names, err = RunMigrations(db, driverQueries, &DirectoryMigrationSource{MigrationsDirectory: "test_migrations_non_txn"}, true)
	require.NotNil(t, err)

	// assert that the names returned are correct
	require.Equal(t, []string{"CreateUserIDComplete", "AddUserIncompleteDown"}, names, "names is incorrect")

	// verify rows in iceman_migrations are correct
	rows, err = db.Query("SELECT name, created_at FROM iceman_migrations ORDER BY applied_at DESC;")
	require.Nil(t, err)

	var migrations []string
	var name string

	var times []time.Time
	var created_at time.Time

	for rows.Next() {
		err = rows.Scan(&name, &created_at)
		require.Nil(t, err)
		migrations = append(migrations, name)
		times = append(times, created_at)
	}
	rows.Close()

	var time1 time.Time
	time1, err = time.Parse("20060102150405", "20160803215857")
	require.Nil(t, err)

	var time2 time.Time
	time2, err = time.Parse("20060102150405", "20160803215856")
	require.Nil(t, err)

	require.Equal(t, []string{"AddUserIncompleteDown", "CreateUserIDComplete"}, migrations, "iceman_migrations names are incorrect")
	require.Equal(t, []time.Time{time1, time2}, times, "iceman_migrations times are incorrect")

	// verify rows in my_table
	rows, err = db.Query("SELECT user_id, nom FROM my_table ORDER BY user_id;")
	require.Nil(t, err)

	var ids []int

	var noms []string

	for rows.Next() {
		var user_id int
		var nom string
		err = rows.Scan(&user_id, &nom)
		require.Nil(t, err)
		ids = append(ids, user_id)
		noms = append(noms, nom)
	}
	rows.Close()

	require.Equal(t, []string{"a", "b", "c", "user_a", "user_b"}, noms, "migrations applied incorrectly")
	require.Equal(t, []int{1, 2, 3, 12345, 67890}, ids, "migrations applied incorrectly")

	// roll back one migration and re-verify everything
	names, err = RunMigrations(db, driverQueries, &DirectoryMigrationSource{MigrationsDirectory: "test_migrations_non_txn"}, false)
	require.NotNil(t, err)

	// assert that the names returned are correct
	require.Empty(t, names, "names should be empty")

	// verify rows in iceman_migrations
	rows, err = db.Query("SELECT name, created_at FROM iceman_migrations ORDER BY applied_at DESC;")
	require.Nil(t, err)

	migrations = make([]string, 0)
	times = make([]time.Time, 0)

	for rows.Next() {
		err = rows.Scan(&name, &created_at)
		require.Nil(t, err)
		migrations = append(migrations, name)
		times = append(times, created_at)
	}
	rows.Close()

	require.Equal(t, []string{"AddUserIncompleteDown", "CreateUserIDComplete"}, migrations, "iceman_migrations names are incorrect")
	require.Equal(t, []time.Time{time1, time2}, times, "iceman_migrations times are incorrect")

	// verify rows in my_table
	rows, err = db.Query("SELECT user_id, nom FROM my_table ORDER BY user_id;")
	require.Nil(t, err)

	ids = make([]int, 0)
	noms = make([]string, 0)

	for rows.Next() {
		var user_id int
		var nom string
		err = rows.Scan(&user_id, &nom)
		require.Nil(t, err)
		ids = append(ids, user_id)
		noms = append(noms, nom)
	}
	rows.Close()

	require.Equal(t, []string{"b", "c", "user_a", "user_b"}, noms, "migrations applied incorrectly")
	require.Equal(t, []int{2, 3, 12345, 67890}, ids, "migrations applied incorrectly")

}
