package daos_test

import (
	"errors"
	"testing"
	"time"

	"github.com/pocketbase/pocketbase/daos"
	"github.com/pocketbase/pocketbase/models"
	"github.com/pocketbase/pocketbase/tests"
)

func TestNew(t *testing.T) {
	testApp, _ := tests.NewTestApp()
	defer testApp.Cleanup()

	dao := daos.New(testApp.DB())

	if dao.DB() != testApp.DB() {
		t.Fatal("The 2 db instances are different")
	}
}

func TestNewMultiDB(t *testing.T) {
	testApp, _ := tests.NewTestApp()
	defer testApp.Cleanup()

	dao := daos.NewMultiDB(testApp.Dao().ConcurrentDB(), testApp.Dao().NonconcurrentDB())

	if dao.DB() != testApp.Dao().ConcurrentDB() {
		t.Fatal("[db-concurrentDB] The 2 db instances are different")
	}

	if dao.ConcurrentDB() != testApp.Dao().ConcurrentDB() {
		t.Fatal("[concurrentDB-concurrentDB] The 2 db instances are different")
	}

	if dao.NonconcurrentDB() != testApp.Dao().NonconcurrentDB() {
		t.Fatal("[nonconcurrentDB-nonconcurrentDB] The 2 db instances are different")
	}
}

func TestDaoModelQuery(t *testing.T) {
	testApp, _ := tests.NewTestApp()
	defer testApp.Cleanup()

	dao := daos.New(testApp.DB())

	scenarios := []struct {
		model    models.Model
		expected string
	}{
		{
			&models.Collection{},
			"SELECT {{_collections}}.* FROM `_collections`",
		},
		{
			&models.Admin{},
			"SELECT {{_admins}}.* FROM `_admins`",
		},
		{
			&models.Request{},
			"SELECT {{_requests}}.* FROM `_requests`",
		},
	}

	for i, scenario := range scenarios {
		sql := dao.ModelQuery(scenario.model).Build().SQL()
		if sql != scenario.expected {
			t.Errorf("(%d) Expected select %s, got %s", i, scenario.expected, sql)
		}
	}
}

func TestDaoModelQueryCancellation(t *testing.T) {
	testApp, _ := tests.NewTestApp()
	defer testApp.Cleanup()

	dao := daos.New(testApp.DB())

	m := &models.Admin{}

	if err := dao.ModelQuery(m).One(m); err != nil {
		t.Fatalf("Failed to execute control query: %v", err)
	}

	dao.ModelQueryTimeout = 0 * time.Millisecond
	if err := dao.ModelQuery(m).One(m); err == nil {
		t.Fatal("Expected to be cancelled, got nil")
	}
}

func TestDaoFindById(t *testing.T) {
	testApp, _ := tests.NewTestApp()
	defer testApp.Cleanup()

	scenarios := []struct {
		model       models.Model
		id          string
		expectError bool
	}{
		// missing id
		{
			&models.Collection{},
			"missing",
			true,
		},
		// existing collection id
		{
			&models.Collection{},
			"wsmn24bux7wo113",
			false,
		},
		// existing admin id
		{
			&models.Admin{},
			"sbmbsdb40jyxf7h",
			false,
		},
	}

	for i, scenario := range scenarios {
		err := testApp.Dao().FindById(scenario.model, scenario.id)
		hasErr := err != nil
		if hasErr != scenario.expectError {
			t.Errorf("(%d) Expected %v, got %v", i, scenario.expectError, err)
		}

		if !scenario.expectError && scenario.id != scenario.model.GetId() {
			t.Errorf("(%d) Expected model with id %v, got %v", i, scenario.id, scenario.model.GetId())
		}
	}
}

func TestDaoRunInTransaction(t *testing.T) {
	testApp, _ := tests.NewTestApp()
	defer testApp.Cleanup()

	// failed nested transaction
	testApp.Dao().RunInTransaction(func(txDao *daos.Dao) error {
		admin, _ := txDao.FindAdminByEmail("test@example.com")

		return txDao.RunInTransaction(func(tx2Dao *daos.Dao) error {
			if err := tx2Dao.DeleteAdmin(admin); err != nil {
				t.Fatal(err)
			}
			return errors.New("test error")
		})
	})

	// admin should still exist
	admin1, _ := testApp.Dao().FindAdminByEmail("test@example.com")
	if admin1 == nil {
		t.Fatal("Expected admin test@example.com to not be deleted")
	}

	// successful nested transaction
	testApp.Dao().RunInTransaction(func(txDao *daos.Dao) error {
		admin, _ := txDao.FindAdminByEmail("test@example.com")

		return txDao.RunInTransaction(func(tx2Dao *daos.Dao) error {
			return tx2Dao.DeleteAdmin(admin)
		})
	})

	// admin should have been deleted
	admin2, _ := testApp.Dao().FindAdminByEmail("test@example.com")
	if admin2 != nil {
		t.Fatalf("Expected admin test@example.com to be deleted, found %v", admin2)
	}
}

func TestDaoSaveCreate(t *testing.T) {
	testApp, _ := tests.NewTestApp()
	defer testApp.Cleanup()

	model := &models.Admin{}
	model.Email = "test_new@example.com"
	model.Avatar = 8
	if err := testApp.Dao().Save(model); err != nil {
		t.Fatal(err)
	}

	// refresh
	model, _ = testApp.Dao().FindAdminByEmail("test_new@example.com")

	if model.Avatar != 8 {
		t.Fatalf("Expected model avatar field to be 8, got %v", model.Avatar)
	}

	expectedHooks := []string{"OnModelBeforeCreate", "OnModelAfterCreate"}
	for _, h := range expectedHooks {
		if v, ok := testApp.EventCalls[h]; !ok || v != 1 {
			t.Fatalf("Expected event %s to be called exactly one time, got %d", h, v)
		}
	}
}

func TestDaoSaveWithInsertId(t *testing.T) {
	testApp, _ := tests.NewTestApp()
	defer testApp.Cleanup()

	model := &models.Admin{}
	model.Id = "test"
	model.Email = "test_new@example.com"
	model.MarkAsNew()
	if err := testApp.Dao().Save(model); err != nil {
		t.Fatal(err)
	}

	// refresh
	model, _ = testApp.Dao().FindAdminById("test")

	if model == nil {
		t.Fatal("Failed to find admin with id 'test'")
	}

	expectedHooks := []string{"OnModelBeforeCreate", "OnModelAfterCreate"}
	for _, h := range expectedHooks {
		if v, ok := testApp.EventCalls[h]; !ok || v != 1 {
			t.Fatalf("Expected event %s to be called exactly one time, got %d", h, v)
		}
	}
}

func TestDaoSaveUpdate(t *testing.T) {
	testApp, _ := tests.NewTestApp()
	defer testApp.Cleanup()

	model, _ := testApp.Dao().FindAdminByEmail("test@example.com")

	model.Avatar = 8
	if err := testApp.Dao().Save(model); err != nil {
		t.Fatal(err)
	}

	// refresh
	model, _ = testApp.Dao().FindAdminByEmail("test@example.com")

	if model.Avatar != 8 {
		t.Fatalf("Expected model avatar field to be updated to 8, got %v", model.Avatar)
	}

	expectedHooks := []string{"OnModelBeforeUpdate", "OnModelAfterUpdate"}
	for _, h := range expectedHooks {
		if v, ok := testApp.EventCalls[h]; !ok || v != 1 {
			t.Fatalf("Expected event %s to be called exactly one time, got %d", h, v)
		}
	}
}

type dummyColumnValueMapper struct {
	models.Admin
}

func (a *dummyColumnValueMapper) ColumnValueMap() map[string]any {
	return map[string]any{
		"email":        a.Email,
		"passwordHash": a.PasswordHash,
		"tokenKey":     "custom_token_key",
	}
}

func TestDaoSaveWithColumnValueMapper(t *testing.T) {
	testApp, _ := tests.NewTestApp()
	defer testApp.Cleanup()

	model := &dummyColumnValueMapper{}
	model.Id = "test_mapped_id" // explicitly set an id
	model.Email = "test_mapped_create@example.com"
	model.TokenKey = "test_unmapped_token_key" // not used in the map
	model.SetPassword("123456")
	model.MarkAsNew()
	if err := testApp.Dao().Save(model); err != nil {
		t.Fatal(err)
	}

	createdModel, _ := testApp.Dao().FindAdminById("test_mapped_id")
	if createdModel == nil {
		t.Fatal("[create] Failed to find model with id 'test_mapped_id'")
	}
	if createdModel.Email != model.Email {
		t.Fatalf("Expected model with email %q, got %q", model.Email, createdModel.Email)
	}
	if createdModel.TokenKey != "custom_token_key" {
		t.Fatalf("Expected model with tokenKey %q, got %q", "custom_token_key", createdModel.TokenKey)
	}

	model.Email = "test_mapped_update@example.com"
	model.Avatar = 9 // not mapped and expect to be ignored
	if err := testApp.Dao().Save(model); err != nil {
		t.Fatal(err)
	}

	updatedModel, _ := testApp.Dao().FindAdminById("test_mapped_id")
	if updatedModel == nil {
		t.Fatal("[update] Failed to find model with id 'test_mapped_id'")
	}
	if updatedModel.Email != model.Email {
		t.Fatalf("Expected model with email %q, got %q", model.Email, createdModel.Email)
	}
	if updatedModel.Avatar != 0 {
		t.Fatalf("Expected model avatar 0, got %v", updatedModel.Avatar)
	}
}

func TestDaoDelete(t *testing.T) {
	testApp, _ := tests.NewTestApp()
	defer testApp.Cleanup()

	model, _ := testApp.Dao().FindAdminByEmail("test@example.com")

	if err := testApp.Dao().Delete(model); err != nil {
		t.Fatal(err)
	}

	model, _ = testApp.Dao().FindAdminByEmail("test@example.com")
	if model != nil {
		t.Fatalf("Expected model to be deleted, found %v", model)
	}

	expectedHooks := []string{"OnModelBeforeDelete", "OnModelAfterDelete"}
	for _, h := range expectedHooks {
		if v, ok := testApp.EventCalls[h]; !ok || v != 1 {
			t.Fatalf("Expected event %s to be called exactly one time, got %d", h, v)
		}
	}
}

func TestDaoRetryCreate(t *testing.T) {
	testApp, _ := tests.NewTestApp()
	defer testApp.Cleanup()

	// init mock retry dao
	retryBeforeCreateHookCalls := 0
	retryAfterCreateHookCalls := 0
	retryDao := daos.New(testApp.DB())
	retryDao.BeforeCreateFunc = func(eventDao *daos.Dao, m models.Model) error {
		retryBeforeCreateHookCalls++
		return errors.New("database is locked")
	}
	retryDao.AfterCreateFunc = func(eventDao *daos.Dao, m models.Model) {
		retryAfterCreateHookCalls++
	}

	model := &models.Admin{Email: "new@example.com"}
	if err := retryDao.Save(model); err != nil {
		t.Fatalf("Expected nil after retry, got error: %v", err)
	}

	// the before hook is expected to be called only once because
	// it is ignored after the first "database is locked" error
	if retryBeforeCreateHookCalls != 1 {
		t.Fatalf("Expected before hook calls to be 1, got %d", retryBeforeCreateHookCalls)
	}

	if retryAfterCreateHookCalls != 1 {
		t.Fatalf("Expected after hook calls to be 1, got %d", retryAfterCreateHookCalls)
	}

	// with non-locking error
	retryBeforeCreateHookCalls = 0
	retryAfterCreateHookCalls = 0
	retryDao.BeforeCreateFunc = func(eventDao *daos.Dao, m models.Model) error {
		retryBeforeCreateHookCalls++
		return errors.New("non-locking error")
	}

	dummy := &models.Admin{Email: "test@example.com"}
	if err := retryDao.Save(dummy); err == nil {
		t.Fatal("Expected error, got nil")
	}

	if retryBeforeCreateHookCalls != 1 {
		t.Fatalf("Expected before hook calls to be 1, got %d", retryBeforeCreateHookCalls)
	}

	if retryAfterCreateHookCalls != 0 {
		t.Fatalf("Expected after hook calls to be 0, got %d", retryAfterCreateHookCalls)
	}
}

func TestDaoRetryUpdate(t *testing.T) {
	testApp, _ := tests.NewTestApp()
	defer testApp.Cleanup()

	model, err := testApp.Dao().FindAdminByEmail("test@example.com")
	if err != nil {
		t.Fatal(err)
	}

	// init mock retry dao
	retryBeforeUpdateHookCalls := 0
	retryAfterUpdateHookCalls := 0
	retryDao := daos.New(testApp.DB())
	retryDao.BeforeUpdateFunc = func(eventDao *daos.Dao, m models.Model) error {
		retryBeforeUpdateHookCalls++
		return errors.New("database is locked")
	}
	retryDao.AfterUpdateFunc = func(eventDao *daos.Dao, m models.Model) {
		retryAfterUpdateHookCalls++
	}

	if err := retryDao.Save(model); err != nil {
		t.Fatalf("Expected nil after retry, got error: %v", err)
	}

	// the before hook is expected to be called only once because
	// it is ignored after the first "database is locked" error
	if retryBeforeUpdateHookCalls != 1 {
		t.Fatalf("Expected before hook calls to be 1, got %d", retryBeforeUpdateHookCalls)
	}

	if retryAfterUpdateHookCalls != 1 {
		t.Fatalf("Expected after hook calls to be 1, got %d", retryAfterUpdateHookCalls)
	}

	// with non-locking error
	retryBeforeUpdateHookCalls = 0
	retryAfterUpdateHookCalls = 0
	retryDao.BeforeUpdateFunc = func(eventDao *daos.Dao, m models.Model) error {
		retryBeforeUpdateHookCalls++
		return errors.New("non-locking error")
	}

	if err := retryDao.Save(model); err == nil {
		t.Fatal("Expected error, got nil")
	}

	if retryBeforeUpdateHookCalls != 1 {
		t.Fatalf("Expected before hook calls to be 1, got %d", retryBeforeUpdateHookCalls)
	}

	if retryAfterUpdateHookCalls != 0 {
		t.Fatalf("Expected after hook calls to be 0, got %d", retryAfterUpdateHookCalls)
	}
}

func TestDaoRetryDelete(t *testing.T) {
	testApp, _ := tests.NewTestApp()
	defer testApp.Cleanup()

	// init mock retry dao
	retryBeforeDeleteHookCalls := 0
	retryAfterDeleteHookCalls := 0
	retryDao := daos.New(testApp.DB())
	retryDao.BeforeDeleteFunc = func(eventDao *daos.Dao, m models.Model) error {
		retryBeforeDeleteHookCalls++
		return errors.New("database is locked")
	}
	retryDao.AfterDeleteFunc = func(eventDao *daos.Dao, m models.Model) {
		retryAfterDeleteHookCalls++
	}

	model, _ := retryDao.FindAdminByEmail("test@example.com")
	if err := retryDao.Delete(model); err != nil {
		t.Fatalf("Expected nil after retry, got error: %v", err)
	}

	// the before hook is expected to be called only once because
	// it is ignored after the first "database is locked" error
	if retryBeforeDeleteHookCalls != 1 {
		t.Fatalf("Expected before hook calls to be 1, got %d", retryBeforeDeleteHookCalls)
	}

	if retryAfterDeleteHookCalls != 1 {
		t.Fatalf("Expected after hook calls to be 1, got %d", retryAfterDeleteHookCalls)
	}

	// with non-locking error
	retryBeforeDeleteHookCalls = 0
	retryAfterDeleteHookCalls = 0
	retryDao.BeforeDeleteFunc = func(eventDao *daos.Dao, m models.Model) error {
		retryBeforeDeleteHookCalls++
		return errors.New("non-locking error")
	}

	dummy := &models.Admin{}
	dummy.RefreshId()
	dummy.MarkAsNotNew()
	if err := retryDao.Delete(dummy); err == nil {
		t.Fatal("Expected error, got nil")
	}

	if retryBeforeDeleteHookCalls != 1 {
		t.Fatalf("Expected before hook calls to be 1, got %d", retryBeforeDeleteHookCalls)
	}

	if retryAfterDeleteHookCalls != 0 {
		t.Fatalf("Expected after hook calls to be 0, got %d", retryAfterDeleteHookCalls)
	}
}

func TestDaoBeforeHooksError(t *testing.T) {
	testApp, _ := tests.NewTestApp()
	defer testApp.Cleanup()

	baseDao := testApp.Dao()

	baseDao.BeforeCreateFunc = func(eventDao *daos.Dao, m models.Model) error {
		return errors.New("before_create")
	}
	baseDao.BeforeUpdateFunc = func(eventDao *daos.Dao, m models.Model) error {
		return errors.New("before_update")
	}
	baseDao.BeforeDeleteFunc = func(eventDao *daos.Dao, m models.Model) error {
		return errors.New("before_delete")
	}

	existingModel, _ := testApp.Dao().FindAdminByEmail("test@example.com")

	// test create error
	// ---
	newModel := &models.Admin{}
	if err := baseDao.Save(newModel); err.Error() != "before_create" {
		t.Fatalf("Expected before_create error, got %v", err)
	}

	// test update error
	// ---
	if err := baseDao.Save(existingModel); err.Error() != "before_update" {
		t.Fatalf("Expected before_update error, got %v", err)
	}

	// test delete error
	// ---
	if err := baseDao.Delete(existingModel); err.Error() != "before_delete" {
		t.Fatalf("Expected before_delete error, got %v", err)
	}
}

func TestDaoTransactionHooksCallsOnFailure(t *testing.T) {
	testApp, _ := tests.NewTestApp()
	defer testApp.Cleanup()

	beforeCreateFuncCalls := 0
	beforeUpdateFuncCalls := 0
	beforeDeleteFuncCalls := 0
	afterCreateFuncCalls := 0
	afterUpdateFuncCalls := 0
	afterDeleteFuncCalls := 0

	baseDao := testApp.Dao()

	baseDao.BeforeCreateFunc = func(eventDao *daos.Dao, m models.Model) error {
		beforeCreateFuncCalls++
		return nil
	}
	baseDao.BeforeUpdateFunc = func(eventDao *daos.Dao, m models.Model) error {
		beforeUpdateFuncCalls++
		return nil
	}
	baseDao.BeforeDeleteFunc = func(eventDao *daos.Dao, m models.Model) error {
		beforeDeleteFuncCalls++
		return nil
	}

	baseDao.AfterCreateFunc = func(eventDao *daos.Dao, m models.Model) {
		afterCreateFuncCalls++
	}
	baseDao.AfterUpdateFunc = func(eventDao *daos.Dao, m models.Model) {
		afterUpdateFuncCalls++
	}
	baseDao.AfterDeleteFunc = func(eventDao *daos.Dao, m models.Model) {
		afterDeleteFuncCalls++
	}

	existingModel, _ := testApp.Dao().FindAdminByEmail("test@example.com")

	baseDao.RunInTransaction(func(txDao1 *daos.Dao) error {
		return txDao1.RunInTransaction(func(txDao2 *daos.Dao) error {
			// test create
			// ---
			newModel := &models.Admin{}
			newModel.Email = "test_new1@example.com"
			newModel.SetPassword("123456")
			if err := txDao2.Save(newModel); err != nil {
				t.Fatal(err)
			}

			// test update (twice)
			// ---
			if err := txDao2.Save(existingModel); err != nil {
				t.Fatal(err)
			}
			if err := txDao2.Save(existingModel); err != nil {
				t.Fatal(err)
			}

			// test delete
			// ---
			if err := txDao2.Delete(existingModel); err != nil {
				t.Fatal(err)
			}

			return errors.New("test_tx_error")
		})
	})

	if beforeCreateFuncCalls != 1 {
		t.Fatalf("Expected beforeCreateFuncCalls to be called 1 times, got %d", beforeCreateFuncCalls)
	}
	if beforeUpdateFuncCalls != 2 {
		t.Fatalf("Expected beforeUpdateFuncCalls to be called 2 times, got %d", beforeUpdateFuncCalls)
	}
	if beforeDeleteFuncCalls != 1 {
		t.Fatalf("Expected beforeDeleteFuncCalls to be called 1 times, got %d", beforeDeleteFuncCalls)
	}
	if afterCreateFuncCalls != 0 {
		t.Fatalf("Expected afterCreateFuncCalls to be called 0 times, got %d", afterCreateFuncCalls)
	}
	if afterUpdateFuncCalls != 0 {
		t.Fatalf("Expected afterUpdateFuncCalls to be called 0 times, got %d", afterUpdateFuncCalls)
	}
	if afterDeleteFuncCalls != 0 {
		t.Fatalf("Expected afterDeleteFuncCalls to be called 0 times, got %d", afterDeleteFuncCalls)
	}
}

func TestDaoTransactionHooksCallsOnSuccess(t *testing.T) {
	testApp, _ := tests.NewTestApp()
	defer testApp.Cleanup()

	beforeCreateFuncCalls := 0
	beforeUpdateFuncCalls := 0
	beforeDeleteFuncCalls := 0
	afterCreateFuncCalls := 0
	afterUpdateFuncCalls := 0
	afterDeleteFuncCalls := 0

	baseDao := testApp.Dao()

	baseDao.BeforeCreateFunc = func(eventDao *daos.Dao, m models.Model) error {
		beforeCreateFuncCalls++
		return nil
	}
	baseDao.BeforeUpdateFunc = func(eventDao *daos.Dao, m models.Model) error {
		beforeUpdateFuncCalls++
		return nil
	}
	baseDao.BeforeDeleteFunc = func(eventDao *daos.Dao, m models.Model) error {
		beforeDeleteFuncCalls++
		return nil
	}

	baseDao.AfterCreateFunc = func(eventDao *daos.Dao, m models.Model) {
		afterCreateFuncCalls++
	}
	baseDao.AfterUpdateFunc = func(eventDao *daos.Dao, m models.Model) {
		afterUpdateFuncCalls++
	}
	baseDao.AfterDeleteFunc = func(eventDao *daos.Dao, m models.Model) {
		afterDeleteFuncCalls++
	}

	existingModel, _ := testApp.Dao().FindAdminByEmail("test@example.com")

	baseDao.RunInTransaction(func(txDao1 *daos.Dao) error {
		return txDao1.RunInTransaction(func(txDao2 *daos.Dao) error {
			// test create
			// ---
			newModel := &models.Admin{}
			newModel.Email = "test_new1@example.com"
			newModel.SetPassword("123456")
			if err := txDao2.Save(newModel); err != nil {
				t.Fatal(err)
			}

			// test update (twice)
			// ---
			if err := txDao2.Save(existingModel); err != nil {
				t.Fatal(err)
			}
			if err := txDao2.Save(existingModel); err != nil {
				t.Fatal(err)
			}

			// test delete
			// ---
			if err := txDao2.Delete(existingModel); err != nil {
				t.Fatal(err)
			}

			return nil
		})
	})

	if beforeCreateFuncCalls != 1 {
		t.Fatalf("Expected beforeCreateFuncCalls to be called 1 times, got %d", beforeCreateFuncCalls)
	}
	if beforeUpdateFuncCalls != 2 {
		t.Fatalf("Expected beforeUpdateFuncCalls to be called 2 times, got %d", beforeUpdateFuncCalls)
	}
	if beforeDeleteFuncCalls != 1 {
		t.Fatalf("Expected beforeDeleteFuncCalls to be called 1 times, got %d", beforeDeleteFuncCalls)
	}
	if afterCreateFuncCalls != 1 {
		t.Fatalf("Expected afterCreateFuncCalls to be called 1 times, got %d", afterCreateFuncCalls)
	}
	if afterUpdateFuncCalls != 2 {
		t.Fatalf("Expected afterUpdateFuncCalls to be called 2 times, got %d", afterUpdateFuncCalls)
	}
	if afterDeleteFuncCalls != 1 {
		t.Fatalf("Expected afterDeleteFuncCalls to be called 1 times, got %d", afterDeleteFuncCalls)
	}
}
