package core_test

import (
	"bytes"
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"regexp"
	"slices"
	"strings"
	"testing"
	"time"

	"github.com/pocketbase/dbx"
	"github.com/pocketbase/pocketbase/core"
	"github.com/pocketbase/pocketbase/tests"
	"github.com/pocketbase/pocketbase/tools/filesystem"
	"github.com/pocketbase/pocketbase/tools/types"
	"github.com/spf13/cast"
)

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

	collection := core.NewBaseCollection("test")
	collection.Fields.Add(&core.BoolField{Name: "status"})

	m := core.NewRecord(collection)

	rawData, err := json.Marshal(m.FieldsData()) // should be initialized with the defaults
	if err != nil {
		t.Fatal(err)
	}

	expected := `{"id":"","status":false}`

	if str := string(rawData); str != expected {
		t.Fatalf("Expected schema data\n%v\ngot\n%v", expected, str)
	}
}

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

	collection := core.NewBaseCollection("test")

	m := core.NewRecord(collection)

	if m.Collection().Name != collection.Name {
		t.Fatalf("Expected collection with name %q, got %q", collection.Name, m.Collection().Name)
	}
}

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

	collection := core.NewBaseCollection("test")

	m := core.NewRecord(collection)

	if m.TableName() != collection.Name {
		t.Fatalf("Expected table %q, got %q", collection.Name, m.TableName())
	}
}

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

	collection := core.NewBaseCollection("test_collection")
	collection.Fields.Add(&core.TextField{Name: "test"})

	m := core.NewRecord(collection)

	// calling PostScan without id
	err := m.PostScan()
	if err == nil {
		t.Fatal("Expected PostScan id error, got nil")
	}

	m.Id = "test_id"
	m.Set("test", "abc")

	if v := m.IsNew(); v != true {
		t.Fatalf("[before PostScan] Expected IsNew %v, got %v", true, v)
	}
	if v := m.Original().PK(); v != "" {
		t.Fatalf("[before PostScan] Expected the original PK to be empty string, got %v", v)
	}
	if v := m.Original().Get("test"); v != "" {
		t.Fatalf("[before PostScan] Expected the original 'test' field to be empty string, got %v", v)
	}

	err = m.PostScan()
	if err != nil {
		t.Fatalf("Expected PostScan nil error, got %v", err)
	}

	if v := m.IsNew(); v != false {
		t.Fatalf("[after PostScan] Expected IsNew %v, got %v", false, v)
	}
	if v := m.Original().PK(); v != "test_id" {
		t.Fatalf("[after PostScan] Expected the original PK to be %q, got %v", "test_id", v)
	}
	if v := m.Original().Get("test"); v != "abc" {
		t.Fatalf("[after PostScan] Expected the original 'test' field to be %q, got %v", "abc", v)
	}
}

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

	collection := core.NewBaseCollection("test")

	m := core.NewRecord(collection)

	tags := m.HookTags()

	expectedTags := []string{collection.Id, collection.Name}

	if len(tags) != len(expectedTags) {
		t.Fatalf("Expected tags\n%v\ngot\n%v", expectedTags, tags)
	}

	for _, tag := range tags {
		if !slices.Contains(expectedTags, tag) {
			t.Errorf("Missing expected tag %q", tag)
		}
	}
}

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

	collection := core.NewBaseCollection("test")

	m := core.NewRecord(collection)
	m.Id = "abc"

	result := m.BaseFilesPath()
	expected := collection.BaseFilesPath() + "/" + m.Id
	if result != expected {
		t.Fatalf("Expected %q, got %q", expected, result)
	}
}

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

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

	record, err := app.FindAuthRecordByEmail("users", "test@example.com")
	if err != nil {
		t.Fatal(err)
	}
	originalId := record.Id
	originalName := record.GetString("name")

	extraFieldsCheck := []string{`"email":`, `"custom":`}

	// change the fields
	record.Id = "changed"
	record.Set("name", "name_new")
	record.Set("custom", "test_custom")
	record.SetExpand(map[string]any{"test": 123})
	record.IgnoreEmailVisibility(true)
	record.IgnoreUnchangedFields(true)
	record.WithCustomData(true)
	record.Unhide(record.Collection().Fields.FieldNames()...)

	// ensure that the email visibility and the custom data toggles are active
	raw, err := record.MarshalJSON()
	if err != nil {
		t.Fatal(err)
	}
	rawStr := string(raw)
	for _, f := range extraFieldsCheck {
		if !strings.Contains(rawStr, f) {
			t.Fatalf("Expected %s in\n%s", f, rawStr)
		}
	}

	// check changes
	if v := record.GetString("name"); v != "name_new" {
		t.Fatalf("Expected name to be %q, got %q", "name_new", v)
	}
	if v := record.GetString("custom"); v != "test_custom" {
		t.Fatalf("Expected custom to be %q, got %q", "test_custom", v)
	}

	// check original
	if v := record.Original().PK(); v != originalId {
		t.Fatalf("Expected the original PK to be %q, got %q", originalId, v)
	}
	if v := record.Original().Id; v != originalId {
		t.Fatalf("Expected the original id to be %q, got %q", originalId, v)
	}
	if v := record.Original().GetString("name"); v != originalName {
		t.Fatalf("Expected the original name to be %q, got %q", originalName, v)
	}
	if v := record.Original().GetString("custom"); v != "" {
		t.Fatalf("Expected the original custom to be %q, got %q", "", v)
	}
	if v := record.Original().Expand(); len(v) != 0 {
		t.Fatalf("Expected empty original expand, got\n%v", v)
	}

	// ensure that the email visibility and the custom flag toggles weren't copied
	originalRaw, err := record.Original().MarshalJSON()
	if err != nil {
		t.Fatal(err)
	}
	originalRawStr := string(originalRaw)
	for _, f := range extraFieldsCheck {
		if strings.Contains(originalRawStr, f) {
			t.Fatalf("Didn't expected %s in original\n%s", f, originalRawStr)
		}
	}

	// loading new data shouldn't affect the original state
	record.Load(map[string]any{"name": "name_new2"})

	if v := record.GetString("name"); v != "name_new2" {
		t.Fatalf("Expected name to be %q, got %q", "name_new2", v)
	}

	if v := record.Original().GetString("name"); v != originalName {
		t.Fatalf("Expected the original name still to be %q, got %q", originalName, v)
	}
}

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

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

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

	extraFieldsCheck := []string{`"email":`, `"custom":`}

	autodateTest := types.NowDateTime()

	// change the fields
	record.Id = "changed"
	record.Set("name", "name_new")
	record.Set("custom", "test_custom")
	record.SetRaw("created", autodateTest)
	record.SetExpand(map[string]any{"test": 123})
	record.IgnoreEmailVisibility(true)
	record.IgnoreUnchangedFields(true)
	record.WithCustomData(true)
	record.Unhide(record.Collection().Fields.FieldNames()...)

	// ensure that the email visibility and the custom data toggles are active
	raw, err := record.MarshalJSON()
	if err != nil {
		t.Fatal(err)
	}
	rawStr := string(raw)
	for _, f := range extraFieldsCheck {
		if !strings.Contains(rawStr, f) {
			t.Fatalf("Expected %s in\n%s", f, rawStr)
		}
	}

	// check changes
	if v := record.GetString("name"); v != "name_new" {
		t.Fatalf("Expected name to be %q, got %q", "name_new", v)
	}
	if v := record.GetDateTime("created").String(); v != autodateTest.String() {
		t.Fatalf("Expected created to be %q, got %q", autodateTest.String(), v)
	}
	if v := record.GetString("custom"); v != "test_custom" {
		t.Fatalf("Expected custom to be %q, got %q", "test_custom", v)
	}

	// check fresh
	if v := record.Fresh().LastSavedPK(); v != originalId {
		t.Fatalf("Expected the fresh LastSavedPK to be %q, got %q", originalId, v)
	}
	if v := record.Fresh().PK(); v != record.Id {
		t.Fatalf("Expected the fresh PK to be %q, got %q", record.Id, v)
	}
	if v := record.Fresh().Id; v != record.Id {
		t.Fatalf("Expected the fresh id to be %q, got %q", record.Id, v)
	}
	if v := record.Fresh().GetString("name"); v != record.GetString("name") {
		t.Fatalf("Expected the fresh name to be %q, got %q", record.GetString("name"), v)
	}
	if v := record.Fresh().GetDateTime("created").String(); v != autodateTest.String() {
		t.Fatalf("Expected the fresh created to be %q, got %q", autodateTest.String(), v)
	}
	if v := record.Fresh().GetDateTime("updated").String(); v != record.GetDateTime("updated").String() {
		t.Fatalf("Expected the fresh updated to be %q, got %q", record.GetDateTime("updated").String(), v)
	}
	if v := record.Fresh().GetString("custom"); v != "" {
		t.Fatalf("Expected the fresh custom to be %q, got %q", "", v)
	}
	if v := record.Fresh().Expand(); len(v) != 0 {
		t.Fatalf("Expected empty fresh expand, got\n%v", v)
	}

	// ensure that the email visibility and the custom flag toggles weren't copied
	freshRaw, err := record.Fresh().MarshalJSON()
	if err != nil {
		t.Fatal(err)
	}
	freshRawStr := string(freshRaw)
	for _, f := range extraFieldsCheck {
		if strings.Contains(freshRawStr, f) {
			t.Fatalf("Didn't expected %s in fresh\n%s", f, freshRawStr)
		}
	}
}

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

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

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

	extraFieldsCheck := []string{`"email":`, `"custom":`}

	autodateTest := types.NowDateTime()

	// change the fields
	record.Id = "changed"
	record.Set("name", "name_new")
	record.Set("custom", "test_custom")
	record.SetRaw("created", autodateTest)
	record.SetExpand(map[string]any{"test": 123})
	record.IgnoreEmailVisibility(true)
	record.WithCustomData(true)
	record.Unhide(record.Collection().Fields.FieldNames()...)

	// ensure that the email visibility and the custom data toggles are active
	raw, err := record.MarshalJSON()
	if err != nil {
		t.Fatal(err)
	}
	rawStr := string(raw)
	for _, f := range extraFieldsCheck {
		if !strings.Contains(rawStr, f) {
			t.Fatalf("Expected %s in\n%s", f, rawStr)
		}
	}

	// check changes
	if v := record.GetString("name"); v != "name_new" {
		t.Fatalf("Expected name to be %q, got %q", "name_new", v)
	}
	if v := record.GetDateTime("created").String(); v != autodateTest.String() {
		t.Fatalf("Expected created to be %q, got %q", autodateTest.String(), v)
	}
	if v := record.GetString("custom"); v != "test_custom" {
		t.Fatalf("Expected custom to be %q, got %q", "test_custom", v)
	}

	// check clone
	if v := record.Clone().LastSavedPK(); v != originalId {
		t.Fatalf("Expected the clone LastSavedPK to be %q, got %q", originalId, v)
	}
	if v := record.Clone().PK(); v != record.Id {
		t.Fatalf("Expected the clone PK to be %q, got %q", record.Id, v)
	}
	if v := record.Clone().Id; v != record.Id {
		t.Fatalf("Expected the clone id to be %q, got %q", record.Id, v)
	}
	if v := record.Clone().GetString("name"); v != record.GetString("name") {
		t.Fatalf("Expected the clone name to be %q, got %q", record.GetString("name"), v)
	}
	if v := record.Clone().GetDateTime("created").String(); v != autodateTest.String() {
		t.Fatalf("Expected the clone created to be %q, got %q", autodateTest.String(), v)
	}
	if v := record.Clone().GetDateTime("updated").String(); v != record.GetDateTime("updated").String() {
		t.Fatalf("Expected the clone updated to be %q, got %q", record.GetDateTime("updated").String(), v)
	}
	if v := record.Clone().GetString("custom"); v != "test_custom" {
		t.Fatalf("Expected the clone custom to be %q, got %q", "test_custom", v)
	}
	if _, ok := record.Clone().Expand()["test"]; !ok {
		t.Fatalf("Expected non-empty clone expand")
	}

	// ensure that the email visibility and the custom data toggles state were copied
	cloneRaw, err := record.Clone().MarshalJSON()
	if err != nil {
		t.Fatal(err)
	}
	cloneRawStr := string(cloneRaw)
	for _, f := range extraFieldsCheck {
		if !strings.Contains(cloneRawStr, f) {
			t.Fatalf("Expected %s in clone\n%s", f, cloneRawStr)
		}
	}
}

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

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

	expand := record.Expand()
	if expand == nil || len(expand) != 0 {
		t.Fatalf("Expected empty map expand, got %v", expand)
	}

	data1 := map[string]any{"a": 123, "b": 456}
	data2 := map[string]any{"c": 123}
	record.SetExpand(data1)
	record.SetExpand(data2) // should overwrite the previous call

	// modify the expand map to check for shallow copy
	data2["d"] = 456

	expand = record.Expand()
	if len(expand) != 1 {
		t.Fatalf("Expected empty map expand, got %v", expand)
	}
	if v := expand["c"]; v != 123 {
		t.Fatalf("Expected to find expand.c %v, got %v", 123, v)
	}
}

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

	collection := core.NewBaseCollection("test")
	collection.Id = "_pbc_123"

	m := core.NewRecord(collection)
	m.Id = "m"

	// a
	a := core.NewRecord(collection)
	a.Id = "a"
	a1 := core.NewRecord(collection)
	a1.Id = "a1"
	a2 := core.NewRecord(collection)
	a2.Id = "a2"
	a3 := core.NewRecord(collection)
	a3.Id = "a3"
	a31 := core.NewRecord(collection)
	a31.Id = "a31"
	a32 := core.NewRecord(collection)
	a32.Id = "a32"
	a.SetExpand(map[string]any{
		"a1":  a1,
		"a23": []*core.Record{a2, a3},
	})
	a3.SetExpand(map[string]any{
		"a31": a31,
		"a32": []*core.Record{a32},
	})

	// b
	b := core.NewRecord(collection)
	b.Id = "b"
	b1 := core.NewRecord(collection)
	b1.Id = "b1"
	b.SetExpand(map[string]any{
		"b1": b1,
	})

	// c
	c := core.NewRecord(collection)
	c.Id = "c"

	// load initial expand
	m.SetExpand(map[string]any{
		"a": a,
		"b": b,
		"c": []*core.Record{c},
	})

	// a (new)
	aNew := core.NewRecord(collection)
	aNew.Id = a.Id
	a3New := core.NewRecord(collection)
	a3New.Id = a3.Id
	a32New := core.NewRecord(collection)
	a32New.Id = "a32New"
	a33New := core.NewRecord(collection)
	a33New.Id = "a33New"
	a3New.SetExpand(map[string]any{
		"a32":    []*core.Record{a32New},
		"a33New": a33New,
	})
	aNew.SetExpand(map[string]any{
		"a23": []*core.Record{a2, a3New},
	})

	// b (new)
	bNew := core.NewRecord(collection)
	bNew.Id = "bNew"
	dNew := core.NewRecord(collection)
	dNew.Id = "dNew"

	// merge expands
	m.MergeExpand(map[string]any{
		"a":    aNew,
		"b":    []*core.Record{bNew},
		"dNew": dNew,
	})

	result := m.Expand()

	raw, err := json.Marshal(result)
	if err != nil {
		t.Fatal(err)
	}
	rawStr := string(raw)

	expected := `{"a":{"collectionId":"_pbc_123","collectionName":"test","expand":{"a1":{"collectionId":"_pbc_123","collectionName":"test","id":"a1"},"a23":[{"collectionId":"_pbc_123","collectionName":"test","id":"a2"},{"collectionId":"_pbc_123","collectionName":"test","expand":{"a31":{"collectionId":"_pbc_123","collectionName":"test","id":"a31"},"a32":[{"collectionId":"_pbc_123","collectionName":"test","id":"a32"},{"collectionId":"_pbc_123","collectionName":"test","id":"a32New"}],"a33New":{"collectionId":"_pbc_123","collectionName":"test","id":"a33New"}},"id":"a3"}]},"id":"a"},"b":[{"collectionId":"_pbc_123","collectionName":"test","expand":{"b1":{"collectionId":"_pbc_123","collectionName":"test","id":"b1"}},"id":"b"},{"collectionId":"_pbc_123","collectionName":"test","id":"bNew"}],"c":[{"collectionId":"_pbc_123","collectionName":"test","id":"c"}],"dNew":{"collectionId":"_pbc_123","collectionName":"test","id":"dNew"}}`

	if expected != rawStr {
		t.Fatalf("Expected \n%v, \ngot \n%v", expected, rawStr)
	}
}

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

	collection := core.NewBaseCollection("test")
	collection.Id = "_pbc_123"

	scenarios := []struct {
		name     string
		expand   map[string]any
		expected string
	}{
		{
			"nil expand",
			nil,
			`{"collectionId":"_pbc_123","collectionName":"test","id":""}`,
		},
		{
			"empty expand",
			map[string]any{},
			`{"collectionId":"_pbc_123","collectionName":"test","id":""}`,
		},
		{
			"non-empty expand",
			map[string]any{"test": core.NewRecord(collection)},
			`{"collectionId":"_pbc_123","collectionName":"test","expand":{"test":{"collectionId":"_pbc_123","collectionName":"test","id":""}},"id":""}`,
		},
	}

	for _, s := range scenarios {
		t.Run(s.name, func(t *testing.T) {
			m := core.NewRecord(collection)
			m.MergeExpand(s.expand)

			raw, err := json.Marshal(m)
			if err != nil {
				t.Fatal(err)
			}
			rawStr := string(raw)

			if rawStr != s.expected {
				t.Fatalf("Expected \n%v, \ngot \n%v", s.expected, rawStr)
			}
		})
	}
}

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

	collection := core.NewBaseCollection("test")

	main := core.NewRecord(collection)

	single := core.NewRecord(collection)
	single.Id = "single"

	multiple1 := core.NewRecord(collection)
	multiple1.Id = "multiple1"

	multiple2 := core.NewRecord(collection)
	multiple2.Id = "multiple2"

	main.SetExpand(map[string]any{
		"single":   single,
		"multiple": []*core.Record{multiple1, multiple2},
	})

	if v := main.ExpandedOne("missing"); v != nil {
		t.Fatalf("Expected nil, got %v", v)
	}

	if v := main.ExpandedOne("single"); v == nil || v.Id != "single" {
		t.Fatalf("Expected record with id %q, got %v", "single", v)
	}

	if v := main.ExpandedOne("multiple"); v == nil || v.Id != "multiple1" {
		t.Fatalf("Expected record with id %q, got %v", "multiple1", v)
	}
}

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

	collection := core.NewBaseCollection("test")

	main := core.NewRecord(collection)

	single := core.NewRecord(collection)
	single.Id = "single"

	multiple1 := core.NewRecord(collection)
	multiple1.Id = "multiple1"

	multiple2 := core.NewRecord(collection)
	multiple2.Id = "multiple2"

	main.SetExpand(map[string]any{
		"single":   single,
		"multiple": []*core.Record{multiple1, multiple2},
	})

	if v := main.ExpandedAll("missing"); v != nil {
		t.Fatalf("Expected nil, got %v", v)
	}

	if v := main.ExpandedAll("single"); len(v) != 1 || v[0].Id != "single" {
		t.Fatalf("Expected [single] slice, got %v", v)
	}

	if v := main.ExpandedAll("multiple"); len(v) != 2 || v[0].Id != "multiple1" || v[1].Id != "multiple2" {
		t.Fatalf("Expected [multiple1, multiple2] slice, got %v", v)
	}
}

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

	collection := core.NewAuthCollection("test")
	collection.Fields.Add(&core.TextField{Name: "field1"})
	collection.Fields.Add(&core.TextField{Name: "field2"})

	m := core.NewRecord(collection)
	m.Id = "test_id" // direct id assignment
	m.Set("email", "test@example.com")
	m.Set("password", "123") // hidden fields should be also returned
	m.Set("tokenKey", "789")
	m.Set("field1", 123)
	m.Set("field2", 456)
	m.Set("unknown", 789)

	raw, err := json.Marshal(m.FieldsData())
	if err != nil {
		t.Fatal(err)
	}

	expected := `{"email":"test@example.com","emailVisibility":false,"field1":"123","field2":"456","id":"test_id","password":"123","tokenKey":"789","verified":false}`

	if v := string(raw); v != expected {
		t.Fatalf("Expected\n%v\ngot\n%v", expected, v)
	}
}

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

	collection := core.NewAuthCollection("test")
	collection.Fields.Add(&core.TextField{Name: "field1"})
	collection.Fields.Add(&core.TextField{Name: "field2"})

	m := core.NewRecord(collection)
	m.Id = "test_id" // direct id assignment
	m.Set("email", "test@example.com")
	m.Set("password", "123") // hidden fields should be also returned
	m.Set("tokenKey", "789")
	m.Set("field1", 123)
	m.Set("field2", 456)
	m.Set("unknown", 789)

	raw, err := json.Marshal(m.CustomData())
	if err != nil {
		t.Fatal(err)
	}

	expected := `{"unknown":789}`

	if v := string(raw); v != expected {
		t.Fatalf("Expected\n%v\ngot\n%v", expected, v)
	}
}

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

	f1 := &mockField{}
	f1.Name = "mock1"

	f2 := &mockField{}
	f2.Name = "mock2"

	f3 := &mockField{}
	f3.Name = "mock3"

	collection := core.NewBaseCollection("test")
	collection.Fields.Add(&core.TextField{Name: "text1"})
	collection.Fields.Add(&core.TextField{Name: "text2"})
	collection.Fields.Add(f1)
	collection.Fields.Add(f2)
	collection.Fields.Add(f3)

	record := core.NewRecord(collection)
	record.Set("text1", 123) // should be converted to string using the ScanValue fallback
	record.SetRaw("text2", 456)
	record.Set("mock1", 1) // should be converted to string using the setter
	record.SetRaw("mock2", 1)
	record.Set("mock3:test", "abc")
	record.Set("unknown", 789)

	t.Run("GetRaw", func(t *testing.T) {
		expected := map[string]any{
			"text1":      "123",
			"text2":      456,
			"mock1":      "1",
			"mock2":      1,
			"mock3":      "modifier_set",
			"mock3:test": nil,
			"unknown":    789,
		}

		for k, v := range expected {
			raw := record.GetRaw(k)
			if raw != v {
				t.Errorf("Expected %q to be %v, got %v", k, v, raw)
			}
		}
	})

	t.Run("Get", func(t *testing.T) {
		expected := map[string]any{
			"text1":      "123",
			"text2":      456,
			"mock1":      "1",
			"mock2":      1,
			"mock3":      "modifier_set",
			"mock3:test": "modifier_get",
			"unknown":    789,
		}

		for k, v := range expected {
			get := record.Get(k)
			if get != v {
				t.Errorf("Expected %q to be %v, got %v", k, v, get)
			}
		}
	})
}

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

	collection := core.NewBaseCollection("test")
	collection.Fields.Add(&core.TextField{Name: "text"})

	record := core.NewRecord(collection)
	record.Load(map[string]any{
		"text":   123,
		"custom": 456,
	})

	expected := map[string]any{
		"text":   "123",
		"custom": 456,
	}

	for k, v := range expected {
		get := record.Get(k)
		if get != v {
			t.Errorf("Expected %q to be %#v, got %#v", k, v, get)
		}
	}
}

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

	scenarios := []struct {
		value    any
		expected bool
	}{
		{nil, false},
		{"", false},
		{0, false},
		{1, true},
		{[]string{"true"}, false},
		{time.Now(), false},
		{"test", false},
		{"false", false},
		{"true", true},
		{false, false},
		{true, true},
	}

	collection := core.NewBaseCollection("test")
	record := core.NewRecord(collection)

	for i, s := range scenarios {
		t.Run(fmt.Sprintf("%d_%#v", i, s.value), func(t *testing.T) {
			record.Set("test", s.value)

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

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

	scenarios := []struct {
		value    any
		expected string
	}{
		{nil, ""},
		{"", ""},
		{0, "0"},
		{1.4, "1.4"},
		{[]string{"true"}, ""},
		{map[string]int{"test": 1}, ""},
		{[]byte("abc"), "abc"},
		{"test", "test"},
		{false, "false"},
		{true, "true"},
	}

	collection := core.NewBaseCollection("test")
	record := core.NewRecord(collection)

	for i, s := range scenarios {
		t.Run(fmt.Sprintf("%d_%#v", i, s.value), func(t *testing.T) {
			record.Set("test", s.value)

			result := record.GetString("test")
			if result != s.expected {
				t.Fatalf("Expected %q, got %q", s.expected, result)
			}
		})
	}
}

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

	scenarios := []struct {
		value    any
		expected int
	}{
		{nil, 0},
		{"", 0},
		{[]string{"true"}, 0},
		{map[string]int{"test": 1}, 0},
		{time.Now(), 0},
		{"test", 0},
		{123, 123},
		{2.4, 2},
		{"123", 123},
		{"123.5", 0},
		{false, 0},
		{true, 1},
	}

	collection := core.NewBaseCollection("test")
	record := core.NewRecord(collection)

	for i, s := range scenarios {
		t.Run(fmt.Sprintf("%d_%#v", i, s.value), func(t *testing.T) {
			record.Set("test", s.value)

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

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

	scenarios := []struct {
		value    any
		expected float64
	}{
		{nil, 0},
		{"", 0},
		{[]string{"true"}, 0},
		{map[string]int{"test": 1}, 0},
		{time.Now(), 0},
		{"test", 0},
		{123, 123},
		{2.4, 2.4},
		{"123", 123},
		{"123.5", 123.5},
		{false, 0},
		{true, 1},
	}

	collection := core.NewBaseCollection("test")
	record := core.NewRecord(collection)

	for i, s := range scenarios {
		t.Run(fmt.Sprintf("%d_%#v", i, s.value), func(t *testing.T) {
			record.Set("test", s.value)

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

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

	nowTime := time.Now()
	testTime, _ := time.Parse(types.DefaultDateLayout, "2022-01-01 08:00:40.000Z")

	scenarios := []struct {
		value    any
		expected time.Time
	}{
		{nil, time.Time{}},
		{"", time.Time{}},
		{false, time.Time{}},
		{true, time.Time{}},
		{"test", time.Time{}},
		{[]string{"true"}, time.Time{}},
		{map[string]int{"test": 1}, time.Time{}},
		{1641024040, testTime},
		{"2022-01-01 08:00:40.000", testTime},
		{nowTime, nowTime},
	}

	collection := core.NewBaseCollection("test")
	record := core.NewRecord(collection)

	for i, s := range scenarios {
		t.Run(fmt.Sprintf("%d_%#v", i, s.value), func(t *testing.T) {
			record.Set("test", s.value)

			result := record.GetDateTime("test")
			if !result.Time().Equal(s.expected) {
				t.Fatalf("Expected %v, got %v", s.expected, result)
			}
		})
	}
}

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

	nowTime := time.Now()

	scenarios := []struct {
		value    any
		expected []string
	}{
		{nil, []string{}},
		{"", []string{}},
		{false, []string{"false"}},
		{true, []string{"true"}},
		{nowTime, []string{}},
		{123, []string{"123"}},
		{"test", []string{"test"}},
		{map[string]int{"test": 1}, []string{}},
		{`["test1", "test2"]`, []string{"test1", "test2"}},
		{[]int{123, 123, 456}, []string{"123", "456"}},
		{[]string{"test", "test", "123"}, []string{"test", "123"}},
	}

	collection := core.NewBaseCollection("test")
	record := core.NewRecord(collection)

	for i, s := range scenarios {
		t.Run(fmt.Sprintf("%d_%#v", i, s.value), func(t *testing.T) {
			record.Set("test", s.value)

			result := record.GetStringSlice("test")

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

			for _, v := range result {
				if !slices.Contains(s.expected, v) {
					t.Fatalf("Cannot find %v in %v", v, s.expected)
				}
			}
		})
	}
}

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

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

	f1, err := filesystem.NewFileFromBytes([]byte("test"), "f1")
	if err != nil {
		t.Fatal(err)
	}
	f1.Name = "f1"

	f2, err := filesystem.NewFileFromBytes([]byte("test"), "f2")
	if err != nil {
		t.Fatal(err)
	}
	f2.Name = "f2"

	record, err := app.FindRecordById("demo3", "lcl9d87w22ml6jy")
	if err != nil {
		t.Fatal(err)
	}
	record.Set("files+", []any{f1, f2})

	scenarios := []struct {
		key      string
		expected string
	}{
		{
			"",
			"null",
		},
		{
			"title",
			"null",
		},
		{
			"files",
			`[{"name":"f1","originalName":"f1","size":4},{"name":"f2","originalName":"f2","size":4}]`,
		},
		{
			"files:uploaded",
			`[{"name":"f1","originalName":"f1","size":4},{"name":"f2","originalName":"f2","size":4}]`,
		},
	}

	for i, s := range scenarios {
		t.Run(fmt.Sprintf("%d_%#v", i, s.key), func(t *testing.T) {
			v := record.GetUploadedFiles(s.key)

			raw, err := json.Marshal(v)
			if err != nil {
				t.Fatal(err)
			}
			rawStr := string(raw)

			if rawStr != s.expected {
				t.Fatalf("Expected\n%s\ngot\n%s", s.expected, rawStr)
			}
		})
	}
}

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

	collection := core.NewBaseCollection("test")
	collection.Fields.Add(&core.JSONField{Name: "field"})

	record := core.NewRecord(collection)

	var testPointer *string
	var testStr string
	var testInt int
	var testBool bool
	var testSlice []int
	var testMap map[string]any

	scenarios := []struct {
		value        any
		destination  any
		expectError  bool
		expectedJSON string
	}{
		{nil, testPointer, false, `null`},
		{nil, testStr, false, `""`},
		{"", testStr, false, `""`},
		{1, testInt, false, `1`},
		{true, testBool, false, `true`},
		{[]int{1, 2, 3}, testSlice, false, `[1,2,3]`},
		{map[string]any{"test": 123}, testMap, false, `{"test":123}`},
		// json encoded values
		{`null`, testPointer, false, `null`},
		{`true`, testBool, false, `true`},
		{`456`, testInt, false, `456`},
		{`"test"`, testStr, false, `"test"`},
		{`[4,5,6]`, testSlice, false, `[4,5,6]`},
		{`{"test":456}`, testMap, false, `{"test":456}`},
	}

	for i, s := range scenarios {
		t.Run(fmt.Sprintf("%d_%#v", i, s.value), func(t *testing.T) {
			record.Set("field", s.value)

			err := record.UnmarshalJSONField("field", &s.destination)
			hasErr := err != nil

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

			raw, _ := json.Marshal(s.destination)
			if v := string(raw); v != s.expectedJSON {
				t.Fatalf("Expected %q, got %q", s.expectedJSON, v)
			}
		})
	}
}

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

	collection := core.NewBaseCollection("test")
	collection.Fields.Add(
		&core.TextField{Name: "field1"},
		&core.FileField{Name: "field2", MaxSelect: 1, MaxSize: 1},
		&core.FileField{Name: "field3", MaxSelect: 2, MaxSize: 1},
	)

	m := core.NewRecord(collection)
	m.Set("field1", "test")
	m.Set("field2", "test.png")
	m.Set("field3", []string{"test1.png", "test2.png"})

	scenarios := []struct {
		filename    string
		expectField string
	}{
		{"", ""},
		{"test", ""},
		{"test2", ""},
		{"test.png", "field2"},
		{"test2.png", "field3"},
	}

	for i, s := range scenarios {
		t.Run(fmt.Sprintf("%d_%#v", i, s.filename), func(t *testing.T) {
			result := m.FindFileFieldByFile(s.filename)

			var fieldName string
			if result != nil {
				fieldName = result.Name
			}

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

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

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

	f1 := &core.TextField{Name: "field1"}
	f2 := &core.FileField{Name: "field2", MaxSelect: 1, MaxSize: 1}
	f3 := &core.SelectField{Name: "field3", MaxSelect: 2, Values: []string{"test1", "test2", "test3"}}
	f4 := &core.RelationField{Name: "field4", MaxSelect: 2}

	colBase := core.NewBaseCollection("test_base")
	colBase.Fields.Add(f1, f2, f3, f4)

	colAuth := core.NewAuthCollection("test_auth")
	colAuth.Fields.Add(f1, f2, f3, f4)

	scenarios := []struct {
		collection *core.Collection
		expected   string
	}{
		{
			colBase,
			`{"field1":"test","field2":"test.png","field3":["test1","test2"],"field4":["test11","test12"],"id":"test_id"}`,
		},
		{
			colAuth,
			`{"email":"test_email","emailVisibility":true,"field1":"test","field2":"test.png","field3":["test1","test2"],"field4":["test11","test12"],"id":"test_id","password":"_TEST_","tokenKey":"test_tokenKey","verified":false}`,
		},
	}

	data := map[string]any{
		"id":              "test_id",
		"field1":          "test",
		"field2":          "test.png",
		"field3":          []string{"test1", "test2"},
		"field4":          []string{"test11", "test12", "test11"}, // strip duplicate,
		"unknown":         "test_unknown",
		"password":        "test_passwordHash",
		"username":        "test_username",
		"emailVisibility": true,
		"email":           "test_email",
		"verified":        "invalid", // should be casted
		"tokenKey":        "test_tokenKey",
	}

	for i, s := range scenarios {
		t.Run(fmt.Sprintf("%d_%s_%s", i, s.collection.Type, s.collection.Name), func(t *testing.T) {
			record := core.NewRecord(s.collection)

			record.Load(data)

			result, err := record.DBExport(app)
			if err != nil {
				t.Fatal(err)
			}

			raw, err := json.Marshal(result)
			if err != nil {
				t.Fatal(err)
			}
			rawStr := string(raw)

			// replace _TEST_ placeholder with .+ regex pattern
			pattern := regexp.MustCompile(strings.ReplaceAll(
				"^"+regexp.QuoteMeta(s.expected)+"$",
				"_TEST_",
				`.+`,
			))

			if !pattern.MatchString(rawStr) {
				t.Fatalf("Expected\n%v\ngot\n%v", s.expected, rawStr)
			}
		})
	}
}

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

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

	col, err := app.FindCollectionByNameOrId("demo3")
	if err != nil {
		t.Fatal(err)
	}

	new := core.NewRecord(col)

	existing, err := app.FindRecordById(col, "mk5fmymtx4wsprk")
	if err != nil {
		t.Fatal(err)
	}
	existing.Set("title", "test_new")
	existing.Set("files", existing.Get("files")) // no change

	scenarios := []struct {
		ignoreUnchangedFields bool
		record                *core.Record
		expected              []string
	}{
		{
			false,
			new,
			[]string{"id", "created", "updated", "title", "files"},
		},
		{
			true,
			new,
			[]string{"id", "created", "updated", "title", "files"},
		},
		{
			false,
			existing,
			[]string{"id", "created", "updated", "title", "files"},
		},
		{
			true,
			existing,
			[]string{"id", "title"},
		},
	}

	for i, s := range scenarios {
		action := "create"
		if !s.record.IsNew() {
			action = "update"
		}

		t.Run(fmt.Sprintf("%d_%s_%v", i, action, s.ignoreUnchangedFields), func(t *testing.T) {
			s.record.IgnoreUnchangedFields(s.ignoreUnchangedFields)

			result, err := s.record.DBExport(app)
			if err != nil {
				t.Fatal(err)
			}

			if len(result) != len(s.expected) {
				t.Fatalf("Expected %d keys, got %d:\n%v", len(s.expected), len(result), result)
			}

			for _, key := range s.expected {
				if _, ok := result[key]; !ok {
					t.Fatalf("Missing expected key %q in\n%v", key, result)
				}
			}
		})
	}
}

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

	f1 := &core.TextField{Name: "field1"}
	f2 := &core.FileField{Name: "field2", MaxSelect: 1, MaxSize: 1}
	f3 := &core.SelectField{Name: "field3", MaxSelect: 2, Values: []string{"test1", "test2", "test3"}}
	f4 := &core.TextField{Name: "field4", Hidden: true}
	f5 := &core.TextField{Name: "field5", Hidden: true}

	colBase := core.NewBaseCollection("test_base")
	colBase.Id = "_pbc_base_123"
	colBase.Fields.Add(f1, f2, f3, f4, f5)

	colAuth := core.NewAuthCollection("test_auth")
	colAuth.Id = "_pbc_auth_123"
	colAuth.Fields.Add(f1, f2, f3, f4, f5)

	scenarios := []struct {
		name                  string
		collection            *core.Collection
		ignoreEmailVisibility bool
		withCustomData        bool
		hideFields            []string
		unhideFields          []string
		expectedJSON          string
	}{
		// base
		{
			"[base] no extra flags",
			colBase,
			false,
			false,
			nil,
			nil,
			`{"collectionId":"_pbc_base_123","collectionName":"test_base","expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"id":"test_id"}`,
		},
		{
			"[base] with email visibility",
			colBase,
			true, // should have no effect
			false,
			nil,
			nil,
			`{"collectionId":"_pbc_base_123","collectionName":"test_base","expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"id":"test_id"}`,
		},
		{
			"[base] with custom data",
			colBase,
			true, // should have no effect
			true,
			nil,
			nil,
			`{"collectionId":"_pbc_base_123","collectionName":"test_base","email":"test_email","emailVisibility":"test_invalid","expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"id":"test_id","password":"test_passwordHash","tokenKey":"test_tokenKey","unknown":"test_unknown","verified":true}`,
		},
		{
			"[base] with explicit hide and unhide fields",
			colBase,
			false,
			true,
			[]string{"field3", "field1", "expand", "collectionId", "collectionName", "email", "tokenKey", "unknown"},
			[]string{"field4", "@pbInternalAbc"},
			`{"emailVisibility":"test_invalid","field2":"field_2.png","field4":"field_4","id":"test_id","password":"test_passwordHash","verified":true}`,
		},
		{
			"[base] trying to unhide custom fields without explicit WithCustomData",
			colBase,
			false,
			true,
			nil,
			[]string{"field5", "@pbInternalAbc", "email", "tokenKey", "unknown"},
			`{"collectionId":"_pbc_base_123","collectionName":"test_base","email":"test_email","emailVisibility":"test_invalid","expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"field5":"field_5","id":"test_id","password":"test_passwordHash","tokenKey":"test_tokenKey","unknown":"test_unknown","verified":true}`,
		},

		// auth
		{
			"[auth] no extra flags",
			colAuth,
			false,
			false,
			nil,
			nil,
			`{"collectionId":"_pbc_auth_123","collectionName":"test_auth","emailVisibility":false,"expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"id":"test_id","verified":true}`,
		},
		{
			"[auth] with email visibility",
			colAuth,
			true,
			false,
			nil,
			nil,
			`{"collectionId":"_pbc_auth_123","collectionName":"test_auth","email":"test_email","emailVisibility":false,"expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"id":"test_id","verified":true}`,
		},
		{
			"[auth] with custom data",
			colAuth,
			false,
			true,
			nil,
			nil,
			`{"collectionId":"_pbc_auth_123","collectionName":"test_auth","emailVisibility":false,"expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"id":"test_id","unknown":"test_unknown","verified":true}`,
		},
		{
			"[auth] with explicit hide and unhide fields",
			colAuth,
			true,
			true,
			[]string{"field3", "field1", "expand", "collectionId", "collectionName", "email", "unknown"},
			[]string{"field4", "@pbInternalAbc"},
			`{"emailVisibility":false,"field2":"field_2.png","field4":"field_4","id":"test_id","verified":true}`,
		},
		{
			"[auth] trying to unhide custom fields without explicit WithCustomData",
			colAuth,
			false,
			true,
			nil,
			[]string{"field5", "@pbInternalAbc", "tokenKey", "unknown", "email"}, // emailVisibility:false has higher priority
			`{"collectionId":"_pbc_auth_123","collectionName":"test_auth","emailVisibility":false,"expand":{"test":123},"field1":"field_1","field2":"field_2.png","field3":["test1","test2"],"field5":"field_5","id":"test_id","unknown":"test_unknown","verified":true}`,
		},
	}

	data := map[string]any{
		"id":              "test_id",
		"field1":          "field_1",
		"field2":          "field_2.png",
		"field3":          []string{"test1", "test2"},
		"field4":          "field_4",
		"field5":          "field_5",
		"expand":          map[string]any{"test": 123},
		"collectionId":    "m_id",   // should be always ignored
		"collectionName":  "m_name", // should be always ignored
		"unknown":         "test_unknown",
		"password":        "test_passwordHash",
		"emailVisibility": "test_invalid", // for auth collections should be casted to bool
		"email":           "test_email",
		"verified":        true,
		"tokenKey":        "test_tokenKey",
		"@pbInternalAbc":  "test_custom_inter", // always hidden
	}

	for _, s := range scenarios {
		t.Run(s.name, func(t *testing.T) {
			m := core.NewRecord(s.collection)

			m.Load(data)
			m.IgnoreEmailVisibility(s.ignoreEmailVisibility)
			m.WithCustomData(s.withCustomData)
			m.Unhide(s.unhideFields...)
			m.Hide(s.hideFields...)

			exportResult, err := json.Marshal(m.PublicExport())
			if err != nil {
				t.Fatal(err)
			}
			exportResultStr := string(exportResult)

			// MarshalJSON and PublicExport should return the same
			marshalResult, err := m.MarshalJSON()
			if err != nil {
				t.Fatal(err)
			}
			marshalResultStr := string(marshalResult)

			if exportResultStr != marshalResultStr {
				t.Fatalf("Expected the PublicExport to be the same as MarshalJSON, but got \n%v \nvs \n%v", exportResultStr, marshalResultStr)
			}

			if exportResultStr != s.expectedJSON {
				t.Fatalf("Expected json \n%v \ngot \n%v", s.expectedJSON, exportResultStr)
			}
		})
	}
}

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

	collection := core.NewBaseCollection("test")
	collection.Fields.Add(&core.TextField{Name: "text"})

	record := core.NewRecord(collection)

	data := map[string]any{
		"text":   123,
		"custom": 456.789,
	}
	rawData, err := json.Marshal(data)
	if err != nil {
		t.Fatal(err)
	}

	err = record.UnmarshalJSON(rawData)
	if err != nil {
		t.Fatalf("Failed to unmarshal: %v", err)
	}

	expected := map[string]any{
		"text":   "123",
		"custom": 456.789,
	}

	for k, v := range expected {
		get := record.Get(k)
		if get != v {
			t.Errorf("Expected %q to be %#v, got %#v", k, v, get)
		}
	}
}

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

	collection := core.NewBaseCollection("test")
	collection.Fields.Add(
		&mockField{core.TextField{Name: "mock"}},
		&core.NumberField{Name: "number"},
	)

	originalData := map[string]any{
		"mock":   "a",
		"number": 2.1,
	}

	record := core.NewRecord(collection)
	for k, v := range originalData {
		record.Set(k, v)
	}

	result := record.ReplaceModifiers(map[string]any{
		"mock:test": "b",
		"number+":   3,
	})

	expected := map[string]any{
		"mock":   "modifier_set",
		"number": 5.1,
	}

	if len(result) != len(expected) {
		t.Fatalf("Expected\n%v\ngot\n%v", expected, result)
	}

	for k, v := range expected {
		if result[k] != v {
			t.Errorf("Expected %q %#v, got %#v", k, v, result[k])
		}
	}

	// ensure that the original data hasn't changed
	for k, v := range originalData {
		rv := record.Get(k)
		if rv != v {
			t.Errorf("Expected original %q %#v, got %#v", k, v, rv)
		}
	}
}

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

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

	// dummy collection to ensure that the specified field validators are triggered
	collection := core.NewBaseCollection("validate_test")
	collection.Fields.Add(
		&core.TextField{Name: "f1", Min: 3},
		&core.NumberField{Name: "f2", Required: true},
	)
	if err := app.Save(collection); err != nil {
		t.Fatal(err)
	}

	record := core.NewRecord(collection)
	record.Id = "!invalid"

	t.Run("no data set", func(t *testing.T) {
		tests.TestValidationErrors(t, app.Validate(record), []string{"id", "f2"})
	})

	t.Run("failing the text field min requirement", func(t *testing.T) {
		record.Set("f1", "a")
		tests.TestValidationErrors(t, app.Validate(record), []string{"id", "f1", "f2"})
	})

	t.Run("satisfying the fields validations", func(t *testing.T) {
		record.Id = strings.Repeat("b", 15)
		record.Set("f1", "abc")
		record.Set("f2", 1)
		tests.TestValidationErrors(t, app.Validate(record), nil)
	})
}

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

	scenarios := []struct {
		name        string
		record      func(app core.App) (*core.Record, error)
		expectError bool
	}{
		// trigger validators
		{
			name: "create - trigger validators",
			record: func(app core.App) (*core.Record, error) {
				c, _ := app.FindCollectionByNameOrId("demo2")
				record := core.NewRecord(c)
				return record, nil
			},
			expectError: true,
		},
		{
			name: "update - trigger validators",
			record: func(app core.App) (*core.Record, error) {
				record, _ := app.FindFirstRecordByData("demo2", "title", "test1")
				record.Set("title", "")
				return record, nil
			},
			expectError: true,
		},

		// create
		{
			name: "create base record",
			record: func(app core.App) (*core.Record, error) {
				c, _ := app.FindCollectionByNameOrId("demo2")
				record := core.NewRecord(c)
				record.Set("title", "new_test")
				return record, nil
			},
			expectError: false,
		},
		{
			name: "create auth record",
			record: func(app core.App) (*core.Record, error) {
				c, _ := app.FindCollectionByNameOrId("nologin")
				record := core.NewRecord(c)
				record.Set("email", "test_new@example.com")
				record.Set("password", "1234567890")
				return record, nil
			},
			expectError: false,
		},
		{
			name: "create view record",
			record: func(app core.App) (*core.Record, error) {
				c, _ := app.FindCollectionByNameOrId("view2")
				record := core.NewRecord(c)
				record.Set("state", true)
				return record, nil
			},
			expectError: true, // view records are read-only
		},

		// update
		{
			name: "update base record",
			record: func(app core.App) (*core.Record, error) {
				record, _ := app.FindFirstRecordByData("demo2", "title", "test1")
				record.Set("title", "test_new")
				return record, nil
			},
			expectError: false,
		},
		{
			name: "update auth record",
			record: func(app core.App) (*core.Record, error) {
				record, _ := app.FindAuthRecordByEmail("nologin", "test@example.com")
				record.Set("name", "test_new")
				record.Set("email", "test_new@example.com")
				return record, nil
			},
			expectError: false,
		},
		{
			name: "update view record",
			record: func(app core.App) (*core.Record, error) {
				record, _ := app.FindFirstRecordByData("view2", "state", true)
				record.Set("state", false)
				return record, nil
			},
			expectError: true, // view records are read-only
		},
	}

	for _, s := range scenarios {
		t.Run(s.name, func(t *testing.T) {
			app, _ := tests.NewTestApp()
			defer app.Cleanup()

			record, err := s.record(app)
			if err != nil {
				t.Fatalf("Failed to retrieve test record: %v", err)
			}

			saveErr := app.Save(record)

			hasErr := saveErr != nil
			if hasErr != s.expectError {
				t.Fatalf("Expected hasErr %v, got %v (%v)", hasErr, s.expectError, saveErr)
			}

			if hasErr {
				return
			}

			// the record should always have an id after successful Save
			if record.Id == "" {
				t.Fatal("Expected record id to be set")
			}

			if record.IsNew() {
				t.Fatal("Expected the record to be marked as not new")
			}

			// refetch and compare the serialization
			refreshed, err := app.FindRecordById(record.Collection(), record.Id)
			if err != nil {
				t.Fatal(err)
			}

			rawRefreshed, err := refreshed.MarshalJSON()
			if err != nil {
				t.Fatal(err)
			}

			raw, err := record.MarshalJSON()
			if err != nil {
				t.Fatal(err)
			}

			if !bytes.Equal(raw, rawRefreshed) {
				t.Fatalf("Expected the refreshed record to be the same as the saved one, got\n%s\nVS\n%s", raw, rawRefreshed)
			}
		})
	}
}

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

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

	baseCollection, _ := app.FindCollectionByNameOrId("demo2")
	authCollection, _ := app.FindCollectionByNameOrId("nologin")

	// base collection test
	r1 := core.NewRecord(baseCollection)
	r1.Set("title", "test_new")
	r1.Set("id", "mk5fmymtx4wsprk") // existing id of demo3 record
	if err := app.Save(r1); err != nil {
		t.Fatalf("Expected nil, got error %v", err)
	}

	// auth collection test
	r2 := core.NewRecord(authCollection)
	r2.SetEmail("test_new@example.com")
	r2.SetPassword("1234567890")
	r2.Set("id", "gk390qegs4y47wn") // existing id of "clients" record
	if err := app.Save(r2); err == nil {
		t.Fatal("Expected error, got nil")
	}

	// try again with unique id
	r2.Set("id", strings.Repeat("a", 15))
	if err := app.Save(r2); err != nil {
		t.Fatalf("Expected nil, got error %v", err)
	}
}

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

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

	rec, err := app.FindRecordById("demo3", "7nwo8tuiatetxdm")
	if err != nil {
		t.Fatal(err)
	}

	rec.Id = strings.Repeat("a", 15)

	err = app.SaveNoValidate(rec)
	if err == nil {
		t.Fatal("Expected save to fail, got nil")
	}

	// no changes
	rec.Load(rec.Original().FieldsData())
	err = app.SaveNoValidate(rec)
	if err != nil {
		t.Fatalf("Expected save to succeed, got error %v", err)
	}
}

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

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

	scenarios := []struct {
		name           string
		payload        map[string]any
		expectedChange bool
	}{
		{
			"no email or password change",
			map[string]any{"name": "example"},
			false,
		},
		{
			"password change",
			map[string]any{"password": "1234567890"},
			true,
		},
		{
			"email change",
			map[string]any{"email": "test_update@example.com"},
			true,
		},
	}

	for _, s := range scenarios {
		t.Run(s.name, func(t *testing.T) {
			record, err := app.FindFirstRecordByFilter("nologin", "1=1")
			if err != nil {
				t.Fatal(err)
			}

			originalTokenKey := record.TokenKey()

			record.Load(s.payload)

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

			newTokenKey := record.TokenKey()

			hasChange := originalTokenKey != newTokenKey

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

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

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

	demoCollection, _ := app.FindCollectionByNameOrId("demo2")

	// delete unsaved record
	// ---
	newRec := core.NewRecord(demoCollection)
	if err := app.Delete(newRec); err == nil {
		t.Fatal("(newRec) Didn't expect to succeed deleting unsaved record")
	}

	// delete view record
	// ---
	viewRec, _ := app.FindRecordById("view2", "84nmscqy84lsi1t")
	if err := app.Delete(viewRec); err == nil {
		t.Fatal("(viewRec) Didn't expect to succeed deleting view record")
	}
	// check if it still exists
	viewRec, _ = app.FindRecordById(viewRec.Collection().Id, viewRec.Id)
	if viewRec == nil {
		t.Fatal("(viewRec) Expected view record to still exists")
	}

	// delete existing record + external auths
	// ---
	rec1, _ := app.FindRecordById("users", "4q1xlclmfloku33")
	if err := app.Delete(rec1); err != nil {
		t.Fatalf("(rec1) Expected nil, got error %v", err)
	}
	// check if it was really deleted
	if refreshed, _ := app.FindRecordById(rec1.Collection().Id, rec1.Id); refreshed != nil {
		t.Fatalf("(rec1) Expected record to be deleted, got %v", refreshed)
	}
	// check if the external auths were deleted
	if auths, _ := app.FindAllExternalAuthsByRecord(rec1); len(auths) > 0 {
		t.Fatalf("(rec1) Expected external auths to be deleted, got %v", auths)
	}

	// delete existing record while being part of a non-cascade required relation
	// ---
	rec2, _ := app.FindRecordById("demo3", "7nwo8tuiatetxdm")
	if err := app.Delete(rec2); err == nil {
		t.Fatalf("(rec2) Expected error, got nil")
	}

	// delete existing record + cascade
	// ---
	calledQueries := []string{}
	app.NonconcurrentDB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {
		calledQueries = append(calledQueries, sql)
	}
	app.DB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {
		calledQueries = append(calledQueries, sql)
	}
	app.NonconcurrentDB().(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) {
		calledQueries = append(calledQueries, sql)
	}
	app.DB().(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) {
		calledQueries = append(calledQueries, sql)
	}
	rec3, _ := app.FindRecordById("users", "oap640cot4yru2s")
	// delete
	if err := app.Delete(rec3); err != nil {
		t.Fatalf("(rec3) Expected nil, got error %v", err)
	}
	// check if it was really deleted
	rec3, _ = app.FindRecordById(rec3.Collection().Id, rec3.Id)
	if rec3 != nil {
		t.Fatalf("(rec3) Expected record to be deleted, got %v", rec3)
	}
	// check if the operation cascaded
	rel, _ := app.FindRecordById("demo1", "84nmscqy84lsi1t")
	if rel != nil {
		t.Fatalf("(rec3) Expected the delete to cascade, found relation %v", rel)
	}
	// ensure that the json rel fields were prefixed
	joinedQueries := strings.Join(calledQueries, " ")
	expectedRelManyPart := "SELECT `demo1`.* FROM `demo1` WHERE EXISTS (SELECT 1 FROM json_each(CASE WHEN json_valid([[demo1.rel_many]]) THEN [[demo1.rel_many]] ELSE json_array([[demo1.rel_many]]) END) {{__je__}} WHERE [[__je__.value]]='"
	if !strings.Contains(joinedQueries, expectedRelManyPart) {
		t.Fatalf("(rec3) Expected the cascade delete to call the query \n%v, got \n%v", expectedRelManyPart, calledQueries)
	}
	expectedRelOnePart := "SELECT `demo1`.* FROM `demo1` WHERE (`demo1`.`rel_one`='"
	if !strings.Contains(joinedQueries, expectedRelOnePart) {
		t.Fatalf("(rec3) Expected the cascade delete to call the query \n%v, got \n%v", expectedRelOnePart, calledQueries)
	}
}

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

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

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

	// find and delete the first c1 record to trigger cascade
	mainRecord, _ := app.FindRecordById("c1", "a")
	if err := app.Delete(mainRecord); err != nil {
		t.Fatal(err)
	}

	// check if the main record was deleted
	_, err := app.FindRecordById(mainRecord.Collection().Id, mainRecord.Id)
	if err == nil {
		t.Fatal("The main record wasn't deleted")
	}

	// check if the c1 b rel field were updated
	c1RecordB, err := app.FindRecordById("c1", "b")
	if err != nil || c1RecordB.GetString("rel") != "" {
		t.Fatalf("Expected c1RecordB.rel to be nil, got %v", c1RecordB.GetString("rel"))
	}

	// check if the c2 rel fields were updated
	c2Records, err := app.FindAllRecords("c2", nil)
	if err != nil || len(c2Records) == 0 {
		t.Fatalf("Failed to fetch c2 records: %v", err)
	}
	for _, r := range c2Records {
		ids := r.GetStringSlice("rel")
		if len(ids) != 1 || ids[0] != "b" {
			t.Fatalf("Expected only 'b' rel id, got %v", ids)
		}
	}

	// check if all c3 relations were deleted
	c3Records, err := app.FindAllRecords("c3", nil)
	if err != nil {
		t.Fatalf("Failed to fetch c3 records: %v", err)
	}
	if total := len(c3Records); total != 0 {
		t.Fatalf("Expected c3 records to be deleted, found %d", total)
	}
}

func createMockBatchProcessingData(app core.App) error {
	// create mock collection without relation
	c1 := core.NewBaseCollection("c1")
	c1.Id = "c1"
	c1.Fields.Add(
		&core.TextField{Name: "text"},
		&core.RelationField{
			Name:          "rel",
			MaxSelect:     1,
			CollectionId:  "c1",
			CascadeDelete: false, // should unset all rel fields
		},
	)
	if err := app.SaveNoValidate(c1); err != nil {
		return err
	}

	// create mock collection with a multi-rel field
	c2 := core.NewBaseCollection("c2")
	c2.Id = "c2"
	c2.Fields.Add(
		&core.TextField{Name: "text"},
		&core.RelationField{
			Name:          "rel",
			MaxSelect:     10,
			CollectionId:  "c1",
			CascadeDelete: false, // should unset all rel fields
		},
	)
	if err := app.SaveNoValidate(c2); err != nil {
		return err
	}

	// create mock collection with a single-rel field
	c3 := core.NewBaseCollection("c3")
	c3.Id = "c3"
	c3.Fields.Add(
		&core.RelationField{
			Name:          "rel",
			MaxSelect:     1,
			CollectionId:  "c1",
			CascadeDelete: true, // should delete all c3 records
		},
	)
	if err := app.SaveNoValidate(c3); err != nil {
		return err
	}

	// insert mock records
	c1RecordA := core.NewRecord(c1)
	c1RecordA.Id = "a"
	c1RecordA.Set("rel", c1RecordA.Id) // self reference
	if err := app.SaveNoValidate(c1RecordA); err != nil {
		return err
	}
	c1RecordB := core.NewRecord(c1)
	c1RecordB.Id = "b"
	c1RecordB.Set("rel", c1RecordA.Id) // rel to another record from the same collection
	if err := app.SaveNoValidate(c1RecordB); err != nil {
		return err
	}
	for i := 0; i < 4500; i++ {
		c2Record := core.NewRecord(c2)
		c2Record.Set("rel", []string{c1RecordA.Id, c1RecordB.Id})
		if err := app.SaveNoValidate(c2Record); err != nil {
			return err
		}

		c3Record := core.NewRecord(c3)
		c3Record.Set("rel", c1RecordA.Id)
		if err := app.SaveNoValidate(c3Record); err != nil {
			return err
		}
	}

	// set the same id as the relation for at least 1 record
	// to check whether the correct condition will be added
	c3Record := core.NewRecord(c3)
	c3Record.Set("rel", c1RecordA.Id)
	c3Record.Id = c1RecordA.Id
	if err := app.SaveNoValidate(c3Record); err != nil {
		return err
	}

	return nil
}

// -------------------------------------------------------------------

type mockField struct {
	core.TextField
}

func (f *mockField) FindGetter(key string) core.GetterFunc {
	switch key {
	case f.Name + ":test":
		return func(record *core.Record) any {
			return "modifier_get"
		}
	default:
		return nil
	}
}

func (f *mockField) FindSetter(key string) core.SetterFunc {
	switch key {
	case f.Name:
		return func(record *core.Record, raw any) {
			record.SetRaw(f.Name, cast.ToString(raw))
		}
	case f.Name + ":test":
		return func(record *core.Record, raw any) {
			record.SetRaw(f.Name, "modifier_set")
		}
	default:
		return nil
	}
}