package core_test

import (
	"fmt"
	"testing"
	"time"

	"github.com/pocketbase/pocketbase/core"
	"github.com/pocketbase/pocketbase/tests"
	"github.com/pocketbase/pocketbase/tools/types"
)

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

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

	otp := core.NewOTP(app)

	if otp.Collection().Name != core.CollectionNameOTPs {
		t.Fatalf("Expected record with %q collection, got %q", core.CollectionNameOTPs, otp.Collection().Name)
	}
}

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

	record := core.NewRecord(core.NewBaseCollection("test"))
	record.Id = "test_id"

	otp := core.OTP{}
	otp.SetProxyRecord(record)

	if otp.ProxyRecord() == nil || otp.ProxyRecord().Id != record.Id {
		t.Fatalf("Expected proxy record with id %q, got %v", record.Id, otp.ProxyRecord())
	}
}

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

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

	otp := core.NewOTP(app)

	testValues := []string{"test_1", "test2", ""}
	for i, testValue := range testValues {
		t.Run(fmt.Sprintf("%d_%q", i, testValue), func(t *testing.T) {
			otp.SetRecordRef(testValue)

			if v := otp.RecordRef(); v != testValue {
				t.Fatalf("Expected getter %q, got %q", testValue, v)
			}

			if v := otp.GetString("recordRef"); v != testValue {
				t.Fatalf("Expected field value %q, got %q", testValue, v)
			}
		})
	}
}

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

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

	otp := core.NewOTP(app)

	testValues := []string{"test_1", "test2", ""}
	for i, testValue := range testValues {
		t.Run(fmt.Sprintf("%d_%q", i, testValue), func(t *testing.T) {
			otp.SetCollectionRef(testValue)

			if v := otp.CollectionRef(); v != testValue {
				t.Fatalf("Expected getter %q, got %q", testValue, v)
			}

			if v := otp.GetString("collectionRef"); v != testValue {
				t.Fatalf("Expected field value %q, got %q", testValue, v)
			}
		})
	}
}

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

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

	otp := core.NewOTP(app)

	testValues := []string{"test_1", "test2", ""}
	for i, testValue := range testValues {
		t.Run(fmt.Sprintf("%d_%q", i, testValue), func(t *testing.T) {
			otp.SetSentTo(testValue)

			if v := otp.SentTo(); v != testValue {
				t.Fatalf("Expected getter %q, got %q", testValue, v)
			}

			if v := otp.GetString("sentTo"); v != testValue {
				t.Fatalf("Expected field value %q, got %q", testValue, v)
			}
		})
	}
}

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

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

	otp := core.NewOTP(app)

	if v := otp.Created().String(); v != "" {
		t.Fatalf("Expected empty created, got %q", v)
	}

	now := types.NowDateTime()
	otp.SetRaw("created", now)

	if v := otp.Created().String(); v != now.String() {
		t.Fatalf("Expected %q created, got %q", now.String(), v)
	}
}

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

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

	otp := core.NewOTP(app)

	if v := otp.Updated().String(); v != "" {
		t.Fatalf("Expected empty updated, got %q", v)
	}

	now := types.NowDateTime()
	otp.SetRaw("updated", now)

	if v := otp.Updated().String(); v != now.String() {
		t.Fatalf("Expected %q updated, got %q", now.String(), v)
	}
}

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

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

	now := types.NowDateTime()

	otp := core.NewOTP(app)
	otp.SetRaw("created", now.Add(-5*time.Minute))

	scenarios := []struct {
		maxElapsed time.Duration
		expected   bool
	}{
		{0 * time.Minute, true},
		{3 * time.Minute, true},
		{5 * time.Minute, true},
		{6 * time.Minute, false},
	}

	for i, s := range scenarios {
		t.Run(fmt.Sprintf("%d_%s", i, s.maxElapsed.String()), func(t *testing.T) {
			result := otp.HasExpired(s.maxElapsed)

			if result != s.expected {
				t.Fatalf("Expected %v, got %v", s.expected, result)
			}
		})
	}
}

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

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

	otpsCol, err := app.FindCollectionByNameOrId(core.CollectionNameOTPs)
	if err != nil {
		t.Fatal(err)
	}

	user, err := app.FindAuthRecordByEmail("users", "test@example.com")
	if err != nil {
		t.Fatal(err)
	}

	t.Run("no proxy record", func(t *testing.T) {
		otp := &core.OTP{}

		if err := app.Validate(otp); err == nil {
			t.Fatal("Expected collection validation error")
		}
	})

	t.Run("non-OTP collection", func(t *testing.T) {
		otp := &core.OTP{}
		otp.SetProxyRecord(core.NewRecord(core.NewBaseCollection("invalid")))
		otp.SetRecordRef(user.Id)
		otp.SetCollectionRef(user.Collection().Id)
		otp.SetPassword("test123")

		if err := app.Validate(otp); err == nil {
			t.Fatal("Expected collection validation error")
		}
	})

	t.Run("OTP collection", func(t *testing.T) {
		otp := &core.OTP{}
		otp.SetProxyRecord(core.NewRecord(otpsCol))
		otp.SetRecordRef(user.Id)
		otp.SetCollectionRef(user.Collection().Id)
		otp.SetPassword("test123")

		if err := app.Validate(otp); err != nil {
			t.Fatalf("Expected nil validation error, got %v", err)
		}
	})
}

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

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

	user, err := app.FindAuthRecordByEmail("users", "test@example.com")
	if err != nil {
		t.Fatal(err)
	}

	demo1, err := app.FindRecordById("demo1", "84nmscqy84lsi1t")
	if err != nil {
		t.Fatal(err)
	}

	scenarios := []struct {
		name         string
		otp          func() *core.OTP
		expectErrors []string
	}{
		{
			"empty",
			func() *core.OTP {
				return core.NewOTP(app)
			},
			[]string{"collectionRef", "recordRef", "password"},
		},
		{
			"non-auth collection",
			func() *core.OTP {
				otp := core.NewOTP(app)
				otp.SetCollectionRef(demo1.Collection().Id)
				otp.SetRecordRef(demo1.Id)
				otp.SetPassword("test123")
				return otp
			},
			[]string{"collectionRef"},
		},
		{
			"missing record id",
			func() *core.OTP {
				otp := core.NewOTP(app)
				otp.SetCollectionRef(user.Collection().Id)
				otp.SetRecordRef("missing")
				otp.SetPassword("test123")
				return otp
			},
			[]string{"recordRef"},
		},
		{
			"valid ref",
			func() *core.OTP {
				otp := core.NewOTP(app)
				otp.SetCollectionRef(user.Collection().Id)
				otp.SetRecordRef(user.Id)
				otp.SetPassword("test123")
				return otp
			},
			[]string{},
		},
	}

	for _, s := range scenarios {
		t.Run(s.name, func(t *testing.T) {
			errs := app.Validate(s.otp())
			tests.TestValidationErrors(t, errs, s.expectErrors)
		})
	}
}

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

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

	user1, err := app.FindAuthRecordByEmail("users", "test@example.com")
	if err != nil {
		t.Fatal(err)
	}

	user2, err := app.FindAuthRecordByEmail("users", "test2@example.com")
	if err != nil {
		t.Fatal(err)
	}

	otpsToCreate := map[*core.Record]int{
		user1: 3,
		user2: 2,
	}
	for user, total := range otpsToCreate {
		for range total {
			otp := core.NewOTP(app)
			otp.SetCollectionRef(user.Collection().Id)
			otp.SetRecordRef(user.Id)
			otp.SetPassword("123456")
			if err := app.Save(otp); err != nil {
				t.Fatal(err)
			}
		}
	}

	// update both users
	err = app.Save(user1)
	if err != nil {
		t.Fatal(err)
	}

	user2.RefreshTokenKey()
	err = app.Save(user2)
	if err != nil {
		t.Fatal(err)
	}

	expectedOTPs := map[*core.Record]int{
		user1: 3,
		user2: 0,
	}

	for user, expected := range expectedOTPs {
		otps, err := app.FindAllOTPsByRecord(user)
		if err != nil {
			t.Fatal(err)
		}

		if len(otps) != expected {
			t.Fatalf("Expected %d OTPs, got %d", expected, len(otps))
		}
	}
}
