package schema_test

import (
	"encoding/json"
	"fmt"
	"testing"
	"time"

	validation "github.com/go-ozzo/ozzo-validation/v4"
	"github.com/pocketbase/pocketbase/models/schema"
	"github.com/pocketbase/pocketbase/tools/types"
)

func TestBaseModelFieldNames(t *testing.T) {
	result := schema.BaseModelFieldNames()
	expected := 3

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

func TestSystemFieldNames(t *testing.T) {
	result := schema.SystemFieldNames()
	expected := 3

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

func TestAuthFieldNames(t *testing.T) {
	result := schema.AuthFieldNames()
	expected := 8

	if len(result) != expected {
		t.Fatalf("Expected %d auth field names, got %d (%v)", expected, len(result), result)
	}
}

func TestFieldTypes(t *testing.T) {
	result := schema.FieldTypes()
	expected := 11

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

func TestArraybleFieldTypes(t *testing.T) {
	result := schema.ArraybleFieldTypes()
	expected := 3

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

func TestSchemaFieldColDefinition(t *testing.T) {
	scenarios := []struct {
		field    schema.SchemaField
		expected string
	}{
		{
			schema.SchemaField{Type: schema.FieldTypeText, Name: "test"},
			"TEXT DEFAULT '' NOT NULL",
		},
		{
			schema.SchemaField{Type: schema.FieldTypeNumber, Name: "test"},
			"NUMERIC DEFAULT 0 NOT NULL",
		},
		{
			schema.SchemaField{Type: schema.FieldTypeBool, Name: "test"},
			"BOOLEAN DEFAULT FALSE NOT NULL",
		},
		{
			schema.SchemaField{Type: schema.FieldTypeEmail, Name: "test"},
			"TEXT DEFAULT '' NOT NULL",
		},
		{
			schema.SchemaField{Type: schema.FieldTypeUrl, Name: "test"},
			"TEXT DEFAULT '' NOT NULL",
		},
		{
			schema.SchemaField{Type: schema.FieldTypeEditor, Name: "test"},
			"TEXT DEFAULT '' NOT NULL",
		},
		{
			schema.SchemaField{Type: schema.FieldTypeDate, Name: "test"},
			"TEXT DEFAULT '' NOT NULL",
		},
		{
			schema.SchemaField{Type: schema.FieldTypeJson, Name: "test"},
			"JSON DEFAULT NULL",
		},
		{
			schema.SchemaField{Type: schema.FieldTypeSelect, Name: "test"},
			"TEXT DEFAULT '' NOT NULL",
		},
		{
			schema.SchemaField{Type: schema.FieldTypeSelect, Name: "test_multiple", Options: &schema.SelectOptions{MaxSelect: 2}},
			"JSON DEFAULT '[]' NOT NULL",
		},
		{
			schema.SchemaField{Type: schema.FieldTypeFile, Name: "test"},
			"TEXT DEFAULT '' NOT NULL",
		},
		{
			schema.SchemaField{Type: schema.FieldTypeFile, Name: "test_multiple", Options: &schema.FileOptions{MaxSelect: 2}},
			"JSON DEFAULT '[]' NOT NULL",
		},
		{
			schema.SchemaField{Type: schema.FieldTypeRelation, Name: "test", Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}},
			"TEXT DEFAULT '' NOT NULL",
		},
		{
			schema.SchemaField{Type: schema.FieldTypeRelation, Name: "test_multiple", Options: &schema.RelationOptions{MaxSelect: nil}},
			"JSON DEFAULT '[]' NOT NULL",
		},
	}

	for i, s := range scenarios {
		def := s.field.ColDefinition()
		if def != s.expected {
			t.Errorf("(%d) Expected definition %q, got %q", i, s.expected, def)
		}
	}
}

func TestSchemaFieldString(t *testing.T) {
	f := schema.SchemaField{
		Id:       "abc",
		Name:     "test",
		Type:     schema.FieldTypeText,
		Required: true,
		Unique:   false,
		System:   true,
		Options: &schema.TextOptions{
			Pattern: "test",
		},
	}

	result := f.String()
	expected := `{"system":true,"id":"abc","name":"test","type":"text","required":true,"unique":false,"options":{"min":null,"max":null,"pattern":"test"}}`

	if result != expected {
		t.Errorf("Expected \n%v, got \n%v", expected, result)
	}
}

func TestSchemaFieldMarshalJSON(t *testing.T) {
	scenarios := []struct {
		field    schema.SchemaField
		expected string
	}{
		// empty
		{
			schema.SchemaField{},
			`{"system":false,"id":"","name":"","type":"","required":false,"unique":false,"options":null}`,
		},
		// without defined options
		{
			schema.SchemaField{
				Id:       "abc",
				Name:     "test",
				Type:     schema.FieldTypeText,
				Required: true,
				Unique:   false,
				System:   true,
			},
			`{"system":true,"id":"abc","name":"test","type":"text","required":true,"unique":false,"options":{"min":null,"max":null,"pattern":""}}`,
		},
		// with defined options
		{
			schema.SchemaField{
				Name:     "test",
				Type:     schema.FieldTypeText,
				Required: true,
				Unique:   false,
				System:   true,
				Options: &schema.TextOptions{
					Pattern: "test",
				},
			},
			`{"system":true,"id":"","name":"test","type":"text","required":true,"unique":false,"options":{"min":null,"max":null,"pattern":"test"}}`,
		},
	}

	for i, s := range scenarios {
		result, err := s.field.MarshalJSON()
		if err != nil {
			t.Fatalf("(%d) %v", i, err)
		}

		if string(result) != s.expected {
			t.Errorf("(%d), Expected \n%v, got \n%v", i, s.expected, string(result))
		}
	}
}

func TestSchemaFieldUnmarshalJSON(t *testing.T) {
	scenarios := []struct {
		data        []byte
		expectError bool
		expectJson  string
	}{
		{
			nil,
			true,
			`{"system":false,"id":"","name":"","type":"","required":false,"unique":false,"options":null}`,
		},
		{
			[]byte{},
			true,
			`{"system":false,"id":"","name":"","type":"","required":false,"unique":false,"options":null}`,
		},
		{
			[]byte(`{"system": true}`),
			true,
			`{"system":true,"id":"","name":"","type":"","required":false,"unique":false,"options":null}`,
		},
		{
			[]byte(`{"invalid"`),
			true,
			`{"system":false,"id":"","name":"","type":"","required":false,"unique":false,"options":null}`,
		},
		{
			[]byte(`{"type":"text","system":true}`),
			false,
			`{"system":true,"id":"","name":"","type":"text","required":false,"unique":false,"options":{"min":null,"max":null,"pattern":""}}`,
		},
		{
			[]byte(`{"type":"text","options":{"pattern":"test"}}`),
			false,
			`{"system":false,"id":"","name":"","type":"text","required":false,"unique":false,"options":{"min":null,"max":null,"pattern":"test"}}`,
		},
	}

	for i, s := range scenarios {
		f := schema.SchemaField{}
		err := f.UnmarshalJSON(s.data)

		hasErr := err != nil
		if hasErr != s.expectError {
			t.Errorf("(%d) Expected hasErr %v, got %v (%v)", i, s.expectError, hasErr, err)
		}

		if f.String() != s.expectJson {
			t.Errorf("(%d), Expected json \n%v, got \n%v", i, s.expectJson, f.String())
		}
	}
}

func TestSchemaFieldValidate(t *testing.T) {
	scenarios := []struct {
		name           string
		field          schema.SchemaField
		expectedErrors []string
	}{
		{
			"empty field",
			schema.SchemaField{},
			[]string{"id", "options", "name", "type"},
		},
		{
			"missing id",
			schema.SchemaField{
				Type: schema.FieldTypeText,
				Id:   "",
				Name: "test",
			},
			[]string{"id"},
		},
		{
			"invalid id length check",
			schema.SchemaField{
				Type: schema.FieldTypeText,
				Id:   "1234",
				Name: "test",
			},
			[]string{"id"},
		},
		{
			"valid id length check",
			schema.SchemaField{
				Type: schema.FieldTypeText,
				Id:   "12345",
				Name: "test",
			},
			[]string{},
		},
		{
			"invalid name format",
			schema.SchemaField{
				Type: schema.FieldTypeText,
				Id:   "1234567890",
				Name: "test!@#",
			},
			[]string{"name"},
		},
		{
			"reserved name (null)",
			schema.SchemaField{
				Type: schema.FieldTypeText,
				Id:   "1234567890",
				Name: "null",
			},
			[]string{"name"},
		},
		{
			"reserved name (true)",
			schema.SchemaField{
				Type: schema.FieldTypeText,
				Id:   "1234567890",
				Name: "null",
			},
			[]string{"name"},
		},
		{
			"reserved name (false)",
			schema.SchemaField{
				Type: schema.FieldTypeText,
				Id:   "1234567890",
				Name: "false",
			},
			[]string{"name"},
		},
		{
			"reserved name (_rowid_)",
			schema.SchemaField{
				Type: schema.FieldTypeText,
				Id:   "1234567890",
				Name: "_rowid_",
			},
			[]string{"name"},
		},
		{
			"reserved name (id)",
			schema.SchemaField{
				Type: schema.FieldTypeText,
				Id:   "1234567890",
				Name: schema.FieldNameId,
			},
			[]string{"name"},
		},
		{
			"reserved name (created)",
			schema.SchemaField{
				Type: schema.FieldTypeText,
				Id:   "1234567890",
				Name: schema.FieldNameCreated,
			},
			[]string{"name"},
		},
		{
			"reserved name (updated)",
			schema.SchemaField{
				Type: schema.FieldTypeText,
				Id:   "1234567890",
				Name: schema.FieldNameUpdated,
			},
			[]string{"name"},
		},
		{
			"reserved name (collectionId)",
			schema.SchemaField{
				Type: schema.FieldTypeText,
				Id:   "1234567890",
				Name: schema.FieldNameCollectionId,
			},
			[]string{"name"},
		},
		{
			"reserved name (collectionName)",
			schema.SchemaField{
				Type: schema.FieldTypeText,
				Id:   "1234567890",
				Name: schema.FieldNameCollectionName,
			},
			[]string{"name"},
		},
		{
			"reserved name (expand)",
			schema.SchemaField{
				Type: schema.FieldTypeText,
				Id:   "1234567890",
				Name: schema.FieldNameExpand,
			},
			[]string{"name"},
		},
		{
			"valid name",
			schema.SchemaField{
				Type: schema.FieldTypeText,
				Id:   "1234567890",
				Name: "test",
			},
			[]string{},
		},
		{
			"unique check for type file",
			schema.SchemaField{
				Type:    schema.FieldTypeFile,
				Id:      "1234567890",
				Name:    "test",
				Unique:  true,
				Options: &schema.FileOptions{MaxSelect: 1, MaxSize: 1},
			},
			[]string{"unique"},
		},
		{
			"trigger options validator (auto init)",
			schema.SchemaField{
				Type: schema.FieldTypeFile,
				Id:   "1234567890",
				Name: "test",
			},
			[]string{"options"},
		},
		{
			"trigger options validator (invalid option field value)",
			schema.SchemaField{
				Type:    schema.FieldTypeFile,
				Id:      "1234567890",
				Name:    "test",
				Options: &schema.FileOptions{MaxSelect: 0, MaxSize: 0},
			},
			[]string{"options"},
		},
		{
			"trigger options validator (valid option field value)",
			schema.SchemaField{
				Type:    schema.FieldTypeFile,
				Id:      "1234567890",
				Name:    "test",
				Options: &schema.FileOptions{MaxSelect: 1, MaxSize: 1},
			},
			[]string{},
		},
	}

	for _, s := range scenarios {
		result := s.field.Validate()

		// parse errors
		errs, ok := result.(validation.Errors)
		if !ok && result != nil {
			t.Errorf("[%s] Failed to parse errors %v", s.name, result)
			continue
		}

		// check errors
		if len(errs) > len(s.expectedErrors) {
			t.Errorf("[%s] Expected error keys %v, got %v", s.name, s.expectedErrors, errs)
		}
		for _, k := range s.expectedErrors {
			if _, ok := errs[k]; !ok {
				t.Errorf("[%s] Missing expected error key %q in %v", s.name, k, errs)
			}
		}
	}
}

func TestSchemaFieldInitOptions(t *testing.T) {
	scenarios := []struct {
		field       schema.SchemaField
		expectError bool
		expectJson  string
	}{
		{
			schema.SchemaField{},
			true,
			`{"system":false,"id":"","name":"","type":"","required":false,"unique":false,"options":null}`,
		},
		{
			schema.SchemaField{Type: "unknown"},
			true,
			`{"system":false,"id":"","name":"","type":"unknown","required":false,"unique":false,"options":null}`,
		},
		{
			schema.SchemaField{Type: schema.FieldTypeText},
			false,
			`{"system":false,"id":"","name":"","type":"text","required":false,"unique":false,"options":{"min":null,"max":null,"pattern":""}}`,
		},
		{
			schema.SchemaField{Type: schema.FieldTypeNumber},
			false,
			`{"system":false,"id":"","name":"","type":"number","required":false,"unique":false,"options":{"min":null,"max":null}}`,
		},
		{
			schema.SchemaField{Type: schema.FieldTypeBool},
			false,
			`{"system":false,"id":"","name":"","type":"bool","required":false,"unique":false,"options":{}}`,
		},
		{
			schema.SchemaField{Type: schema.FieldTypeEmail},
			false,
			`{"system":false,"id":"","name":"","type":"email","required":false,"unique":false,"options":{"exceptDomains":null,"onlyDomains":null}}`,
		},
		{
			schema.SchemaField{Type: schema.FieldTypeUrl},
			false,
			`{"system":false,"id":"","name":"","type":"url","required":false,"unique":false,"options":{"exceptDomains":null,"onlyDomains":null}}`,
		},
		{
			schema.SchemaField{Type: schema.FieldTypeEditor},
			false,
			`{"system":false,"id":"","name":"","type":"editor","required":false,"unique":false,"options":{}}`,
		},
		{
			schema.SchemaField{Type: schema.FieldTypeDate},
			false,
			`{"system":false,"id":"","name":"","type":"date","required":false,"unique":false,"options":{"min":"","max":""}}`,
		},
		{
			schema.SchemaField{Type: schema.FieldTypeSelect},
			false,
			`{"system":false,"id":"","name":"","type":"select","required":false,"unique":false,"options":{"maxSelect":0,"values":null}}`,
		},
		{
			schema.SchemaField{Type: schema.FieldTypeJson},
			false,
			`{"system":false,"id":"","name":"","type":"json","required":false,"unique":false,"options":{}}`,
		},
		{
			schema.SchemaField{Type: schema.FieldTypeFile},
			false,
			`{"system":false,"id":"","name":"","type":"file","required":false,"unique":false,"options":{"maxSelect":0,"maxSize":0,"mimeTypes":null,"thumbs":null,"protected":false}}`,
		},
		{
			schema.SchemaField{Type: schema.FieldTypeRelation},
			false,
			`{"system":false,"id":"","name":"","type":"relation","required":false,"unique":false,"options":{"collectionId":"","cascadeDelete":false,"minSelect":null,"maxSelect":null,"displayFields":null}}`,
		},
		{
			schema.SchemaField{Type: schema.FieldTypeUser},
			false,
			`{"system":false,"id":"","name":"","type":"user","required":false,"unique":false,"options":{"maxSelect":0,"cascadeDelete":false}}`,
		},
		{
			schema.SchemaField{
				Type:    schema.FieldTypeText,
				Options: &schema.TextOptions{Pattern: "test"},
			},
			false,
			`{"system":false,"id":"","name":"","type":"text","required":false,"unique":false,"options":{"min":null,"max":null,"pattern":"test"}}`,
		},
	}

	for i, s := range scenarios {
		err := s.field.InitOptions()

		hasErr := err != nil
		if hasErr != s.expectError {
			t.Errorf("(%d) Expected %v, got %v (%v)", i, s.expectError, hasErr, err)
		}

		if s.field.String() != s.expectJson {
			t.Errorf("(%d), Expected %v, got %v", i, s.expectJson, s.field.String())
		}
	}
}

func TestSchemaFieldPrepareValue(t *testing.T) {
	scenarios := []struct {
		field      schema.SchemaField
		value      any
		expectJson string
	}{
		{schema.SchemaField{Type: "unknown"}, "test", `"test"`},
		{schema.SchemaField{Type: "unknown"}, 123, "123"},
		{schema.SchemaField{Type: "unknown"}, []int{1, 2, 1}, "[1,2,1]"},

		// text
		{schema.SchemaField{Type: schema.FieldTypeText}, nil, `""`},
		{schema.SchemaField{Type: schema.FieldTypeText}, "", `""`},
		{schema.SchemaField{Type: schema.FieldTypeText}, []int{1, 2}, `""`},
		{schema.SchemaField{Type: schema.FieldTypeText}, "test", `"test"`},
		{schema.SchemaField{Type: schema.FieldTypeText}, 123, `"123"`},

		// email
		{schema.SchemaField{Type: schema.FieldTypeEmail}, nil, `""`},
		{schema.SchemaField{Type: schema.FieldTypeEmail}, "", `""`},
		{schema.SchemaField{Type: schema.FieldTypeEmail}, []int{1, 2}, `""`},
		{schema.SchemaField{Type: schema.FieldTypeEmail}, "test", `"test"`},
		{schema.SchemaField{Type: schema.FieldTypeEmail}, 123, `"123"`},

		// url
		{schema.SchemaField{Type: schema.FieldTypeUrl}, nil, `""`},
		{schema.SchemaField{Type: schema.FieldTypeUrl}, "", `""`},
		{schema.SchemaField{Type: schema.FieldTypeUrl}, []int{1, 2}, `""`},
		{schema.SchemaField{Type: schema.FieldTypeUrl}, "test", `"test"`},
		{schema.SchemaField{Type: schema.FieldTypeUrl}, 123, `"123"`},

		// editor
		{schema.SchemaField{Type: schema.FieldTypeEditor}, nil, `""`},
		{schema.SchemaField{Type: schema.FieldTypeEditor}, "", `""`},
		{schema.SchemaField{Type: schema.FieldTypeEditor}, []int{1, 2}, `""`},
		{schema.SchemaField{Type: schema.FieldTypeEditor}, "test", `"test"`},
		{schema.SchemaField{Type: schema.FieldTypeEditor}, 123, `"123"`},

		// json
		{schema.SchemaField{Type: schema.FieldTypeJson}, nil, "null"},
		{schema.SchemaField{Type: schema.FieldTypeJson}, "null", "null"},
		{schema.SchemaField{Type: schema.FieldTypeJson}, 123, "123"},
		{schema.SchemaField{Type: schema.FieldTypeJson}, "123", "123"},
		{schema.SchemaField{Type: schema.FieldTypeJson}, 123.456, "123.456"},
		{schema.SchemaField{Type: schema.FieldTypeJson}, "123.456", "123.456"},
		{schema.SchemaField{Type: schema.FieldTypeJson}, "123.456 abc", `"123.456 abc"`}, // invalid numeric string
		{schema.SchemaField{Type: schema.FieldTypeJson}, true, "true"},
		{schema.SchemaField{Type: schema.FieldTypeJson}, "true", "true"},
		{schema.SchemaField{Type: schema.FieldTypeJson}, false, "false"},
		{schema.SchemaField{Type: schema.FieldTypeJson}, "false", "false"},
		{schema.SchemaField{Type: schema.FieldTypeJson}, "", `""`},
		{schema.SchemaField{Type: schema.FieldTypeJson}, `test`, `"test"`},
		{schema.SchemaField{Type: schema.FieldTypeJson}, `"test"`, `"test"`},
		{schema.SchemaField{Type: schema.FieldTypeJson}, `{test":1}`, `"{test\":1}"`}, // invalid object string
		{schema.SchemaField{Type: schema.FieldTypeJson}, `[1 2 3]`, `"[1 2 3]"`},      // invalid array string
		{schema.SchemaField{Type: schema.FieldTypeJson}, map[string]int{}, `{}`},
		{schema.SchemaField{Type: schema.FieldTypeJson}, `{}`, `{}`},
		{schema.SchemaField{Type: schema.FieldTypeJson}, map[string]int{"test": 123}, `{"test":123}`},
		{schema.SchemaField{Type: schema.FieldTypeJson}, `{"test":123}`, `{"test":123}`},
		{schema.SchemaField{Type: schema.FieldTypeJson}, []int{}, `[]`},
		{schema.SchemaField{Type: schema.FieldTypeJson}, `[]`, `[]`},
		{schema.SchemaField{Type: schema.FieldTypeJson}, []int{1, 2, 1}, `[1,2,1]`},
		{schema.SchemaField{Type: schema.FieldTypeJson}, `[1,2,1]`, `[1,2,1]`},

		// number
		{schema.SchemaField{Type: schema.FieldTypeNumber}, nil, "0"},
		{schema.SchemaField{Type: schema.FieldTypeNumber}, "", "0"},
		{schema.SchemaField{Type: schema.FieldTypeNumber}, "test", "0"},
		{schema.SchemaField{Type: schema.FieldTypeNumber}, 1, "1"},
		{schema.SchemaField{Type: schema.FieldTypeNumber}, 1.5, "1.5"},
		{schema.SchemaField{Type: schema.FieldTypeNumber}, "1.5", "1.5"},

		// bool
		{schema.SchemaField{Type: schema.FieldTypeBool}, nil, "false"},
		{schema.SchemaField{Type: schema.FieldTypeBool}, 1, "true"},
		{schema.SchemaField{Type: schema.FieldTypeBool}, 0, "false"},
		{schema.SchemaField{Type: schema.FieldTypeBool}, "", "false"},
		{schema.SchemaField{Type: schema.FieldTypeBool}, "test", "false"},
		{schema.SchemaField{Type: schema.FieldTypeBool}, "false", "false"},
		{schema.SchemaField{Type: schema.FieldTypeBool}, "true", "true"},
		{schema.SchemaField{Type: schema.FieldTypeBool}, false, "false"},
		{schema.SchemaField{Type: schema.FieldTypeBool}, true, "true"},

		// date
		{schema.SchemaField{Type: schema.FieldTypeDate}, nil, `""`},
		{schema.SchemaField{Type: schema.FieldTypeDate}, "", `""`},
		{schema.SchemaField{Type: schema.FieldTypeDate}, "test", `""`},
		{schema.SchemaField{Type: schema.FieldTypeDate}, 1641024040, `"2022-01-01 08:00:40.000Z"`},
		{schema.SchemaField{Type: schema.FieldTypeDate}, "2022-01-01 11:27:10.123", `"2022-01-01 11:27:10.123Z"`},
		{schema.SchemaField{Type: schema.FieldTypeDate}, "2022-01-01 11:27:10.123Z", `"2022-01-01 11:27:10.123Z"`},
		{schema.SchemaField{Type: schema.FieldTypeDate}, types.DateTime{}, `""`},
		{schema.SchemaField{Type: schema.FieldTypeDate}, time.Time{}, `""`},

		// select (single)
		{schema.SchemaField{Type: schema.FieldTypeSelect}, nil, `""`},
		{schema.SchemaField{Type: schema.FieldTypeSelect}, "", `""`},
		{schema.SchemaField{Type: schema.FieldTypeSelect}, 123, `"123"`},
		{schema.SchemaField{Type: schema.FieldTypeSelect}, "test", `"test"`},
		{schema.SchemaField{Type: schema.FieldTypeSelect}, []string{"test1", "test2"}, `"test2"`},
		{
			// no values validation/filtering
			schema.SchemaField{
				Type: schema.FieldTypeSelect,
				Options: &schema.SelectOptions{
					Values: []string{"test1", "test2"},
				},
			},
			"test",
			`"test"`,
		},
		// select (multiple)
		{
			schema.SchemaField{
				Type:    schema.FieldTypeSelect,
				Options: &schema.SelectOptions{MaxSelect: 2},
			},
			nil,
			`[]`,
		},
		{
			schema.SchemaField{
				Type:    schema.FieldTypeSelect,
				Options: &schema.SelectOptions{MaxSelect: 2},
			},
			"",
			`[]`,
		},
		{
			schema.SchemaField{
				Type:    schema.FieldTypeSelect,
				Options: &schema.SelectOptions{MaxSelect: 2},
			},
			[]string{},
			`[]`,
		},
		{
			schema.SchemaField{
				Type:    schema.FieldTypeSelect,
				Options: &schema.SelectOptions{MaxSelect: 2},
			},
			123,
			`["123"]`,
		},
		{
			schema.SchemaField{
				Type:    schema.FieldTypeSelect,
				Options: &schema.SelectOptions{MaxSelect: 2},
			},
			"test",
			`["test"]`,
		},
		{
			// no values validation
			schema.SchemaField{
				Type:    schema.FieldTypeSelect,
				Options: &schema.SelectOptions{MaxSelect: 2},
			},
			[]string{"test1", "test2", "test3"},
			`["test1","test2","test3"]`,
		},
		{
			// duplicated values
			schema.SchemaField{
				Type:    schema.FieldTypeSelect,
				Options: &schema.SelectOptions{MaxSelect: 2},
			},
			[]string{"test1", "test2", "test1"},
			`["test1","test2"]`,
		},

		// file (single)
		{schema.SchemaField{Type: schema.FieldTypeFile}, nil, `""`},
		{schema.SchemaField{Type: schema.FieldTypeFile}, "", `""`},
		{schema.SchemaField{Type: schema.FieldTypeFile}, 123, `"123"`},
		{schema.SchemaField{Type: schema.FieldTypeFile}, "test", `"test"`},
		{schema.SchemaField{Type: schema.FieldTypeFile}, []string{"test1", "test2"}, `"test2"`},
		// file (multiple)
		{
			schema.SchemaField{
				Type:    schema.FieldTypeFile,
				Options: &schema.FileOptions{MaxSelect: 2},
			},
			nil,
			`[]`,
		},
		{
			schema.SchemaField{
				Type:    schema.FieldTypeFile,
				Options: &schema.FileOptions{MaxSelect: 2},
			},
			"",
			`[]`,
		},
		{
			schema.SchemaField{
				Type:    schema.FieldTypeFile,
				Options: &schema.FileOptions{MaxSelect: 2},
			},
			[]string{},
			`[]`,
		},
		{
			schema.SchemaField{
				Type:    schema.FieldTypeFile,
				Options: &schema.FileOptions{MaxSelect: 2},
			},
			123,
			`["123"]`,
		},
		{
			schema.SchemaField{
				Type:    schema.FieldTypeFile,
				Options: &schema.FileOptions{MaxSelect: 2},
			},
			"test",
			`["test"]`,
		},
		{
			// no values validation
			schema.SchemaField{
				Type:    schema.FieldTypeFile,
				Options: &schema.FileOptions{MaxSelect: 2},
			},
			[]string{"test1", "test2", "test3"},
			`["test1","test2","test3"]`,
		},
		{
			// duplicated values
			schema.SchemaField{
				Type:    schema.FieldTypeFile,
				Options: &schema.FileOptions{MaxSelect: 2},
			},
			[]string{"test1", "test2", "test1"},
			`["test1","test2"]`,
		},

		// relation (single)
		{
			schema.SchemaField{
				Type:    schema.FieldTypeRelation,
				Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)},
			},
			nil,
			`""`,
		},
		{
			schema.SchemaField{
				Type:    schema.FieldTypeRelation,
				Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)},
			},
			"",
			`""`,
		},
		{
			schema.SchemaField{
				Type:    schema.FieldTypeRelation,
				Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)},
			},
			123,
			`"123"`,
		},
		{
			schema.SchemaField{
				Type:    schema.FieldTypeRelation,
				Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)},
			},
			"abc",
			`"abc"`,
		},
		{
			schema.SchemaField{
				Type:    schema.FieldTypeRelation,
				Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)},
			},
			"1ba88b4f-e9da-42f0-9764-9a55c953e724",
			`"1ba88b4f-e9da-42f0-9764-9a55c953e724"`,
		},
		{
			schema.SchemaField{Type: schema.FieldTypeRelation, Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}},
			[]string{"1ba88b4f-e9da-42f0-9764-9a55c953e724", "2ba88b4f-e9da-42f0-9764-9a55c953e724"},
			`"2ba88b4f-e9da-42f0-9764-9a55c953e724"`,
		},
		// relation (multiple)
		{
			schema.SchemaField{
				Type:    schema.FieldTypeRelation,
				Options: &schema.RelationOptions{MaxSelect: types.Pointer(2)},
			},
			nil,
			`[]`,
		},
		{
			schema.SchemaField{
				Type:    schema.FieldTypeRelation,
				Options: &schema.RelationOptions{MaxSelect: types.Pointer(2)},
			},
			"",
			`[]`,
		},
		{
			schema.SchemaField{
				Type:    schema.FieldTypeRelation,
				Options: &schema.RelationOptions{MaxSelect: types.Pointer(2)},
			},
			[]string{},
			`[]`,
		},
		{
			schema.SchemaField{
				Type:    schema.FieldTypeRelation,
				Options: &schema.RelationOptions{MaxSelect: types.Pointer(2)},
			},
			123,
			`["123"]`,
		},
		{
			schema.SchemaField{
				Type:    schema.FieldTypeRelation,
				Options: &schema.RelationOptions{MaxSelect: types.Pointer(2)},
			},
			[]string{"", "abc"},
			`["abc"]`,
		},
		{
			// no values validation
			schema.SchemaField{
				Type:    schema.FieldTypeRelation,
				Options: &schema.RelationOptions{MaxSelect: types.Pointer(2)},
			},
			[]string{"1ba88b4f-e9da-42f0-9764-9a55c953e724", "2ba88b4f-e9da-42f0-9764-9a55c953e724"},
			`["1ba88b4f-e9da-42f0-9764-9a55c953e724","2ba88b4f-e9da-42f0-9764-9a55c953e724"]`,
		},
		{
			// duplicated values
			schema.SchemaField{
				Type:    schema.FieldTypeRelation,
				Options: &schema.RelationOptions{MaxSelect: types.Pointer(2)},
			},
			[]string{"1ba88b4f-e9da-42f0-9764-9a55c953e724", "2ba88b4f-e9da-42f0-9764-9a55c953e724", "1ba88b4f-e9da-42f0-9764-9a55c953e724"},
			`["1ba88b4f-e9da-42f0-9764-9a55c953e724","2ba88b4f-e9da-42f0-9764-9a55c953e724"]`,
		},
	}

	for i, s := range scenarios {
		result := s.field.PrepareValue(s.value)

		encoded, err := json.Marshal(result)
		if err != nil {
			t.Errorf("(%d) %v", i, err)
			continue
		}

		if string(encoded) != s.expectJson {
			t.Errorf("(%d), Expected %v, got %v", i, s.expectJson, string(encoded))
		}
	}
}

func TestSchemaFieldPrepareValueWithModifier(t *testing.T) {
	scenarios := []struct {
		name          string
		field         schema.SchemaField
		baseValue     any
		modifier      string
		modifierValue any
		expectJson    string
	}{
		// text
		{
			"text with '+' modifier",
			schema.SchemaField{Type: schema.FieldTypeText},
			"base",
			"+",
			"new",
			`"base"`,
		},
		{
			"text with '-' modifier",
			schema.SchemaField{Type: schema.FieldTypeText},
			"base",
			"-",
			"new",
			`"base"`,
		},
		{
			"text with unknown modifier",
			schema.SchemaField{Type: schema.FieldTypeText},
			"base",
			"?",
			"new",
			`"base"`,
		},
		{
			"text cast check",
			schema.SchemaField{Type: schema.FieldTypeText},
			123,
			"?",
			"new",
			`"123"`,
		},

		// number
		{
			"number with '+' modifier",
			schema.SchemaField{Type: schema.FieldTypeNumber},
			1,
			"+",
			4,
			`5`,
		},
		{
			"number with '-' modifier",
			schema.SchemaField{Type: schema.FieldTypeNumber},
			1,
			"-",
			4,
			`-3`,
		},
		{
			"number with unknown modifier",
			schema.SchemaField{Type: schema.FieldTypeNumber},
			"1",
			"?",
			4,
			`1`,
		},
		{
			"number cast check",
			schema.SchemaField{Type: schema.FieldTypeNumber},
			"test",
			"+",
			"4",
			`4`,
		},

		// bool
		{
			"bool with '+' modifier",
			schema.SchemaField{Type: schema.FieldTypeBool},
			true,
			"+",
			false,
			`true`,
		},
		{
			"bool with '-' modifier",
			schema.SchemaField{Type: schema.FieldTypeBool},
			true,
			"-",
			false,
			`true`,
		},
		{
			"bool with unknown modifier",
			schema.SchemaField{Type: schema.FieldTypeBool},
			true,
			"?",
			false,
			`true`,
		},
		{
			"bool cast check",
			schema.SchemaField{Type: schema.FieldTypeBool},
			"true",
			"?",
			false,
			`true`,
		},

		// email
		{
			"email with '+' modifier",
			schema.SchemaField{Type: schema.FieldTypeEmail},
			"base",
			"+",
			"new",
			`"base"`,
		},
		{
			"email with '-' modifier",
			schema.SchemaField{Type: schema.FieldTypeEmail},
			"base",
			"-",
			"new",
			`"base"`,
		},
		{
			"email with unknown modifier",
			schema.SchemaField{Type: schema.FieldTypeEmail},
			"base",
			"?",
			"new",
			`"base"`,
		},
		{
			"email cast check",
			schema.SchemaField{Type: schema.FieldTypeEmail},
			123,
			"?",
			"new",
			`"123"`,
		},

		// url
		{
			"url with '+' modifier",
			schema.SchemaField{Type: schema.FieldTypeUrl},
			"base",
			"+",
			"new",
			`"base"`,
		},
		{
			"url with '-' modifier",
			schema.SchemaField{Type: schema.FieldTypeUrl},
			"base",
			"-",
			"new",
			`"base"`,
		},
		{
			"url with unknown modifier",
			schema.SchemaField{Type: schema.FieldTypeUrl},
			"base",
			"?",
			"new",
			`"base"`,
		},
		{
			"url cast check",
			schema.SchemaField{Type: schema.FieldTypeUrl},
			123,
			"-",
			"new",
			`"123"`,
		},

		// editor
		{
			"editor with '+' modifier",
			schema.SchemaField{Type: schema.FieldTypeEditor},
			"base",
			"+",
			"new",
			`"base"`,
		},
		{
			"editor with '-' modifier",
			schema.SchemaField{Type: schema.FieldTypeEditor},
			"base",
			"-",
			"new",
			`"base"`,
		},
		{
			"editor with unknown modifier",
			schema.SchemaField{Type: schema.FieldTypeEditor},
			"base",
			"?",
			"new",
			`"base"`,
		},
		{
			"editor cast check",
			schema.SchemaField{Type: schema.FieldTypeEditor},
			123,
			"-",
			"new",
			`"123"`,
		},

		// date
		{
			"date with '+' modifier",
			schema.SchemaField{Type: schema.FieldTypeDate},
			"2023-01-01 00:00:00.123",
			"+",
			"2023-02-01 00:00:00.456",
			`"2023-01-01 00:00:00.123Z"`,
		},
		{
			"date with '-' modifier",
			schema.SchemaField{Type: schema.FieldTypeDate},
			"2023-01-01 00:00:00.123Z",
			"-",
			"2023-02-01 00:00:00.456Z",
			`"2023-01-01 00:00:00.123Z"`,
		},
		{
			"date with unknown modifier",
			schema.SchemaField{Type: schema.FieldTypeDate},
			"2023-01-01 00:00:00.123",
			"?",
			"2023-01-01 00:00:00.456",
			`"2023-01-01 00:00:00.123Z"`,
		},
		{
			"date cast check",
			schema.SchemaField{Type: schema.FieldTypeDate},
			1672524000, // 2022-12-31 22:00:00.000Z
			"+",
			100,
			`"2022-12-31 22:00:00.000Z"`,
		},

		// json
		{
			"json with '+' modifier",
			schema.SchemaField{Type: schema.FieldTypeJson},
			10,
			"+",
			5,
			`10`,
		},
		{
			"json with '+' modifier (slice)",
			schema.SchemaField{Type: schema.FieldTypeJson},
			[]string{"a", "b"},
			"+",
			"c",
			`["a","b"]`,
		},
		{
			"json with '-' modifier",
			schema.SchemaField{Type: schema.FieldTypeJson},
			10,
			"-",
			5,
			`10`,
		},
		{
			"json with '-' modifier (slice)",
			schema.SchemaField{Type: schema.FieldTypeJson},
			`["a","b"]`,
			"-",
			"c",
			`["a","b"]`,
		},
		{
			"json with unknown modifier",
			schema.SchemaField{Type: schema.FieldTypeJson},
			`"base"`,
			"?",
			`"new"`,
			`"base"`,
		},

		// single select
		{
			"single select with '+' modifier (empty base)",
			schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 1}},
			"",
			"+",
			"b",
			`"b"`,
		},
		{
			"single select with '+' modifier (nonempty base)",
			schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 1}},
			"a",
			"+",
			"b",
			`"b"`,
		},
		{
			"single select with '-' modifier (empty base)",
			schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 1}},
			"",
			"-",
			"a",
			`""`,
		},
		{
			"single select with '-' modifier (nonempty base and empty modifier value)",
			schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 1}},
			"a",
			"-",
			"",
			`"a"`,
		},
		{
			"single select with '-' modifier (nonempty base and different value)",
			schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 1}},
			"a",
			"-",
			"b",
			`"a"`,
		},
		{
			"single select with '-' modifier (nonempty base and matching value)",
			schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 1}},
			"a",
			"-",
			"a",
			`""`,
		},
		{
			"single select with '-' modifier (nonempty base and matching value in a slice)",
			schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 1}},
			"a",
			"-",
			[]string{"b", "a", "c", "123"},
			`""`,
		},
		{
			"single select with unknown modifier (nonempty)",
			schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 1}},
			"",
			"?",
			"a",
			`""`,
		},

		// multi select
		{
			"multi select with '+' modifier (empty base)",
			schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 10}},
			nil,
			"+",
			"b",
			`["b"]`,
		},
		{
			"multi select with '+' modifier (nonempty base)",
			schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 10}},
			[]string{"a"},
			"+",
			[]string{"b", "c"},
			`["a","b","c"]`,
		},
		{
			"multi select with '+' modifier (nonempty base; already existing value)",
			schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 10}},
			[]string{"a", "b"},
			"+",
			"b",
			`["a","b"]`,
		},
		{
			"multi select with '-' modifier (empty base)",
			schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 10}},
			nil,
			"-",
			[]string{"a"},
			`[]`,
		},
		{
			"multi select with '-' modifier (nonempty base and empty modifier value)",
			schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 10}},
			"a",
			"-",
			"",
			`["a"]`,
		},
		{
			"multi select with '-' modifier (nonempty base and different value)",
			schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 10}},
			"a",
			"-",
			"b",
			`["a"]`,
		},
		{
			"multi select with '-' modifier (nonempty base and matching value)",
			schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 10}},
			[]string{"a", "b", "c", "d"},
			"-",
			"c",
			`["a","b","d"]`,
		},
		{
			"multi select with '-' modifier (nonempty base and matching value in a slice)",
			schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 10}},
			[]string{"a", "b", "c", "d"},
			"-",
			[]string{"b", "a", "123"},
			`["c","d"]`,
		},
		{
			"multi select with unknown modifier (nonempty)",
			schema.SchemaField{Type: schema.FieldTypeSelect, Options: &schema.SelectOptions{MaxSelect: 10}},
			[]string{"a", "b"},
			"?",
			"a",
			`["a","b"]`,
		},

		// single relation
		{
			"single relation with '+' modifier (empty base)",
			schema.SchemaField{Type: schema.FieldTypeRelation, Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}},
			"",
			"+",
			"b",
			`"b"`,
		},
		{
			"single relation with '+' modifier (nonempty base)",
			schema.SchemaField{Type: schema.FieldTypeRelation, Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}},
			"a",
			"+",
			"b",
			`"b"`,
		},
		{
			"single relation with '-' modifier (empty base)",
			schema.SchemaField{Type: schema.FieldTypeRelation, Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}},
			"",
			"-",
			"a",
			`""`,
		},
		{
			"single relation with '-' modifier (nonempty base and empty modifier value)",
			schema.SchemaField{Type: schema.FieldTypeRelation, Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}},
			"a",
			"-",
			"",
			`"a"`,
		},
		{
			"single relation with '-' modifier (nonempty base and different value)",
			schema.SchemaField{Type: schema.FieldTypeRelation, Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}},
			"a",
			"-",
			"b",
			`"a"`,
		},
		{
			"single relation with '-' modifier (nonempty base and matching value)",
			schema.SchemaField{Type: schema.FieldTypeRelation, Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}},
			"a",
			"-",
			"a",
			`""`,
		},
		{
			"single relation with '-' modifier (nonempty base and matching value in a slice)",
			schema.SchemaField{Type: schema.FieldTypeRelation, Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}},
			"a",
			"-",
			[]string{"b", "a", "c", "123"},
			`""`,
		},
		{
			"single relation with unknown modifier (nonempty)",
			schema.SchemaField{Type: schema.FieldTypeRelation, Options: &schema.RelationOptions{MaxSelect: types.Pointer(1)}},
			"",
			"?",
			"a",
			`""`,
		},

		// multi relation
		{
			"multi relation with '+' modifier (empty base)",
			schema.SchemaField{Type: schema.FieldTypeRelation},
			nil,
			"+",
			"b",
			`["b"]`,
		},
		{
			"multi relation with '+' modifier (nonempty base)",
			schema.SchemaField{Type: schema.FieldTypeRelation},
			[]string{"a"},
			"+",
			[]string{"b", "c"},
			`["a","b","c"]`,
		},
		{
			"multi relation with '+' modifier (nonempty base; already existing value)",
			schema.SchemaField{Type: schema.FieldTypeRelation},
			[]string{"a", "b"},
			"+",
			"b",
			`["a","b"]`,
		},
		{
			"multi relation with '-' modifier (empty base)",
			schema.SchemaField{Type: schema.FieldTypeRelation},
			nil,
			"-",
			[]string{"a"},
			`[]`,
		},
		{
			"multi relation with '-' modifier (nonempty base and empty modifier value)",
			schema.SchemaField{Type: schema.FieldTypeRelation},
			"a",
			"-",
			"",
			`["a"]`,
		},
		{
			"multi relation with '-' modifier (nonempty base and different value)",
			schema.SchemaField{Type: schema.FieldTypeRelation},
			"a",
			"-",
			"b",
			`["a"]`,
		},
		{
			"multi relation with '-' modifier (nonempty base and matching value)",
			schema.SchemaField{Type: schema.FieldTypeRelation},
			[]string{"a", "b", "c", "d"},
			"-",
			"c",
			`["a","b","d"]`,
		},
		{
			"multi relation with '-' modifier (nonempty base and matching value in a slice)",
			schema.SchemaField{Type: schema.FieldTypeRelation},
			[]string{"a", "b", "c", "d"},
			"-",
			[]string{"b", "a", "123"},
			`["c","d"]`,
		},
		{
			"multi relation with unknown modifier (nonempty)",
			schema.SchemaField{Type: schema.FieldTypeRelation},
			[]string{"a", "b"},
			"?",
			"a",
			`["a","b"]`,
		},

		// single file
		{
			"single file with '+' modifier (empty base)",
			schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 1}},
			"",
			"+",
			"b",
			`""`,
		},
		{
			"single file with '+' modifier (nonempty base)",
			schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 1}},
			"a",
			"+",
			"b",
			`"a"`,
		},
		{
			"single file with '-' modifier (empty base)",
			schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 1}},
			"",
			"-",
			"a",
			`""`,
		},
		{
			"single file with '-' modifier (nonempty base and empty modifier value)",
			schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 1}},
			"a",
			"-",
			"",
			`"a"`,
		},
		{
			"single file with '-' modifier (nonempty base and different value)",
			schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 1}},
			"a",
			"-",
			"b",
			`"a"`,
		},
		{
			"single file with '-' modifier (nonempty base and matching value)",
			schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 1}},
			"a",
			"-",
			"a",
			`""`,
		},
		{
			"single file with '-' modifier (nonempty base and matching value in a slice)",
			schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 1}},
			"a",
			"-",
			[]string{"b", "a", "c", "123"},
			`""`,
		},
		{
			"single file with unknown modifier (nonempty)",
			schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 1}},
			"",
			"?",
			"a",
			`""`,
		},

		// multi file
		{
			"multi file with '+' modifier (empty base)",
			schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 10}},
			nil,
			"+",
			"b",
			`[]`,
		},
		{
			"multi file with '+' modifier (nonempty base)",
			schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 10}},
			[]string{"a"},
			"+",
			[]string{"b", "c"},
			`["a"]`,
		},
		{
			"multi file with '+' modifier (nonempty base; already existing value)",
			schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 10}},
			[]string{"a", "b"},
			"+",
			"b",
			`["a","b"]`,
		},
		{
			"multi file with '-' modifier (empty base)",
			schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 10}},
			nil,
			"-",
			[]string{"a"},
			`[]`,
		},
		{
			"multi file with '-' modifier (nonempty base and empty modifier value)",
			schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 10}},
			"a",
			"-",
			"",
			`["a"]`,
		},
		{
			"multi file with '-' modifier (nonempty base and different value)",
			schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 10}},
			"a",
			"-",
			"b",
			`["a"]`,
		},
		{
			"multi file with '-' modifier (nonempty base and matching value)",
			schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 10}},
			[]string{"a", "b", "c", "d"},
			"-",
			"c",
			`["a","b","d"]`,
		},
		{
			"multi file with '-' modifier (nonempty base and matching value in a slice)",
			schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 10}},
			[]string{"a", "b", "c", "d"},
			"-",
			[]string{"b", "a", "123"},
			`["c","d"]`,
		},
		{
			"multi file with unknown modifier (nonempty)",
			schema.SchemaField{Type: schema.FieldTypeFile, Options: &schema.FileOptions{MaxSelect: 10}},
			[]string{"a", "b"},
			"?",
			"a",
			`["a","b"]`,
		},
	}

	for _, s := range scenarios {
		result := s.field.PrepareValueWithModifier(s.baseValue, s.modifier, s.modifierValue)

		encoded, err := json.Marshal(result)
		if err != nil {
			t.Fatalf("[%s] %v", s.name, err)
		}

		if string(encoded) != s.expectJson {
			t.Fatalf("[%s], Expected %v, got %v", s.name, s.expectJson, string(encoded))
		}
	}
}

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

type fieldOptionsScenario struct {
	name           string
	options        schema.FieldOptions
	expectedErrors []string
}

func checkFieldOptionsScenarios(t *testing.T, scenarios []fieldOptionsScenario) {
	for i, s := range scenarios {
		result := s.options.Validate()

		prefix := fmt.Sprintf("%d", i)
		if s.name != "" {
			prefix = s.name
		}

		// parse errors
		errs, ok := result.(validation.Errors)
		if !ok && result != nil {
			t.Errorf("[%s] Failed to parse errors %v", prefix, result)
			continue
		}

		// check errors
		if len(errs) > len(s.expectedErrors) {
			t.Errorf("[%s] Expected error keys %v, got %v", prefix, s.expectedErrors, errs)
		}
		for _, k := range s.expectedErrors {
			if _, ok := errs[k]; !ok {
				t.Errorf("[%s] Missing expected error key %q in %v", prefix, k, errs)
			}
		}
	}
}

func TestTextOptionsValidate(t *testing.T) {
	minus := -1
	number0 := 0
	number1 := 10
	number2 := 20
	scenarios := []fieldOptionsScenario{
		{
			"empty",
			schema.TextOptions{},
			[]string{},
		},
		{
			"min - failure",
			schema.TextOptions{
				Min: &minus,
			},
			[]string{"min"},
		},
		{
			"min - success",
			schema.TextOptions{
				Min: &number0,
			},
			[]string{},
		},
		{
			"max - failure without min",
			schema.TextOptions{
				Max: &minus,
			},
			[]string{"max"},
		},
		{
			"max - failure with min",
			schema.TextOptions{
				Min: &number2,
				Max: &number1,
			},
			[]string{"max"},
		},
		{
			"max - success",
			schema.TextOptions{
				Min: &number1,
				Max: &number2,
			},
			[]string{},
		},
		{
			"pattern - failure",
			schema.TextOptions{Pattern: "(test"},
			[]string{"pattern"},
		},
		{
			"pattern - success",
			schema.TextOptions{Pattern: `^\#?\w+$`},
			[]string{},
		},
	}

	checkFieldOptionsScenarios(t, scenarios)
}

func TestNumberOptionsValidate(t *testing.T) {
	number1 := 10.0
	number2 := 20.0
	scenarios := []fieldOptionsScenario{
		{
			"empty",
			schema.NumberOptions{},
			[]string{},
		},
		{
			"max - without min",
			schema.NumberOptions{
				Max: &number1,
			},
			[]string{},
		},
		{
			"max - failure with min",
			schema.NumberOptions{
				Min: &number2,
				Max: &number1,
			},
			[]string{"max"},
		},
		{
			"max - success with min",
			schema.NumberOptions{
				Min: &number1,
				Max: &number2,
			},
			[]string{},
		},
	}

	checkFieldOptionsScenarios(t, scenarios)
}

func TestBoolOptionsValidate(t *testing.T) {
	scenarios := []fieldOptionsScenario{
		{
			"empty",
			schema.BoolOptions{},
			[]string{},
		},
	}

	checkFieldOptionsScenarios(t, scenarios)
}

func TestEmailOptionsValidate(t *testing.T) {
	scenarios := []fieldOptionsScenario{
		{
			"empty",
			schema.EmailOptions{},
			[]string{},
		},
		{
			"ExceptDomains failure",
			schema.EmailOptions{
				ExceptDomains: []string{"invalid"},
			},
			[]string{"exceptDomains"},
		},
		{
			"ExceptDomains success",
			schema.EmailOptions{
				ExceptDomains: []string{"example.com", "sub.example.com"},
			},
			[]string{},
		},
		{
			"OnlyDomains check",
			schema.EmailOptions{
				OnlyDomains: []string{"invalid"},
			},
			[]string{"onlyDomains"},
		},
		{
			"OnlyDomains success",
			schema.EmailOptions{
				OnlyDomains: []string{"example.com", "sub.example.com"},
			},
			[]string{},
		},
		{
			"OnlyDomains + ExceptDomains at the same time",
			schema.EmailOptions{
				ExceptDomains: []string{"test1.com"},
				OnlyDomains:   []string{"test2.com"},
			},
			[]string{"exceptDomains", "onlyDomains"},
		},
	}

	checkFieldOptionsScenarios(t, scenarios)
}

func TestUrlOptionsValidate(t *testing.T) {
	scenarios := []fieldOptionsScenario{
		{
			"empty",
			schema.UrlOptions{},
			[]string{},
		},
		{
			"ExceptDomains failure",
			schema.UrlOptions{
				ExceptDomains: []string{"invalid"},
			},
			[]string{"exceptDomains"},
		},
		{
			"ExceptDomains success",
			schema.UrlOptions{
				ExceptDomains: []string{"example.com", "sub.example.com"},
			},
			[]string{},
		},
		{
			"OnlyDomains check",
			schema.UrlOptions{
				OnlyDomains: []string{"invalid"},
			},
			[]string{"onlyDomains"},
		},
		{
			"OnlyDomains success",
			schema.UrlOptions{
				OnlyDomains: []string{"example.com", "sub.example.com"},
			},
			[]string{},
		},
		{
			"OnlyDomains + ExceptDomains at the same time",
			schema.UrlOptions{
				ExceptDomains: []string{"test1.com"},
				OnlyDomains:   []string{"test2.com"},
			},
			[]string{"exceptDomains", "onlyDomains"},
		},
	}

	checkFieldOptionsScenarios(t, scenarios)
}

func TestEditorOptionsValidate(t *testing.T) {
	scenarios := []fieldOptionsScenario{
		{
			"empty",
			schema.EditorOptions{},
			[]string{},
		},
	}

	checkFieldOptionsScenarios(t, scenarios)
}

func TestDateOptionsValidate(t *testing.T) {
	date1 := types.NowDateTime()
	date2, _ := types.ParseDateTime(date1.Time().AddDate(1, 0, 0))

	scenarios := []fieldOptionsScenario{
		{
			"empty",
			schema.DateOptions{},
			[]string{},
		},
		{
			"min only",
			schema.DateOptions{
				Min: date1,
			},
			[]string{},
		},
		{
			"max only",
			schema.DateOptions{
				Min: date1,
			},
			[]string{},
		},
		{
			"zero min + max",
			schema.DateOptions{
				Min: types.DateTime{},
				Max: date1,
			},
			[]string{},
		},
		{
			"min + zero max",
			schema.DateOptions{
				Min: date1,
				Max: types.DateTime{},
			},
			[]string{},
		},
		{
			"min > max",
			schema.DateOptions{
				Min: date2,
				Max: date1,
			},
			[]string{"max"},
		},
		{
			"min == max",
			schema.DateOptions{
				Min: date1,
				Max: date1,
			},
			[]string{"max"},
		},
		{
			"min < max",
			schema.DateOptions{
				Min: date1,
				Max: date2,
			},
			[]string{},
		},
	}

	checkFieldOptionsScenarios(t, scenarios)
}

func TestSelectOptionsValidate(t *testing.T) {
	scenarios := []fieldOptionsScenario{
		{
			"empty",
			schema.SelectOptions{},
			[]string{"values", "maxSelect"},
		},
		{
			"MaxSelect <= 0",
			schema.SelectOptions{
				Values:    []string{"test1", "test2"},
				MaxSelect: 0,
			},
			[]string{"maxSelect"},
		},
		{
			"MaxSelect > Values",
			schema.SelectOptions{
				Values:    []string{"test1", "test2"},
				MaxSelect: 3,
			},
			[]string{"maxSelect"},
		},
		{
			"MaxSelect <= Values",
			schema.SelectOptions{
				Values:    []string{"test1", "test2"},
				MaxSelect: 2,
			},
			[]string{},
		},
	}

	checkFieldOptionsScenarios(t, scenarios)
}

func TestSelectOptionsIsMultiple(t *testing.T) {
	scenarios := []struct {
		maxSelect int
		expect    bool
	}{
		{-1, false},
		{0, false},
		{1, false},
		{2, true},
	}

	for i, s := range scenarios {
		opt := schema.SelectOptions{
			MaxSelect: s.maxSelect,
		}

		if v := opt.IsMultiple(); v != s.expect {
			t.Errorf("[%d] Expected %v, got %v", i, s.expect, v)
		}
	}
}

func TestJsonOptionsValidate(t *testing.T) {
	scenarios := []fieldOptionsScenario{
		{
			"empty",
			schema.JsonOptions{},
			[]string{},
		},
	}

	checkFieldOptionsScenarios(t, scenarios)
}

func TestFileOptionsValidate(t *testing.T) {
	scenarios := []fieldOptionsScenario{
		{
			"empty",
			schema.FileOptions{},
			[]string{"maxSelect", "maxSize"},
		},
		{
			"MaxSelect <= 0 && maxSize <= 0",
			schema.FileOptions{
				MaxSize:   0,
				MaxSelect: 0,
			},
			[]string{"maxSelect", "maxSize"},
		},
		{
			"MaxSelect > 0 && maxSize > 0",
			schema.FileOptions{
				MaxSize:   2,
				MaxSelect: 1,
			},
			[]string{},
		},
		{
			"invalid thumbs format",
			schema.FileOptions{
				MaxSize:   1,
				MaxSelect: 2,
				Thumbs:    []string{"100", "200x100"},
			},
			[]string{"thumbs"},
		},
		{
			"invalid thumbs format - zero width and height",
			schema.FileOptions{
				MaxSize:   1,
				MaxSelect: 2,
				Thumbs:    []string{"0x0", "0x0t", "0x0b", "0x0f"},
			},
			[]string{"thumbs"},
		},
		{
			"valid thumbs format",
			schema.FileOptions{
				MaxSize:   1,
				MaxSelect: 2,
				Thumbs: []string{
					"100x100", "200x100", "0x100", "100x0",
					"10x10t", "10x10b", "10x10f",
				},
			},
			[]string{},
		},
	}

	checkFieldOptionsScenarios(t, scenarios)
}

func TestFileOptionsIsMultiple(t *testing.T) {
	scenarios := []struct {
		maxSelect int
		expect    bool
	}{
		{-1, false},
		{0, false},
		{1, false},
		{2, true},
	}

	for i, s := range scenarios {
		opt := schema.FileOptions{
			MaxSelect: s.maxSelect,
		}

		if v := opt.IsMultiple(); v != s.expect {
			t.Errorf("[%d] Expected %v, got %v", i, s.expect, v)
		}
	}
}

func TestRelationOptionsValidate(t *testing.T) {
	scenarios := []fieldOptionsScenario{
		{
			"empty",
			schema.RelationOptions{},
			[]string{"collectionId"},
		},
		{
			"empty CollectionId",
			schema.RelationOptions{
				CollectionId: "",
				MaxSelect:    types.Pointer(1),
			},
			[]string{"collectionId"},
		},
		{
			"MinSelect < 0",
			schema.RelationOptions{
				CollectionId: "abc",
				MinSelect:    types.Pointer(-1),
			},
			[]string{"minSelect"},
		},
		{
			"MinSelect >= 0",
			schema.RelationOptions{
				CollectionId: "abc",
				MinSelect:    types.Pointer(0),
			},
			[]string{},
		},
		{
			"MaxSelect <= 0",
			schema.RelationOptions{
				CollectionId: "abc",
				MaxSelect:    types.Pointer(0),
			},
			[]string{"maxSelect"},
		},
		{
			"MaxSelect > 0 && nonempty CollectionId",
			schema.RelationOptions{
				CollectionId: "abc",
				MaxSelect:    types.Pointer(1),
			},
			[]string{},
		},
		{
			"MinSelect < MaxSelect",
			schema.RelationOptions{
				CollectionId: "abc",
				MinSelect:    nil,
				MaxSelect:    types.Pointer(1),
			},
			[]string{},
		},
		{
			"MinSelect = MaxSelect (non-zero)",
			schema.RelationOptions{
				CollectionId: "abc",
				MinSelect:    types.Pointer(1),
				MaxSelect:    types.Pointer(1),
			},
			[]string{},
		},
		{
			"MinSelect = MaxSelect (both zero)",
			schema.RelationOptions{
				CollectionId: "abc",
				MinSelect:    types.Pointer(0),
				MaxSelect:    types.Pointer(0),
			},
			[]string{"maxSelect"},
		},
		{
			"MinSelect > MaxSelect",
			schema.RelationOptions{
				CollectionId: "abc",
				MinSelect:    types.Pointer(2),
				MaxSelect:    types.Pointer(1),
			},
			[]string{"maxSelect"},
		},
	}

	checkFieldOptionsScenarios(t, scenarios)
}

func TestRelationOptionsIsMultiple(t *testing.T) {
	scenarios := []struct {
		maxSelect *int
		expect    bool
	}{
		{nil, true},
		{types.Pointer(-1), false},
		{types.Pointer(0), false},
		{types.Pointer(1), false},
		{types.Pointer(2), true},
	}

	for i, s := range scenarios {
		opt := schema.RelationOptions{
			MaxSelect: s.maxSelect,
		}

		if v := opt.IsMultiple(); v != s.expect {
			t.Errorf("[%d] Expected %v, got %v", i, s.expect, v)
		}
	}
}
