package core_test

import (
	"context"
	"database/sql"
	"log/slog"
	"os"
	"testing"
	"time"

	_ "unsafe"

	"github.com/pocketbase/dbx"
	"github.com/pocketbase/pocketbase/core"
	"github.com/pocketbase/pocketbase/tests"
	"github.com/pocketbase/pocketbase/tools/logger"
	"github.com/pocketbase/pocketbase/tools/mailer"
)

func TestNewBaseApp(t *testing.T) {
	const testDataDir = "./pb_base_app_test_data_dir/"
	defer os.RemoveAll(testDataDir)

	app := core.NewBaseApp(core.BaseAppConfig{
		DataDir:       testDataDir,
		EncryptionEnv: "test_env",
		IsDev:         true,
	})

	if app.DataDir() != testDataDir {
		t.Fatalf("expected DataDir %q, got %q", testDataDir, app.DataDir())
	}

	if app.EncryptionEnv() != "test_env" {
		t.Fatalf("expected EncryptionEnv test_env, got %q", app.EncryptionEnv())
	}

	if !app.IsDev() {
		t.Fatalf("expected IsDev true, got %v", app.IsDev())
	}

	if app.Store() == nil {
		t.Fatal("expected Store to be set, got nil")
	}

	if app.Settings() == nil {
		t.Fatal("expected Settings to be set, got nil")
	}

	if app.SubscriptionsBroker() == nil {
		t.Fatal("expected SubscriptionsBroker to be set, got nil")
	}

	if app.Cron() == nil {
		t.Fatal("expected Cron to be set, got nil")
	}
}

func TestBaseAppBootstrap(t *testing.T) {
	const testDataDir = "./pb_base_app_test_data_dir/"
	defer os.RemoveAll(testDataDir)

	app := core.NewBaseApp(core.BaseAppConfig{
		DataDir: testDataDir,
	})
	defer app.ResetBootstrapState()

	if app.IsBootstrapped() {
		t.Fatal("Didn't expect the application to be bootstrapped.")
	}

	if err := app.Bootstrap(); err != nil {
		t.Fatal(err)
	}

	if !app.IsBootstrapped() {
		t.Fatal("Expected the application to be bootstrapped.")
	}

	if stat, err := os.Stat(testDataDir); err != nil || !stat.IsDir() {
		t.Fatal("Expected test data directory to be created.")
	}

	type nilCheck struct {
		name      string
		value     any
		expectNil bool
	}

	runNilChecks := func(checks []nilCheck) {
		for _, check := range checks {
			t.Run(check.name, func(t *testing.T) {
				isNil := check.value == nil
				if isNil != check.expectNil {
					t.Fatalf("Expected isNil %v, got %v", check.expectNil, isNil)
				}
			})
		}
	}

	nilChecksBeforeReset := []nilCheck{
		{"[before] concurrentDB", app.DB(), false},
		{"[before] nonconcurrentDB", app.NonconcurrentDB(), false},
		{"[before] auxConcurrentDB", app.AuxDB(), false},
		{"[before] auxNonconcurrentDB", app.AuxNonconcurrentDB(), false},
		{"[before] settings", app.Settings(), false},
		{"[before] logger", app.Logger(), false},
		{"[before] cached collections", app.Store().Get(core.StoreKeyCachedCollections), false},
	}

	runNilChecks(nilChecksBeforeReset)

	// reset
	if err := app.ResetBootstrapState(); err != nil {
		t.Fatal(err)
	}

	nilChecksAfterReset := []nilCheck{
		{"[after] concurrentDB", app.DB(), true},
		{"[after] nonconcurrentDB", app.NonconcurrentDB(), true},
		{"[after] auxConcurrentDB", app.AuxDB(), true},
		{"[after] auxNonconcurrentDB", app.AuxNonconcurrentDB(), true},
		{"[after] settings", app.Settings(), false},
		{"[after] logger", app.Logger(), false},
		{"[after] cached collections", app.Store().Get(core.StoreKeyCachedCollections), false},
	}

	runNilChecks(nilChecksAfterReset)
}

func TestNewBaseAppIsTransactional(t *testing.T) {
	const testDataDir = "./pb_base_app_test_data_dir/"
	defer os.RemoveAll(testDataDir)

	app := core.NewBaseApp(core.BaseAppConfig{
		DataDir: testDataDir,
	})
	defer app.ResetBootstrapState()

	if err := app.Bootstrap(); err != nil {
		t.Fatal(err)
	}

	if app.IsTransactional() {
		t.Fatalf("Didn't expect the app to be transactional")
	}

	app.RunInTransaction(func(txApp core.App) error {
		if !txApp.IsTransactional() {
			t.Fatalf("Expected the app to be transactional")
		}

		return nil
	})
}

func TestBaseAppNewMailClient(t *testing.T) {
	const testDataDir = "./pb_base_app_test_data_dir/"
	defer os.RemoveAll(testDataDir)

	app := core.NewBaseApp(core.BaseAppConfig{
		DataDir:       testDataDir,
		EncryptionEnv: "pb_test_env",
	})
	defer app.ResetBootstrapState()

	client1 := app.NewMailClient()
	m1, ok := client1.(*mailer.Sendmail)
	if !ok {
		t.Fatalf("Expected mailer.Sendmail instance, got %v", m1)
	}
	if m1.OnSend() == nil || m1.OnSend().Length() == 0 {
		t.Fatal("Expected OnSend hook to be registered")
	}

	app.Settings().SMTP.Enabled = true

	client2 := app.NewMailClient()
	m2, ok := client2.(*mailer.SMTPClient)
	if !ok {
		t.Fatalf("Expected mailer.SMTPClient instance, got %v", m2)
	}
	if m2.OnSend() == nil || m2.OnSend().Length() == 0 {
		t.Fatal("Expected OnSend hook to be registered")
	}
}

func TestBaseAppNewFilesystem(t *testing.T) {
	const testDataDir = "./pb_base_app_test_data_dir/"
	defer os.RemoveAll(testDataDir)

	app := core.NewBaseApp(core.BaseAppConfig{
		DataDir: testDataDir,
	})
	defer app.ResetBootstrapState()

	// local
	local, localErr := app.NewFilesystem()
	if localErr != nil {
		t.Fatal(localErr)
	}
	if local == nil {
		t.Fatal("Expected local filesystem instance, got nil")
	}

	// misconfigured s3
	app.Settings().S3.Enabled = true
	s3, s3Err := app.NewFilesystem()
	if s3Err == nil {
		t.Fatal("Expected S3 error, got nil")
	}
	if s3 != nil {
		t.Fatalf("Expected nil s3 filesystem, got %v", s3)
	}
}

func TestBaseAppNewBackupsFilesystem(t *testing.T) {
	const testDataDir = "./pb_base_app_test_data_dir/"
	defer os.RemoveAll(testDataDir)

	app := core.NewBaseApp(core.BaseAppConfig{
		DataDir: testDataDir,
	})
	defer app.ResetBootstrapState()

	// local
	local, localErr := app.NewBackupsFilesystem()
	if localErr != nil {
		t.Fatal(localErr)
	}
	if local == nil {
		t.Fatal("Expected local backups filesystem instance, got nil")
	}

	// misconfigured s3
	app.Settings().Backups.S3.Enabled = true
	s3, s3Err := app.NewBackupsFilesystem()
	if s3Err == nil {
		t.Fatal("Expected S3 error, got nil")
	}
	if s3 != nil {
		t.Fatalf("Expected nil s3 backups filesystem, got %v", s3)
	}
}

func TestBaseAppLoggerWrites(t *testing.T) {
	t.Parallel()

	app, _ := tests.NewTestApp()
	defer app.Cleanup()

	// reset
	if err := app.DeleteOldLogs(time.Now()); err != nil {
		t.Fatal(err)
	}

	const logsThreshold = 200

	totalLogs := func(app core.App, t *testing.T) int {
		var total int

		err := app.LogQuery().Select("count(*)").Row(&total)
		if err != nil {
			t.Fatalf("Failed to fetch total logs: %v", err)
		}

		return total
	}

	t.Run("disabled logs retention", func(t *testing.T) {
		app.Settings().Logs.MaxDays = 0

		for i := 0; i < logsThreshold+1; i++ {
			app.Logger().Error("test")
		}

		if total := totalLogs(app, t); total != 0 {
			t.Fatalf("Expected no logs, got %d", total)
		}
	})

	t.Run("test batch logs writes", func(t *testing.T) {
		app.Settings().Logs.MaxDays = 1

		for i := 0; i < logsThreshold-1; i++ {
			app.Logger().Error("test")
		}

		if total := totalLogs(app, t); total != 0 {
			t.Fatalf("Expected no logs, got %d", total)
		}

		// should trigger batch write
		app.Logger().Error("test")

		// should be added for the next batch write
		app.Logger().Error("test")

		if total := totalLogs(app, t); total != logsThreshold {
			t.Fatalf("Expected %d logs, got %d", logsThreshold, total)
		}

		// wait for ~3 secs to check the timer trigger
		time.Sleep(3200 * time.Millisecond)
		if total := totalLogs(app, t); total != logsThreshold+1 {
			t.Fatalf("Expected %d logs, got %d", logsThreshold+1, total)
		}
	})
}

func TestBaseAppRefreshSettingsLoggerMinLevelEnabled(t *testing.T) {
	scenarios := []struct {
		name  string
		isDev bool
		level int
		// level->enabled map
		expectations map[int]bool
	}{
		{
			"dev mode",
			true,
			4,
			map[int]bool{
				3: true,
				4: true,
				5: true,
			},
		},
		{
			"nondev mode",
			false,
			4,
			map[int]bool{
				3: false,
				4: true,
				5: true,
			},
		},
	}

	for _, s := range scenarios {
		t.Run(s.name, func(t *testing.T) {
			const testDataDir = "./pb_base_app_test_data_dir/"
			defer os.RemoveAll(testDataDir)

			app := core.NewBaseApp(core.BaseAppConfig{
				DataDir: testDataDir,
				IsDev:   s.isDev,
			})
			defer app.ResetBootstrapState()

			if err := app.Bootstrap(); err != nil {
				t.Fatal(err)
			}

			// silence query logs
			app.DB().(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) {}
			app.DB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {}
			app.NonconcurrentDB().(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) {}
			app.NonconcurrentDB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {}

			handler, ok := app.Logger().Handler().(*logger.BatchHandler)
			if !ok {
				t.Fatalf("Expected BatchHandler, got %v", app.Logger().Handler())
			}

			app.Settings().Logs.MinLevel = s.level

			if err := app.Save(app.Settings()); err != nil {
				t.Fatalf("Failed to save settings: %v", err)
			}

			for level, enabled := range s.expectations {
				if v := handler.Enabled(context.Background(), slog.Level(level)); v != enabled {
					t.Fatalf("Expected level %d Enabled() to be %v, got %v", level, enabled, v)
				}
			}
		})
	}
}