package database

import (
	"log"
	"os"
	"testing"
	"time"

	"github.com/alicebob/miniredis/v2"
	"github.com/forceu/gokapi/internal/configuration/database/dbabstraction"
	"github.com/forceu/gokapi/internal/configuration/database/dbcache"
	"github.com/forceu/gokapi/internal/models"
	"github.com/forceu/gokapi/internal/test"
)

var configSqlite = models.DbConnection{
	HostUrl: "./test/gokapi.sqlite",
	Type:    0, // dbabstraction.TypeSqlite
}

var configRedis = models.DbConnection{
	RedisPrefix: "test_",
	HostUrl:     "127.0.0.1:26379",
	Type:        1, // dbabstraction.TypeRedis
}

var mRedis *miniredis.Miniredis

var availableDatabases []dbabstraction.Database

func TestMain(m *testing.M) {

	mRedis = miniredis.NewMiniRedis()
	err := mRedis.StartAddr("127.0.0.1:26379")
	if err != nil {
		log.Fatal("Could not start miniredis")
	}
	exitVal := m.Run()
	mRedis.Close()
	os.RemoveAll("./test/")
	os.Exit(exitVal)
}

func TestInit(t *testing.T) {
	availableDatabases = make([]dbabstraction.Database, 0)
	Connect(configRedis)
	availableDatabases = append(availableDatabases, db)
	Connect(configSqlite)
	availableDatabases = append(availableDatabases, db)
	defer test.ExpectPanic(t)
	Connect(models.DbConnection{Type: 2})
}

func TestApiKeys(t *testing.T) {
	runAllTypesCompareOutput(t, func() any { return GetAllApiKeys() }, map[string]models.ApiKey{})
	newApiKey := models.ApiKey{
		Id:           "test",
		FriendlyName: "testKey",
		PublicId:     "wfwefewwfefwe",
		LastUsed:     1000,
		Permissions:  10,
	}
	runAllTypesNoOutput(t, func() { SaveApiKey(newApiKey) })
	runAllTypesCompareTwoOutputs(t, func() (any, any) {
		return GetApiKey("test")
	}, newApiKey, true)
	newApiKey.LastUsed = 2000
	runAllTypesNoOutput(t, func() { UpdateTimeApiKey(newApiKey) })
	runAllTypesCompareOutput(t, func() any { return GetAllApiKeys() }, map[string]models.ApiKey{"test": newApiKey})
	runAllTypesNoOutput(t, func() { DeleteApiKey("test") })
	runAllTypesCompareTwoOutputs(t, func() (any, any) {
		return GetApiKey("test")
	}, models.ApiKey{}, false)

	runAllTypesNoOutput(t, func() {
		SaveApiKey(models.ApiKey{
			Id:       "publicTest",
			PublicId: "publicId",
		})
	})
	runAllTypesCompareOutput(t, func() any {
		_, ok := GetApiKey("publicTest")
		return ok
	}, true)
	runAllTypesCompareOutput(t, func() any {
		_, ok := GetApiKeyByPublicKey("publicTest")
		return ok
	}, false)
	runAllTypesCompareOutput(t, func() any {
		_, ok := GetApiKey("publicId")
		return ok
	}, false)
	runAllTypesCompareTwoOutputs(t, func() (any, any) {
		return GetApiKeyByPublicKey("publicId")
	}, "publicTest", true)
}

func TestE2E(t *testing.T) {
	input := models.E2EInfoEncrypted{
		Version:        1,
		Nonce:          []byte("test"),
		Content:        []byte("test2"),
		AvailableFiles: []string{"should", "not", "be", "saved"},
	}
	runAllTypesNoOutput(t, func() { SaveEnd2EndInfo(input, 3) })
	input.AvailableFiles = nil
	runAllTypesCompareOutput(t, func() any { return GetEnd2EndInfo(3) }, input)
	runAllTypesNoOutput(t, func() { DeleteEnd2EndInfo(3) })
	runAllTypesCompareOutput(t, func() any { return GetEnd2EndInfo(3) }, models.E2EInfoEncrypted{})
}

func TestSessions(t *testing.T) {
	runAllTypesCompareTwoOutputs(t, func() (any, any) { return GetSession("newsession") }, models.Session{}, false)
	input := models.Session{
		RenewAt:    time.Now().Add(10 * time.Second).Unix(),
		ValidUntil: time.Now().Add(20 * time.Second).Unix(),
	}
	runAllTypesNoOutput(t, func() { SaveSession("newsession", input) })
	runAllTypesCompareTwoOutputs(t, func() (any, any) { return GetSession("newsession") }, input, true)
	runAllTypesNoOutput(t, func() { DeleteSession("newsession") })
	runAllTypesCompareTwoOutputs(t, func() (any, any) { return GetSession("newsession") }, models.Session{}, false)
	runAllTypesNoOutput(t, func() { SaveSession("newsession", input) })
	runAllTypesCompareTwoOutputs(t, func() (any, any) { return GetSession("newsession") }, input, true)
	runAllTypesNoOutput(t, func() { DeleteAllSessions() })
	runAllTypesCompareTwoOutputs(t, func() (any, any) { return GetSession("newsession") }, models.Session{}, false)

	runAllTypesNoOutput(t, func() {
		SaveSession("session1", models.Session{
			RenewAt:    2147483645,
			ValidUntil: 2147483645,
			UserId:     20,
		})
		SaveSession("session2", models.Session{
			RenewAt:    2147483645,
			ValidUntil: 2147483645,
			UserId:     20,
		})
		SaveSession("session3", models.Session{
			RenewAt:    2147483645,
			ValidUntil: 2147483645,
			UserId:     40,
		})
	})

	runAllTypesCompareOutput(t, func() any {
		_, ok := GetSession("session1")
		return ok
	}, true)
	runAllTypesCompareOutput(t, func() any {
		_, ok := GetSession("session2")
		return ok
	}, true)
	runAllTypesCompareOutput(t, func() any {
		_, ok := GetSession("session3")
		return ok
	}, true)
	runAllTypesNoOutput(t, func() {
		DeleteAllSessionsByUser(20)
	})
	runAllTypesCompareOutput(t, func() any {
		_, ok := GetSession("session1")
		return ok
	}, false)
	runAllTypesCompareOutput(t, func() any {
		_, ok := GetSession("session2")
		return ok
	}, false)
	runAllTypesCompareOutput(t, func() any {
		_, ok := GetSession("session3")
		return ok
	}, true)
}

func TestHotlinks(t *testing.T) {
	runAllTypesCompareTwoOutputs(t, func() (any, any) { return GetHotlink("newhotlink") }, "", false)
	newFile := models.File{Id: "testfile",
		HotlinkId: "newhotlink"}
	runAllTypesNoOutput(t, func() { SaveHotlink(newFile) })
	runAllTypesCompareTwoOutputs(t, func() (any, any) { return GetHotlink("newhotlink") }, "testfile", true)
	runAllTypesCompareOutput(t, func() any { return GetAllHotlinks() }, []string{"newhotlink"})
	runAllTypesNoOutput(t, func() { DeleteHotlink("newhotlink") })
	runAllTypesCompareOutput(t, func() any { return GetAllHotlinks() }, []string{})
}

func TestMetaData(t *testing.T) {
	runAllTypesCompareOutput(t, func() any { return GetAllMetadata() }, map[string]models.File{})
	runAllTypesCompareTwoOutputs(t, func() (any, any) { return GetMetaDataById("testid") }, models.File{}, false)
	file := models.File{
		Id:                 "testid",
		Name:               "Testname",
		Size:               "3Kb",
		SHA1:               "12345556",
		PasswordHash:       "sfffwefwe",
		HotlinkId:          "hotlink",
		ContentType:        "none",
		AwsBucket:          "aws1",
		ExpireAt:           time.Now().Add(10 * time.Second).Unix(),
		PendingDeletion:    time.Now().Add(8 * time.Second).Unix(),
		UploadDate:         time.Now().Unix(),
		SizeBytes:          3 * 1024,
		DownloadsRemaining: 2,
		DownloadCount:      5,
		Encryption: models.EncryptionInfo{
			IsEncrypted:         true,
			IsEndToEndEncrypted: true,
			DecryptionKey:       []byte("dekey"),
			Nonce:               []byte("nonce"),
		},
		UnlimitedDownloads: true,
		UnlimitedTime:      true,
	}
	runAllTypesNoOutput(t, func() { SaveMetaData(file) })
	runAllTypesCompareTwoOutputs(t, func() (any, any) { return GetMetaDataById("testid") }, file, true)
	runAllTypesCompareOutput(t, func() any { return GetAllMetadata() }, map[string]models.File{"testid": file})
	runAllTypesNoOutput(t, func() { DeleteMetaData("testid") })
	runAllTypesCompareOutput(t, func() any { return GetAllMetadata() }, map[string]models.File{})
	runAllTypesCompareTwoOutputs(t, func() (any, any) { return GetMetaDataById("testid") }, models.File{}, false)

	increasedDownload := file
	increasedDownload.DownloadCount = increasedDownload.DownloadCount + 1

	runAllTypesCompareTwoOutputs(t, func() (any, any) {
		SaveMetaData(file)
		IncreaseDownloadCount(file.Id, false)
		return GetMetaDataById(file.Id)
	}, increasedDownload, true)

	increasedDownload.DownloadCount = increasedDownload.DownloadCount + 1
	increasedDownload.DownloadsRemaining = increasedDownload.DownloadsRemaining - 1

	runAllTypesCompareTwoOutputs(t, func() (any, any) {
		IncreaseDownloadCount(file.Id, true)
		return GetMetaDataById(file.Id)
	}, increasedDownload, true)
	runAllTypesNoOutput(t, func() { DeleteMetaData(file.Id) })
}

func TestUsers(t *testing.T) {
	runAllTypesCompareOutput(t, func() any { return len(GetAllUsers()) }, 0)
	user := models.User{
		Id:            1000,
		Name:          "test2",
		Permissions:   models.UserPermissionNone,
		UserLevel:     models.UserLevelAdmin,
		LastOnline:    1338,
		Password:      "1234568",
		ResetPassword: true,
	}
	runAllTypesNoOutput(t, func() { SaveUser(user, true) })
	user.Id = 1
	runAllTypesCompareTwoOutputs(t, func() (any, any) {
		return GetUser(1)
	}, user, true)
	runAllTypesCompareOutput(t, func() any { return len(GetAllUsers()) }, 1)
	user.Name = "test3"
	runAllTypesNoOutput(t, func() { SaveUser(user, false) })
	runAllTypesCompareOutput(t, func() any { return len(GetAllUsers()) }, 1)
	runAllTypesCompareTwoOutputs(t, func() (any, any) {
		return GetUserByName("test3")
	}, user, true)
	runAllTypesCompareTwoOutputs(t, func() (any, any) {
		return GetUserByName("TEST3")
	}, user, true)
	user.Name = "test4"
	runAllTypesNoOutput(t, func() { SaveUser(user, true) })
	var allUsersSqlite []models.User
	var allUsersRedis []models.User
	runAllTypesCompareOutput(t, func() any {
		allUsers := GetAllUsers()
		switch db.GetType() {
		case dbabstraction.TypeSqlite:
			allUsersSqlite = allUsers
		case dbabstraction.TypeRedis:
			allUsersRedis = allUsers
		default:
			t.Fatal("Unrecognized database type")
		}
		return len(GetAllUsers())
	}, 2)
	test.IsEqual(t, allUsersSqlite, allUsersRedis)
	runAllTypesNoOutput(t, func() {
		dbcache.Init()
		UpdateUserLastOnline(1)
	})
	runAllTypesCompareTwoOutputs(t, func() (any, any) {
		retrievedUser, ok := GetUser(1)
		isUpdated := time.Now().Unix()-retrievedUser.LastOnline < 5 && time.Now().Unix()-retrievedUser.LastOnline > -1
		return isUpdated, ok
	}, true, true)
	runAllTypesNoOutput(t, func() { DeleteUser(1) })
	runAllTypesCompareOutput(t, func() any {
		_, ok := GetUser(1)
		return ok
	}, false)

	user.Id = 10
	user.Name = "TEST5"
	runAllTypesNoOutput(t, func() { SaveUser(user, false) })
	runAllTypesCompareOutput(t, func() any {
		retrievedUser, _ := GetUser(10)
		return retrievedUser.Name
	}, "test5")

	runAllTypesCompareOutput(t, func() any {
		_, ok := GetSuperAdmin()
		return ok
	}, false)

	runAllTypesCompareOutput(t, func() any {
		err := EditSuperAdmin("user", "password")
		return err == nil
	}, false)

	runAllTypesNoOutput(t, func() {
		users := GetAllUsers()
		for _, rUser := range users {
			DeleteUser(rUser.Id)
		}
	})
	runAllTypesCompareOutput(t, func() any { return len(GetAllUsers()) }, 0)

	runAllTypesCompareOutput(t, func() any {
		return EditSuperAdmin("username", "pwhash")
	}, nil)
	runAllTypesCompareOutput(t, func() any {
		admin, ok := GetSuperAdmin()
		test.IsEqualInt(t, int(admin.Permissions), int(models.UserPermissionAll))
		test.IsEqualInt(t, int(admin.UserLevel), int(models.UserLevelSuperAdmin))
		test.IsEqualString(t, admin.Name, "username")
		test.IsEqualString(t, admin.Password, "pwhash")
		return ok
	}, true)

	runAllTypesNoOutput(t, func() {
		err := EditSuperAdmin("username2", "")
		test.IsNil(t, err)
		admin, ok := GetSuperAdmin()
		test.IsEqualBool(t, ok, true)
		test.IsEqualString(t, admin.Name, "username2")
		test.IsEqualString(t, admin.Password, "pwhash")
	})
	runAllTypesNoOutput(t, func() {
		err := EditSuperAdmin("", "pwhash2")
		test.IsNil(t, err)
		admin, ok := GetSuperAdmin()
		test.IsEqualBool(t, ok, true)
		test.IsEqualString(t, admin.Name, "username2")
		test.IsEqualString(t, admin.Password, "pwhash2")
	})

	user.Name = ""
	defer test.ExpectPanic(t)
	SaveUser(user, true)
}

func TestUpgrade(t *testing.T) {
	runAllTypesNoOutput(t, func() { test.IsEqualBool(t, db.GetDbVersion() != 1, true) })
	runAllTypesNoOutput(t, func() { db.SetDbVersion(1) })
	runAllTypesNoOutput(t, func() { test.IsEqualInt(t, db.GetDbVersion(), 1) })
	// runAllTypesNoOutput(t, func() { Upgrade() })
	// runAllTypesNoOutput(t, func() { test.IsEqualInt(t, db.GetDbVersion(), db.GetSchemaVersion()) })
}

func TestRunGarbageCollection(t *testing.T) {
	runAllTypesNoOutput(t, func() { RunGarbageCollection() })
}

func TestClose(t *testing.T) {
	runAllTypesNoOutput(t, func() { Close() })
}

func runAllTypesNoOutput(t *testing.T, functionToRun func()) {
	t.Helper()
	for _, database := range availableDatabases {
		db = database
		functionToRun()
	}
}

func runAllTypesCompareOutput(t *testing.T, functionToRun func() any, expectedOutput any) {
	t.Helper()
	for _, database := range availableDatabases {
		db = database
		output := functionToRun()
		test.IsEqual(t, output, expectedOutput)
	}
}

func runAllTypesCompareTwoOutputs(t *testing.T, functionToRun func() (any, any), expectedOutput1, expectedOutput2 any) {
	t.Helper()
	for _, database := range availableDatabases {
		db = database
		output1, output2 := functionToRun()
		test.IsEqual(t, output1, expectedOutput1)
		test.IsEqual(t, output2, expectedOutput2)
	}
}

func TestParseUrl(t *testing.T) {
	expectedOutput := models.DbConnection{}
	output, err := ParseUrl("invalid", false)
	test.IsNotNil(t, err)
	test.IsEqual(t, output, expectedOutput)

	_, err = ParseUrl("", false)
	test.IsNotNil(t, err)
	_, err = ParseUrl("inv\r\nalid", false)
	test.IsNotNil(t, err)
	_, err = ParseUrl("", false)
	test.IsNotNil(t, err)

	expectedOutput = models.DbConnection{
		HostUrl: "./test",
		Type:    dbabstraction.TypeSqlite,
	}
	output, err = ParseUrl("sqlite://./test", false)
	test.IsNil(t, err)
	test.IsEqual(t, output, expectedOutput)

	_, err = ParseUrl("sqlite:///invalid", true)
	test.IsNotNil(t, err)
	output, err = ParseUrl("sqlite:///invalid", false)
	test.IsNil(t, err)
	test.IsEqualString(t, output.HostUrl, "/invalid")

	expectedOutput = models.DbConnection{
		HostUrl:     "127.0.0.1:1234",
		RedisPrefix: "",
		Username:    "",
		Password:    "",
		RedisUseSsl: false,
		Type:        dbabstraction.TypeRedis,
	}
	output, err = ParseUrl("redis://127.0.0.1:1234", false)
	test.IsNil(t, err)
	test.IsEqual(t, output, expectedOutput)

	expectedOutput = models.DbConnection{
		HostUrl:     "127.0.0.1:1234",
		RedisPrefix: "tpref",
		Username:    "tuser",
		Password:    "tpw",
		RedisUseSsl: true,
		Type:        dbabstraction.TypeRedis,
	}
	output, err = ParseUrl("redis://tuser:tpw@127.0.0.1:1234/?ssl=true&prefix=tpref", false)
	test.IsNil(t, err)
	test.IsEqual(t, output, expectedOutput)
}

func TestMigration(t *testing.T) {
	configNew := models.DbConnection{
		RedisPrefix: "testmigrate_",
		HostUrl:     "127.0.0.1:26379",
		Type:        1, // dbabstraction.TypeRedis
	}
	dbOld, err := dbabstraction.GetNew(configSqlite)
	test.IsNil(t, err)
	testFile := models.File{Id: "file1234", HotlinkId: "hotlink123"}
	dbOld.SaveMetaData(testFile)
	dbOld.SaveHotlink(testFile)
	dbOld.SaveApiKey(models.ApiKey{Id: "api123"})
	dbOld.SaveHotlink(testFile)
	dbOld.Close()

	Migrate(configSqlite, configNew)

	dbNew, err := dbabstraction.GetNew(configNew)
	test.IsNil(t, err)
	_, ok := dbNew.GetHotlink("hotlink123")
	test.IsEqualBool(t, ok, true)
	_, ok = dbNew.GetApiKey("api123")
	test.IsEqualBool(t, ok, true)
	_, ok = dbNew.GetMetaDataById("file1234")
	test.IsEqualBool(t, ok, true)
}
