package core_test

import (
	"encoding/json"
	"fmt"
	"slices"
	"strings"
	"testing"

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

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

	scenarios := []struct {
		typ      string
		name     string
		expected []string
	}{
		{
			"",
			"",
			[]string{
				`"id":""`,
				`"name":""`,
				`"type":"base"`,
				`"system":false`,
				`"indexes":[]`,
				`"fields":[{`,
				`"name":"id"`,
				`"type":"text"`,
				`"listRule":null`,
				`"viewRule":null`,
				`"createRule":null`,
				`"updateRule":null`,
				`"deleteRule":null`,
			},
		},
		{
			"unknown",
			"test",
			[]string{
				`"id":"_pbc_3632233996"`,
				`"name":"test"`,
				`"type":"base"`,
				`"system":false`,
				`"indexes":[]`,
				`"fields":[{`,
				`"name":"id"`,
				`"type":"text"`,
				`"listRule":null`,
				`"viewRule":null`,
				`"createRule":null`,
				`"updateRule":null`,
				`"deleteRule":null`,
			},
		},
		{
			"base",
			"test",
			[]string{
				`"id":"_pbc_3632233996"`,
				`"name":"test"`,
				`"type":"base"`,
				`"system":false`,
				`"indexes":[]`,
				`"fields":[{`,
				`"name":"id"`,
				`"type":"text"`,
				`"listRule":null`,
				`"viewRule":null`,
				`"createRule":null`,
				`"updateRule":null`,
				`"deleteRule":null`,
			},
		},
		{
			"view",
			"test",
			[]string{
				`"id":"_pbc_3632233996"`,
				`"name":"test"`,
				`"type":"view"`,
				`"indexes":[]`,
				`"fields":[]`,
				`"system":false`,
				`"listRule":null`,
				`"viewRule":null`,
				`"createRule":null`,
				`"updateRule":null`,
				`"deleteRule":null`,
			},
		},
		{
			"auth",
			"test",
			[]string{
				`"id":"_pbc_3632233996"`,
				`"name":"test"`,
				`"type":"auth"`,
				`"fields":[{`,
				`"system":false`,
				`"type":"text"`,
				`"type":"email"`,
				`"name":"id"`,
				`"name":"email"`,
				`"name":"password"`,
				`"name":"tokenKey"`,
				`"name":"emailVisibility"`,
				`"name":"verified"`,
				`idx_email`,
				`idx_tokenKey`,
				`"listRule":null`,
				`"viewRule":null`,
				`"createRule":null`,
				`"updateRule":null`,
				`"deleteRule":null`,
				`"identityFields":["email"]`,
			},
		},
	}

	for i, s := range scenarios {
		t.Run(fmt.Sprintf("%d_%s_%s", i, s.typ, s.name), func(t *testing.T) {
			result := core.NewCollection(s.typ, s.name).String()

			for _, part := range s.expected {
				if !strings.Contains(result, part) {
					t.Fatalf("Missing part %q in\n%v", part, result)
				}
			}
		})
	}
}

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

	scenarios := []struct {
		name     string
		expected []string
	}{
		{
			"",
			[]string{
				`"id":""`,
				`"name":""`,
				`"type":"base"`,
				`"system":false`,
				`"indexes":[]`,
				`"fields":[{`,
				`"name":"id"`,
				`"type":"text"`,
				`"listRule":null`,
				`"viewRule":null`,
				`"createRule":null`,
				`"updateRule":null`,
				`"deleteRule":null`,
			},
		},
		{
			"test",
			[]string{
				`"id":"_pbc_3632233996"`,
				`"name":"test"`,
				`"type":"base"`,
				`"system":false`,
				`"indexes":[]`,
				`"fields":[{`,
				`"name":"id"`,
				`"type":"text"`,
				`"listRule":null`,
				`"viewRule":null`,
				`"createRule":null`,
				`"updateRule":null`,
				`"deleteRule":null`,
			},
		},
	}

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

			for _, part := range s.expected {
				if !strings.Contains(result, part) {
					t.Fatalf("Missing part %q in\n%v", part, result)
				}
			}
		})
	}
}

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

	scenarios := []struct {
		name     string
		expected []string
	}{
		{
			"",
			[]string{
				`"id":""`,
				`"name":""`,
				`"type":"view"`,
				`"indexes":[]`,
				`"fields":[]`,
				`"system":false`,
				`"listRule":null`,
				`"viewRule":null`,
				`"createRule":null`,
				`"updateRule":null`,
				`"deleteRule":null`,
			},
		},
		{
			"test",
			[]string{
				`"id":"_pbc_3632233996"`,
				`"name":"test"`,
				`"type":"view"`,
				`"indexes":[]`,
				`"fields":[]`,
				`"system":false`,
				`"listRule":null`,
				`"viewRule":null`,
				`"createRule":null`,
				`"updateRule":null`,
				`"deleteRule":null`,
			},
		},
	}

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

			for _, part := range s.expected {
				if !strings.Contains(result, part) {
					t.Fatalf("Missing part %q in\n%v", part, result)
				}
			}
		})
	}
}

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

	scenarios := []struct {
		name     string
		expected []string
	}{
		{
			"",
			[]string{
				`"id":""`,
				`"name":""`,
				`"type":"auth"`,
				`"fields":[{`,
				`"system":false`,
				`"type":"text"`,
				`"type":"email"`,
				`"name":"id"`,
				`"name":"email"`,
				`"name":"password"`,
				`"name":"tokenKey"`,
				`"name":"emailVisibility"`,
				`"name":"verified"`,
				`idx_email`,
				`idx_tokenKey`,
				`"listRule":null`,
				`"viewRule":null`,
				`"createRule":null`,
				`"updateRule":null`,
				`"deleteRule":null`,
				`"identityFields":["email"]`,
			},
		},
		{
			"test",
			[]string{
				`"id":"_pbc_3632233996"`,
				`"name":"test"`,
				`"type":"auth"`,
				`"fields":[{`,
				`"system":false`,
				`"type":"text"`,
				`"type":"email"`,
				`"name":"id"`,
				`"name":"email"`,
				`"name":"password"`,
				`"name":"tokenKey"`,
				`"name":"emailVisibility"`,
				`"name":"verified"`,
				`idx_email`,
				`idx_tokenKey`,
				`"listRule":null`,
				`"viewRule":null`,
				`"createRule":null`,
				`"updateRule":null`,
				`"deleteRule":null`,
				`"identityFields":["email"]`,
			},
		},
	}

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

			for _, part := range s.expected {
				if !strings.Contains(result, part) {
					t.Fatalf("Missing part %q in\n%v", part, result)
				}
			}
		})
	}
}

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

	c := core.NewBaseCollection("test")
	if c.TableName() != "_collections" {
		t.Fatalf("Expected tableName %q, got %q", "_collections", c.TableName())
	}
}

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

	c := core.Collection{}

	if c.BaseFilesPath() != "" {
		t.Fatalf("Expected empty string, got %q", c.BaseFilesPath())
	}

	c.Id = "test"

	if c.BaseFilesPath() != c.Id {
		t.Fatalf("Expected %q, got %q", c.Id, c.BaseFilesPath())
	}
}

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

	scenarios := []struct {
		typ      string
		expected bool
	}{
		{"unknown", false},
		{core.CollectionTypeBase, true},
		{core.CollectionTypeView, false},
		{core.CollectionTypeAuth, false},
	}

	for _, s := range scenarios {
		t.Run(s.typ, func(t *testing.T) {
			c := core.Collection{}
			c.Type = s.typ

			if v := c.IsBase(); v != s.expected {
				t.Fatalf("Expected %v, got %v", s.expected, v)
			}
		})
	}
}

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

	scenarios := []struct {
		typ      string
		expected bool
	}{
		{"unknown", false},
		{core.CollectionTypeBase, false},
		{core.CollectionTypeView, true},
		{core.CollectionTypeAuth, false},
	}

	for _, s := range scenarios {
		t.Run(s.typ, func(t *testing.T) {
			c := core.Collection{}
			c.Type = s.typ

			if v := c.IsView(); v != s.expected {
				t.Fatalf("Expected %v, got %v", s.expected, v)
			}
		})
	}
}

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

	scenarios := []struct {
		typ      string
		expected bool
	}{
		{"unknown", false},
		{core.CollectionTypeBase, false},
		{core.CollectionTypeView, false},
		{core.CollectionTypeAuth, true},
	}

	for _, s := range scenarios {
		t.Run(s.typ, func(t *testing.T) {
			c := core.Collection{}
			c.Type = s.typ

			if v := c.IsAuth(); v != s.expected {
				t.Fatalf("Expected %v, got %v", s.expected, v)
			}
		})
	}
}

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

	rawOptions := types.JSONRaw(`{
		"viewQuery":"select 1",
		"authRule":"1=2"
	}`)

	scenarios := []struct {
		typ        string
		rawOptions types.JSONRaw
		expected   []string
	}{
		{
			core.CollectionTypeBase,
			rawOptions,
			[]string{
				`lastSavedPK:"test"`,
				`ViewQuery:""`,
				`AuthRule:(*string)(nil)`,
			},
		},
		{
			core.CollectionTypeView,
			rawOptions,
			[]string{
				`lastSavedPK:"test"`,
				`ViewQuery:"select 1"`,
				`AuthRule:(*string)(nil)`,
			},
		},
		{
			core.CollectionTypeAuth,
			rawOptions,
			[]string{
				`lastSavedPK:"test"`,
				`ViewQuery:""`,
				`AuthRule:(*string)(0x`,
			},
		},
	}

	for i, s := range scenarios {
		t.Run(fmt.Sprintf("%d_%s", i, s.typ), func(t *testing.T) {
			c := core.Collection{}
			c.Id = "test"
			c.Type = s.typ
			c.RawOptions = s.rawOptions

			err := c.PostScan()
			if err != nil {
				t.Fatal(err)
			}

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

			rawModel := fmt.Sprintf("%#v", c)

			for _, part := range s.expected {
				if !strings.Contains(rawModel, part) {
					t.Fatalf("Missing part %q in\n%v", part, rawModel)
				}
			}
		})
	}
}

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

	scenarios := []struct {
		name               string
		raw                string
		collection         func() *core.Collection
		expectedCollection func() *core.Collection
	}{
		{
			"base new empty",
			`{"type":"base","name":"test","listRule":"1=2","authRule":"1=3","viewQuery":"abc"}`,
			func() *core.Collection {
				return &core.Collection{}
			},
			func() *core.Collection {
				c := core.NewBaseCollection("test")
				c.ListRule = types.Pointer("1=2")
				c.AuthRule = types.Pointer("1=3")
				c.ViewQuery = "abc"
				return c
			},
		},
		{
			"view new empty",
			`{"type":"view","name":"test","listRule":"1=2","authRule":"1=3","viewQuery":"abc"}`,
			func() *core.Collection {
				return &core.Collection{}
			},
			func() *core.Collection {
				c := core.NewViewCollection("test")
				c.ListRule = types.Pointer("1=2")
				c.AuthRule = types.Pointer("1=3")
				c.ViewQuery = "abc"
				return c
			},
		},
		{
			"auth new empty",
			`{"type":"auth","name":"test","listRule":"1=2","authRule":"1=3","viewQuery":"abc"}`,
			func() *core.Collection {
				return &core.Collection{}
			},
			func() *core.Collection {
				c := core.NewAuthCollection("test")
				c.ListRule = types.Pointer("1=2")
				c.AuthRule = types.Pointer("1=3")
				c.ViewQuery = "abc"
				return c
			},
		},
		{
			"new but with set type (no default fields load)",
			`{"type":"base","name":"test","listRule":"1=2","authRule":"1=3","viewQuery":"abc"}`,
			func() *core.Collection {
				c := &core.Collection{}
				c.Type = core.CollectionTypeBase
				return c
			},
			func() *core.Collection {
				c := &core.Collection{}
				c.Type = core.CollectionTypeBase
				c.Name = "test"
				c.ListRule = types.Pointer("1=2")
				c.AuthRule = types.Pointer("1=3")
				c.ViewQuery = "abc"
				return c
			},
		},
		{
			"existing (no default fields load)",
			`{"type":"auth","name":"test","listRule":"1=2","authRule":"1=3","viewQuery":"abc"}`,
			func() *core.Collection {
				c, _ := app.FindCollectionByNameOrId("demo1")
				return c
			},
			func() *core.Collection {
				c, _ := app.FindCollectionByNameOrId("demo1")
				c.Type = core.CollectionTypeAuth
				c.Name = "test"
				c.ListRule = types.Pointer("1=2")
				c.AuthRule = types.Pointer("1=3")
				c.ViewQuery = "abc"
				return c
			},
		},
	}

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

			err := json.Unmarshal([]byte(s.raw), collection)
			if err != nil {
				t.Fatal(err)
			}

			rawResult, err := json.Marshal(collection)
			if err != nil {
				t.Fatal(err)
			}
			rawResultStr := string(rawResult)

			rawExpected, err := json.Marshal(s.expectedCollection())
			if err != nil {
				t.Fatal(err)
			}
			rawExpectedStr := string(rawExpected)

			if rawResultStr != rawExpectedStr {
				t.Fatalf("Expected collection\n%s\ngot\n%s", rawExpectedStr, rawResultStr)
			}
		})
	}
}

func TestCollectionSerialize(t *testing.T) {
	scenarios := []struct {
		name        string
		collection  func() *core.Collection
		expected    []string
		notExpected []string
	}{
		{
			"base",
			func() *core.Collection {
				c := core.NewCollection(core.CollectionTypeBase, "test")
				c.ViewQuery = "1=1"
				c.OAuth2.Providers = []core.OAuth2ProviderConfig{
					{Name: "test1", ClientId: "test_client_id1", ClientSecret: "test_client_secret1"},
					{Name: "test2", ClientId: "test_client_id2", ClientSecret: "test_client_secret2"},
				}

				return c
			},
			[]string{
				`"id":"_pbc_3632233996"`,
				`"name":"test"`,
				`"type":"base"`,
			},
			[]string{
				"verificationTemplate",
				"manageRule",
				"authRule",
				"secret",
				"oauth2",
				"clientId",
				"clientSecret",
				"viewQuery",
			},
		},
		{
			"view",
			func() *core.Collection {
				c := core.NewCollection(core.CollectionTypeView, "test")
				c.ViewQuery = "1=1"
				c.OAuth2.Providers = []core.OAuth2ProviderConfig{
					{Name: "test1", ClientId: "test_client_id1", ClientSecret: "test_client_secret1"},
					{Name: "test2", ClientId: "test_client_id2", ClientSecret: "test_client_secret2"},
				}

				return c
			},
			[]string{
				`"id":"_pbc_3632233996"`,
				`"name":"test"`,
				`"type":"view"`,
				`"viewQuery":"1=1"`,
			},
			[]string{
				"verificationTemplate",
				"manageRule",
				"authRule",
				"secret",
				"oauth2",
				"clientId",
				"clientSecret",
			},
		},
		{
			"auth",
			func() *core.Collection {
				c := core.NewCollection(core.CollectionTypeAuth, "test")
				c.ViewQuery = "1=1"
				c.OAuth2.Providers = []core.OAuth2ProviderConfig{
					{Name: "test1", ClientId: "test_client_id1", ClientSecret: "test_client_secret1"},
					{Name: "test2", ClientId: "test_client_id2", ClientSecret: "test_client_secret2"},
				}

				return c
			},
			[]string{
				`"id":"_pbc_3632233996"`,
				`"name":"test"`,
				`"type":"auth"`,
				`"oauth2":{`,
				`"providers":[{`,
				`"clientId":"test_client_id1"`,
				`"clientId":"test_client_id2"`,
			},
			[]string{
				"viewQuery",
				"secret",
				"clientSecret",
			},
		},
	}

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

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

			if rawStr != collection.String() {
				t.Fatalf("Expected the same serialization, got\n%v\nVS\n%v", collection.String(), rawStr)
			}

			for _, part := range s.expected {
				if !strings.Contains(rawStr, part) {
					t.Fatalf("Missing part %q in\n%v", part, rawStr)
				}
			}

			for _, part := range s.notExpected {
				if strings.Contains(rawStr, part) {
					t.Fatalf("Didn't expect part %q in\n%v", part, rawStr)
				}
			}
		})
	}
}

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

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

	date, err := types.ParseDateTime("2024-07-01 01:02:03.456Z")
	if err != nil {
		t.Fatal(err)
	}

	scenarios := []struct {
		typ      string
		expected string
	}{
		{
			"unknown",
			`{"createRule":"1=3","created":"2024-07-01 01:02:03.456Z","deleteRule":"1=5","fields":[{"hidden":false,"id":"bool597745380","name":"f1","presentable":false,"required":false,"system":true,"type":"bool"},{"hidden":false,"id":"bool3131674462","name":"f2","presentable":false,"required":true,"system":false,"type":"bool"}],"id":"test_id","indexes":["CREATE INDEX idx1 on test_name(id)","CREATE INDEX idx2 on test_name(id)"],"listRule":"1=1","name":"test_name","options":"{}","system":true,"type":"unknown","updateRule":"1=4","updated":"2024-07-01 01:02:03.456Z","viewRule":"1=7"}`,
		},
		{
			core.CollectionTypeBase,
			`{"createRule":"1=3","created":"2024-07-01 01:02:03.456Z","deleteRule":"1=5","fields":[{"hidden":false,"id":"bool597745380","name":"f1","presentable":false,"required":false,"system":true,"type":"bool"},{"hidden":false,"id":"bool3131674462","name":"f2","presentable":false,"required":true,"system":false,"type":"bool"}],"id":"test_id","indexes":["CREATE INDEX idx1 on test_name(id)","CREATE INDEX idx2 on test_name(id)"],"listRule":"1=1","name":"test_name","options":"{}","system":true,"type":"base","updateRule":"1=4","updated":"2024-07-01 01:02:03.456Z","viewRule":"1=7"}`,
		},
		{
			core.CollectionTypeView,
			`{"createRule":"1=3","created":"2024-07-01 01:02:03.456Z","deleteRule":"1=5","fields":[{"hidden":false,"id":"bool597745380","name":"f1","presentable":false,"required":false,"system":true,"type":"bool"},{"hidden":false,"id":"bool3131674462","name":"f2","presentable":false,"required":true,"system":false,"type":"bool"}],"id":"test_id","indexes":["CREATE INDEX idx1 on test_name(id)","CREATE INDEX idx2 on test_name(id)"],"listRule":"1=1","name":"test_name","options":{"viewQuery":"select 1"},"system":true,"type":"view","updateRule":"1=4","updated":"2024-07-01 01:02:03.456Z","viewRule":"1=7"}`,
		},
		{
			core.CollectionTypeAuth,
			`{"createRule":"1=3","created":"2024-07-01 01:02:03.456Z","deleteRule":"1=5","fields":[{"hidden":false,"id":"bool597745380","name":"f1","presentable":false,"required":false,"system":true,"type":"bool"},{"hidden":false,"id":"bool3131674462","name":"f2","presentable":false,"required":true,"system":false,"type":"bool"}],"id":"test_id","indexes":["CREATE INDEX idx1 on test_name(id)","CREATE INDEX idx2 on test_name(id)"],"listRule":"1=1","name":"test_name","options":{"authRule":null,"manageRule":"1=6","authAlert":{"enabled":false,"emailTemplate":{"subject":"","body":""}},"oauth2":{"providers":null,"mappedFields":{"id":"","name":"","username":"","avatarURL":""},"enabled":false},"passwordAuth":{"enabled":false,"identityFields":null},"mfa":{"enabled":false,"duration":0,"rule":""},"otp":{"enabled":false,"duration":0,"length":0,"emailTemplate":{"subject":"","body":""}},"authToken":{"duration":0},"passwordResetToken":{"duration":0},"emailChangeToken":{"duration":0},"verificationToken":{"duration":0},"fileToken":{"duration":0},"verificationTemplate":{"subject":"","body":""},"resetPasswordTemplate":{"subject":"","body":""},"confirmEmailChangeTemplate":{"subject":"","body":""}},"system":true,"type":"auth","updateRule":"1=4","updated":"2024-07-01 01:02:03.456Z","viewRule":"1=7"}`,
		},
	}

	for i, s := range scenarios {
		t.Run(fmt.Sprintf("%d_%s", i, s.typ), func(t *testing.T) {
			c := core.Collection{}
			c.Type = s.typ
			c.Id = "test_id"
			c.Name = "test_name"
			c.System = true
			c.ListRule = types.Pointer("1=1")
			c.ViewRule = types.Pointer("1=2")
			c.CreateRule = types.Pointer("1=3")
			c.UpdateRule = types.Pointer("1=4")
			c.DeleteRule = types.Pointer("1=5")
			c.ManageRule = types.Pointer("1=6")
			c.ViewRule = types.Pointer("1=7")
			c.Created = date
			c.Updated = date
			c.Indexes = types.JSONArray[string]{"CREATE INDEX idx1 on test_name(id)", "CREATE INDEX idx2 on test_name(id)"}
			c.ViewQuery = "select 1"
			c.Fields.Add(&core.BoolField{Name: "f1", System: true})
			c.Fields.Add(&core.BoolField{Name: "f2", Required: true})
			c.RawOptions = types.JSONRaw(`{"viewQuery": "select 2"}`) // should be ignored

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

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

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

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

	checkIndexes := func(t *testing.T, indexes, expectedIndexes []string) {
		if len(indexes) != len(expectedIndexes) {
			t.Fatalf("Expected %d indexes, got %d\n%v", len(expectedIndexes), len(indexes), indexes)
		}

		for _, idx := range expectedIndexes {
			if !slices.Contains(indexes, idx) {
				t.Fatalf("Missing index\n%v\nin\n%v", idx, indexes)
			}
		}
	}

	c := core.NewBaseCollection("test")
	checkIndexes(t, c.Indexes, nil)

	c.AddIndex("idx1", false, "colA,colB", "colA != 1")
	c.AddIndex("idx2", true, "colA", "")
	c.AddIndex("idx3", false, "colA", "")
	c.AddIndex("idx3", false, "colB", "") // should overwrite the previous one

	idx1 := "CREATE INDEX `idx1` ON `test` (colA,colB) WHERE colA != 1"
	idx2 := "CREATE UNIQUE INDEX `idx2` ON `test` (colA)"
	idx3 := "CREATE INDEX `idx3` ON `test` (colB)"

	checkIndexes(t, c.Indexes, []string{idx1, idx2, idx3})

	c.RemoveIndex("iDx2")    // case-insensitive
	c.RemoveIndex("missing") // noop

	checkIndexes(t, c.Indexes, []string{idx1, idx3})

	expectedIndexes := map[string]string{
		"missing": "",
		"idx1":    idx1,
		// the name is case insensitive
		"iDX3": idx3,
	}
	for key, expectedIdx := range expectedIndexes {
		idx := c.GetIndex(key)
		if idx != expectedIdx {
			t.Errorf("Expected index %q to be\n%v\ngot\n%v", key, expectedIdx, idx)
		}
	}
}

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

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

	scenarios := []struct {
		name                   string
		collection             string
		disableIntegrityChecks bool
		expectError            bool
	}{
		{
			name:        "unsaved",
			collection:  "",
			expectError: true,
		},
		{
			name:        "system",
			collection:  core.CollectionNameSuperusers,
			expectError: true,
		},
		{
			name:        "base with references",
			collection:  "demo1",
			expectError: true,
		},
		{
			name:                   "base with references with disabled integrity checks",
			collection:             "demo1",
			disableIntegrityChecks: true,
			expectError:            false,
		},
		{
			name:        "base without references",
			collection:  "demo1",
			expectError: true,
		},
		{
			name:        "view with reference",
			collection:  "view1",
			expectError: true,
		},
		{
			name:                   "view with references with disabled integrity checks",
			collection:             "view1",
			disableIntegrityChecks: true,
			expectError:            false,
		},
		{
			name:                   "view without references",
			collection:             "view2",
			disableIntegrityChecks: true,
			expectError:            false,
		},
	}

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

			var col *core.Collection

			if s.collection == "" {
				col = core.NewBaseCollection("test")
			} else {
				var err error
				col, err = app.FindCollectionByNameOrId(s.collection)
				if err != nil {
					t.Fatal(err)
				}
			}

			if s.disableIntegrityChecks {
				col.IntegrityChecks(!s.disableIntegrityChecks)
			}

			err := app.Delete(col)

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

			exists := app.HasTable(col.Name)

			if !col.IsNew() && exists != hasErr {
				t.Fatalf("Expected HasTable %v, got %v", hasErr, exists)
			}

			if !hasErr {
				cache, _ := app.FindCachedCollectionByNameOrId(col.Id)
				if cache != nil {
					t.Fatal("Expected the collection to be removed from the cache.")
				}
			}
		})
	}
}

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

	scenarios := []struct {
		name          string
		collection    func(app core.App) (*core.Collection, error)
		expectError   bool
		expectColumns []string
	}{
		// trigger validators
		{
			name: "create - trigger validators",
			collection: func(app core.App) (*core.Collection, error) {
				c := core.NewBaseCollection("!invalid")
				c.Fields.Add(&core.TextField{Name: "example"})
				c.AddIndex("test_save_idx", false, "example", "")
				return c, nil
			},
			expectError: true,
		},
		{
			name: "update - trigger validators",
			collection: func(app core.App) (*core.Collection, error) {
				c, _ := app.FindCollectionByNameOrId("demo5")
				c.Name = "demo1"
				c.Fields.Add(&core.TextField{Name: "example"})
				c.Fields.RemoveByName("file")
				c.AddIndex("test_save_idx", false, "example", "")
				return c, nil
			},
			expectError: true,
		},

		// create
		{
			name: "create base collection",
			collection: func(app core.App) (*core.Collection, error) {
				c := core.NewBaseCollection("new")
				c.Type = ""                 // should be auto set to "base"
				c.Fields.RemoveByName("id") // ensure that the default fields will be loaded
				c.Fields.Add(&core.TextField{Name: "example"})
				c.AddIndex("test_save_idx", false, "example", "")
				return c, nil
			},
			expectError: false,
			expectColumns: []string{
				"id", "example",
			},
		},
		{
			name: "create auth collection",
			collection: func(app core.App) (*core.Collection, error) {
				c := core.NewAuthCollection("new")
				c.Fields.RemoveByName("id")    // ensure that the default fields will be loaded
				c.Fields.RemoveByName("email") // ensure that the default fields will be loaded
				c.Fields.Add(&core.TextField{Name: "example"})
				c.AddIndex("test_save_idx", false, "example", "")
				return c, nil
			},
			expectError: false,
			expectColumns: []string{
				"id", "email", "tokenKey", "password",
				"verified", "emailVisibility", "example",
			},
		},
		{
			name: "create view collection",
			collection: func(app core.App) (*core.Collection, error) {
				c := core.NewViewCollection("new")
				c.Fields.Add(&core.TextField{Name: "ignored"}) // should be ignored
				c.ViewQuery = "select 1 as id, 2 as example"
				return c, nil
			},
			expectError: false,
			expectColumns: []string{
				"id", "example",
			},
		},

		// update
		{
			name: "update base collection",
			collection: func(app core.App) (*core.Collection, error) {
				c, _ := app.FindCollectionByNameOrId("demo5")
				c.Fields.Add(&core.TextField{Name: "example"})
				c.Fields.RemoveByName("file")
				c.Fields.GetByName("total").SetName("total_updated")
				c.AddIndex("test_save_idx", false, "example", "")
				return c, nil
			},
			expectError: false,
			expectColumns: []string{
				"id", "select_one", "select_many", "rel_one", "rel_many",
				"total_updated", "created", "updated", "example",
			},
		},
		{
			name: "update auth collection",
			collection: func(app core.App) (*core.Collection, error) {
				c, _ := app.FindCollectionByNameOrId("clients")
				c.Fields.Add(&core.TextField{Name: "example"})
				c.Fields.RemoveByName("file")
				c.Fields.GetByName("name").SetName("name_updated")
				c.AddIndex("test_save_idx", false, "example", "")
				return c, nil
			},
			expectError: false,
			expectColumns: []string{
				"id", "email", "emailVisibility", "password", "tokenKey",
				"verified", "username", "name_updated", "created", "updated", "example",
			},
		},
		{
			name: "update view collection",
			collection: func(app core.App) (*core.Collection, error) {
				c, _ := app.FindCollectionByNameOrId("view2")
				c.Fields.Add(&core.TextField{Name: "example"}) // should be ignored
				c.ViewQuery = "select 1 as id, 2 as example"
				return c, nil
			},
			expectError: false,
			expectColumns: []string{
				"id", "example",
			},
		},

		// auth normalization
		{
			name: "unset missing oauth2 mapped fields",
			collection: func(app core.App) (*core.Collection, error) {
				c := core.NewAuthCollection("new")
				c.OAuth2.Enabled = true
				// shouldn't fail
				c.OAuth2.MappedFields = core.OAuth2KnownFields{
					Id:        "missing",
					Name:      "missing",
					Username:  "missing",
					AvatarURL: "missing",
				}
				return c, nil
			},
			expectError: false,
			expectColumns: []string{
				"id", "email", "emailVisibility", "password", "tokenKey", "verified",
			},
		},
	}

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

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

			saveErr := app.Save(collection)

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

			if hasErr {
				return
			}

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

			// the timestamp fields should be non-empty after successful Save
			if collection.Created.String() == "" {
				t.Fatal("Expected collection created to be set")
			}
			if collection.Updated.String() == "" {
				t.Fatal("Expected collection updated to be set")
			}

			// check if the records table was synced
			hasTable := app.HasTable(collection.Name)
			if !hasTable {
				t.Fatalf("Expected records table %s to be created", collection.Name)
			}

			// check if the records table has the fields fields
			columns, err := app.TableColumns(collection.Name)
			if err != nil {
				t.Fatal(err)
			}
			if len(columns) != len(s.expectColumns) {
				t.Fatalf("Expected columns\n%v\ngot\n%v", s.expectColumns, columns)
			}
			for i, c := range columns {
				if !slices.Contains(s.expectColumns, c) {
					t.Fatalf("[%d] Didn't expect record column %q", i, c)
				}
			}

			// make sure that all collection indexes exists
			indexes, err := app.TableIndexes(collection.Name)
			if err != nil {
				t.Fatal(err)
			}
			if len(indexes) != len(collection.Indexes) {
				t.Fatalf("Expected %d indexes, got %d", len(collection.Indexes), len(indexes))
			}
			for _, idx := range collection.Indexes {
				parsed := dbutils.ParseIndex(idx)
				if _, ok := indexes[parsed.IndexName]; !ok {
					t.Fatalf("Missing index %q in\n%v", idx, indexes)
				}
			}
		})
	}
}

// indirect update of a field used in view should cause view(s) update
func TestCollectionSaveIndirectViewsUpdate(t *testing.T) {
	t.Parallel()

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

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

	// update MaxSelect fields
	{
		relMany := collection.Fields.GetByName("rel_many").(*core.RelationField)
		relMany.MaxSelect = 1

		fileOne := collection.Fields.GetByName("file_one").(*core.FileField)
		fileOne.MaxSelect = 10

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

	// check view1 fields
	{
		view1, err := app.FindCollectionByNameOrId("view1")
		if err != nil {
			t.Fatal(err)
		}

		relMany := view1.Fields.GetByName("rel_many").(*core.RelationField)
		if relMany.MaxSelect != 1 {
			t.Fatalf("Expected view1.rel_many MaxSelect to be %d, got %v", 1, relMany.MaxSelect)
		}

		fileOne := view1.Fields.GetByName("file_one").(*core.FileField)
		if fileOne.MaxSelect != 10 {
			t.Fatalf("Expected view1.file_one MaxSelect to be %d, got %v", 10, fileOne.MaxSelect)
		}
	}

	// check view2 fields
	{
		view2, err := app.FindCollectionByNameOrId("view2")
		if err != nil {
			t.Fatal(err)
		}

		relMany := view2.Fields.GetByName("rel_many").(*core.RelationField)
		if relMany.MaxSelect != 1 {
			t.Fatalf("Expected view2.rel_many MaxSelect to be %d, got %v", 1, relMany.MaxSelect)
		}
	}
}

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

	viewName := "test_wrapping"

	scenarios := []struct {
		name     string
		query    string
		expected string
	}{
		{
			"no wrapping - text field",
			"select text as id, bool from demo1",
			"CREATE VIEW `test_wrapping` AS SELECT * FROM (select text as id, bool from demo1)",
		},
		{
			"no wrapping - id field",
			"select text as id, bool from demo1",
			"CREATE VIEW `test_wrapping` AS SELECT * FROM (select text as id, bool from demo1)",
		},
		{
			"no wrapping - relation field",
			"select rel_one as id, bool from demo1",
			"CREATE VIEW `test_wrapping` AS SELECT * FROM (select rel_one as id, bool from demo1)",
		},
		{
			"no wrapping - select field",
			"select select_many as id, bool from demo1",
			"CREATE VIEW `test_wrapping` AS SELECT * FROM (select select_many as id, bool from demo1)",
		},
		{
			"no wrapping - email field",
			"select email as id, bool from demo1",
			"CREATE VIEW `test_wrapping` AS SELECT * FROM (select email as id, bool from demo1)",
		},
		{
			"no wrapping - datetime field",
			"select datetime as id, bool from demo1",
			"CREATE VIEW `test_wrapping` AS SELECT * FROM (select datetime as id, bool from demo1)",
		},
		{
			"no wrapping - url field",
			"select url as id, bool from demo1",
			"CREATE VIEW `test_wrapping` AS SELECT * FROM (select url as id, bool from demo1)",
		},
		{
			"wrapping - bool field",
			"select bool as id, text as txt, url from demo1",
			"CREATE VIEW `test_wrapping` AS SELECT * FROM (SELECT CAST(`id` as TEXT) `id`,`txt`,`url` FROM (select bool as id, text as txt, url from demo1))",
		},
		{
			"wrapping - bool field (different order)",
			"select text as txt, url, bool as id from demo1",
			"CREATE VIEW `test_wrapping` AS SELECT * FROM (SELECT `txt`,`url`,CAST(`id` as TEXT) `id` FROM (select text as txt, url, bool as id from demo1))",
		},
		{
			"wrapping - json field",
			"select json as id, text, url from demo1",
			"CREATE VIEW `test_wrapping` AS SELECT * FROM (SELECT CAST(`id` as TEXT) `id`,`text`,`url` FROM (select json as id, text, url from demo1))",
		},
		{
			"wrapping - numeric id",
			"select 1 as id",
			"CREATE VIEW `test_wrapping` AS SELECT * FROM (SELECT CAST(`id` as TEXT) `id` FROM (select 1 as id))",
		},
		{
			"wrapping - expresion",
			"select ('test') as id",
			"CREATE VIEW `test_wrapping` AS SELECT * FROM (SELECT CAST(`id` as TEXT) `id` FROM (select ('test') as id))",
		},
		{
			"no wrapping - cast as text",
			"select cast('test' as text) as id",
			"CREATE VIEW `test_wrapping` AS SELECT * FROM (select cast('test' as text) as id)",
		},
	}

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

			collection := core.NewViewCollection(viewName)
			collection.ViewQuery = s.query

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

			var sql string

			rowErr := app.DB().NewQuery("SELECT sql FROM sqlite_master WHERE type='view' AND name={:name}").
				Bind(dbx.Params{"name": viewName}).
				Row(&sql)
			if rowErr != nil {
				t.Fatalf("Failed to retrieve view sql: %v", rowErr)
			}

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