package storage

import (
	"database/sql"
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/go-sql-driver/mysql"
	"github.com/stretchr/testify/assert"
)

func TestMySQLUpdateCurrent(t *testing.T) {
	db, err := sqlmock.New()
	assert.Nil(t, err, "Could not initialize mock DB")
	s := NewMySQLStorage(db)
	update := MetaUpdate{
		Role:    "root",
		Version: 0,
		Data:    []byte("1"),
	}
	sqlmock.ExpectQuery("SELECT count\\(\\*\\) FROM `tuf_files` WHERE `gun`=\\? AND `role`=\\? AND `version`>=\\?\\;").WithArgs(
		"testGUN",
		update.Role,
		update.Version,
	).WillReturnRows(sqlmock.RowsFromCSVString([]string{"count(*)"}, "0"))
	sqlmock.ExpectExec("INSERT INTO `tuf_files` \\(`gun`, `role`, `version`, `data`\\) VALUES \\(\\?,\\?,\\?,\\?\\);").WithArgs(
		"testGUN",
		update.Role,
		update.Version,
		update.Data,
	).WillReturnResult(sqlmock.NewResult(0, 1))

	err = s.UpdateCurrent(
		"testGUN",
		update,
	)
	assert.Nil(t, err, "UpdateCurrent errored unexpectedly: %v", err)

	// There's a bug in the mock lib
	//err = db.Close()
	//assert.Nil(t, err, "Expectation not met: %v", err)
}

func TestMySQLUpdateCurrentError(t *testing.T) {
	db, err := sqlmock.New()
	assert.Nil(t, err, "Could not initialize mock DB")
	s := NewMySQLStorage(db)
	update := MetaUpdate{
		Role:    "root",
		Version: 0,
		Data:    []byte("1"),
	}
	sqlmock.ExpectQuery("SELECT count\\(\\*\\) FROM `tuf_files` WHERE `gun`=\\? AND `role`=\\? AND `version`>=\\?\\;").WithArgs(
		"testGUN",
		update.Role,
		update.Version,
	).WillReturnRows(sqlmock.RowsFromCSVString([]string{"count(*)"}, "0"))
	sqlmock.ExpectExec("INSERT INTO `tuf_files` \\(`gun`, `role`, `version`, `data`\\) VALUES \\(\\?,\\?,\\?,\\?\\);").WithArgs(
		"testGUN",
		update.Role,
		update.Version,
		update.Data,
	).WillReturnError(
		&mysql.MySQLError{
			Number:  1022,
			Message: "Duplicate key error",
		},
	)

	err = s.UpdateCurrent(
		"testGUN",
		update,
	)
	assert.NotNil(t, err, "Error should not be nil")
	assert.IsType(t, &ErrOldVersion{}, err, "Expected ErrOldVersion error type, got: %v", err)

	// There's a bug in the mock lib
	//err = db.Close()
	//assert.Nil(t, err, "Expectation not met: %v", err)
}

func TestMySQLUpdateMany(t *testing.T) {
	db, err := sqlmock.New()
	assert.Nil(t, err, "Could not initialize mock DB")
	s := NewMySQLStorage(db)
	update1 := MetaUpdate{
		Role:    "root",
		Version: 0,
		Data:    []byte("1"),
	}
	update2 := MetaUpdate{
		Role:    "targets",
		Version: 1,
		Data:    []byte("2"),
	}
	// start transation
	sqlmock.ExpectBegin()

	sqlmock.ExpectQuery("SELECT count\\(\\*\\) FROM `tuf_files` WHERE `gun`=\\? AND `role`=\\? AND `version`>=\\?\\;").WithArgs(
		"testGUN",
		update1.Role,
		update1.Version,
	).WillReturnRows(sqlmock.RowsFromCSVString([]string{"count(*)"}, "0"))
	sqlmock.ExpectExec("INSERT INTO `tuf_files` \\(`gun`, `role`, `version`, `data`\\) VALUES \\(\\?,\\?,\\?,\\?\\);").WithArgs(
		"testGUN",
		update1.Role,
		update1.Version,
		update1.Data,
	).WillReturnResult(sqlmock.NewResult(0, 1))

	sqlmock.ExpectQuery("SELECT count\\(\\*\\) FROM `tuf_files` WHERE `gun`=\\? AND `role`=\\? AND `version`>=\\?\\;").WithArgs(
		"testGUN",
		update2.Role,
		update2.Version,
	).WillReturnRows(sqlmock.RowsFromCSVString([]string{"count(*)"}, "0"))
	sqlmock.ExpectExec("INSERT INTO `tuf_files` \\(`gun`, `role`, `version`, `data`\\) VALUES \\(\\?,\\?,\\?,\\?\\);").WithArgs(
		"testGUN",
		update2.Role,
		update2.Version,
		update2.Data,
	).WillReturnResult(sqlmock.NewResult(0, 1))

	// expect commit
	sqlmock.ExpectCommit()

	err = s.UpdateMany(
		"testGUN",
		[]MetaUpdate{update1, update2},
	)
	assert.Nil(t, err, "UpdateMany errored unexpectedly: %v", err)

	// There's a bug in the mock lib
	//err = db.Close()
	//assert.Nil(t, err, "Expectation not met: %v", err)
}

func TestMySQLUpdateManyRollback(t *testing.T) {
	db, err := sqlmock.New()
	assert.Nil(t, err, "Could not initialize mock DB")
	s := NewMySQLStorage(db)
	update := MetaUpdate{
		Role:    "root",
		Version: 0,
		Data:    []byte("1"),
	}
	execError := mysql.MySQLError{}
	// start transation
	sqlmock.ExpectBegin()

	sqlmock.ExpectQuery("SELECT count\\(\\*\\) FROM `tuf_files` WHERE `gun`=\\? AND `role`=\\? AND `version`>=\\?\\;").WithArgs(
		"testGUN",
		update.Role,
		update.Version,
	).WillReturnRows(sqlmock.RowsFromCSVString([]string{"count(*)"}, "0"))
	// insert first update
	sqlmock.ExpectExec("INSERT INTO `tuf_files` \\(`gun`, `role`, `version`, `data`\\) VALUES \\(\\?,\\?,\\?,\\?\\);").WithArgs(
		"testGUN",
		update.Role,
		update.Version,
		update.Data,
	).WillReturnError(&execError)

	// expect commit
	sqlmock.ExpectRollback()

	err = s.UpdateMany(
		"testGUN",
		[]MetaUpdate{update},
	)
	assert.IsType(t, &execError, err, "UpdateMany returned wrong error type")

	err = db.Close()
	assert.Nil(t, err, "Expectation not met: %v", err)
}

func TestMySQLUpdateManyDuplicate(t *testing.T) {
	db, err := sqlmock.New()
	assert.Nil(t, err, "Could not initialize mock DB")
	s := NewMySQLStorage(db)
	update := MetaUpdate{
		Role:    "root",
		Version: 0,
		Data:    []byte("1"),
	}
	execError := mysql.MySQLError{Number: 1022}
	// start transation
	sqlmock.ExpectBegin()

	sqlmock.ExpectQuery("SELECT count\\(\\*\\) FROM `tuf_files` WHERE `gun`=\\? AND `role`=\\? AND `version`>=\\?\\;").WithArgs(
		"testGUN",
		update.Role,
		update.Version,
	).WillReturnRows(sqlmock.RowsFromCSVString([]string{"count(*)"}, "0"))
	// insert first update
	sqlmock.ExpectExec("INSERT INTO `tuf_files` \\(`gun`, `role`, `version`, `data`\\) VALUES \\(\\?,\\?,\\?,\\?\\);").WithArgs(
		"testGUN",
		update.Role,
		update.Version,
		update.Data,
	).WillReturnError(&execError)

	// expect commit
	sqlmock.ExpectRollback()

	err = s.UpdateMany(
		"testGUN",
		[]MetaUpdate{update},
	)
	assert.IsType(t, &ErrOldVersion{}, err, "UpdateMany returned wrong error type")

	err = db.Close()
	assert.Nil(t, err, "Expectation not met: %v", err)
}

func TestMySQLGetCurrent(t *testing.T) {
	db, err := sqlmock.New()
	assert.Nil(t, err, "Could not initialize mock DB")
	s := NewMySQLStorage(db)

	sqlmock.ExpectQuery(
		"SELECT `data` FROM `tuf_files` WHERE `gun`=\\? AND `role`=\\? ORDER BY `version` DESC LIMIT 1;",
	).WithArgs("testGUN", "root").WillReturnRows(
		sqlmock.RowsFromCSVString(
			[]string{"data"},
			"1",
		),
	)

	byt, err := s.GetCurrent("testGUN", "root")
	assert.Nil(t, err, "Expected nil error from GetCurrent")
	assert.Equal(t, []byte("1"), byt, "Returned data was no correct")

	// TODO(endophage): these two lines are breaking because there
	//                  seems to be some problem with go-sqlmock
	//err = db.Close()
	//assert.Nil(t, err, "Expectation not met: %v", err)
}

func TestMySQLDelete(t *testing.T) {
	db, err := sqlmock.New()
	assert.Nil(t, err, "Could not initialize mock DB")
	s := NewMySQLStorage(db)

	sqlmock.ExpectExec(
		"DELETE FROM `tuf_files` WHERE `gun`=\\?;",
	).WithArgs("testGUN").WillReturnResult(sqlmock.NewResult(0, 1))

	err = s.Delete("testGUN")
	assert.Nil(t, err, "Expected nil error from Delete")

	err = db.Close()
	assert.Nil(t, err, "Expectation not met: %v", err)
}

func TestMySQLGetTimestampKeyNoKey(t *testing.T) {
	db, err := sqlmock.New()
	assert.Nil(t, err, "Could not initialize mock DB")
	s := NewMySQLStorage(db)

	sqlmock.ExpectQuery(
		"SELECT `cipher`, `public` FROM `timestamp_keys` WHERE `gun`=\\?;",
	).WithArgs("testGUN").WillReturnError(sql.ErrNoRows)

	_, _, err = s.GetTimestampKey("testGUN")
	assert.IsType(t, &ErrNoKey{}, err, "Expected ErrNoKey from GetTimestampKey")

	//err = db.Close()
	//assert.Nil(t, err, "Expectation not met: %v", err)
}

func TestMySQLSetTimestampKeyExists(t *testing.T) {
	db, err := sqlmock.New()
	assert.Nil(t, err, "Could not initialize mock DB")
	s := NewMySQLStorage(db)

	sqlmock.ExpectExec(
		"INSERT INTO `timestamp_keys` \\(`gun`, `cipher`, `public`\\) VALUES \\(\\?,\\?,\\?\\);",
	).WithArgs(
		"testGUN",
		"testCipher",
		[]byte("1"),
	).WillReturnError(
		&mysql.MySQLError{Number: 1022},
	)

	err = s.SetTimestampKey("testGUN", "testCipher", []byte("1"))
	assert.IsType(t, &ErrTimestampKeyExists{}, err, "Expected ErrTimestampKeyExists from SetTimestampKey")

	err = db.Close()
	assert.Nil(t, err, "Expectation not met: %v", err)
}
