package apis_test

import (
	"errors"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

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

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

	scenarios := []tests.ApiScenario{
		{
			Name:            "unauthorized",
			Method:          http.MethodGet,
			URL:             "/api/collections",
			ExpectedStatus:  401,
			ExpectedContent: []string{`"data":{}`},
			ExpectedEvents:  map[string]int{"*": 0},
		},
		{
			Name:   "authorized as regular user",
			Method: http.MethodGet,
			URL:    "/api/collections",
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo",
			},
			ExpectedStatus:  403,
			ExpectedContent: []string{`"data":{}`},
			ExpectedEvents:  map[string]int{"*": 0},
		},
		{
			Name:   "authorized as superuser",
			Method: http.MethodGet,
			URL:    "/api/collections",
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"page":1`,
				`"perPage":30`,
				`"totalItems":16`,
				`"items":[{`,
				`"name":"` + core.CollectionNameSuperusers + `"`,
				`"name":"` + core.CollectionNameAuthOrigins + `"`,
				`"name":"` + core.CollectionNameExternalAuths + `"`,
				`"name":"` + core.CollectionNameMFAs + `"`,
				`"name":"` + core.CollectionNameOTPs + `"`,
				`"name":"users"`,
				`"name":"nologin"`,
				`"name":"clients"`,
				`"name":"demo1"`,
				`"name":"demo2"`,
				`"name":"demo3"`,
				`"name":"demo4"`,
				`"name":"demo5"`,
				`"name":"numeric_id_view"`,
				`"name":"view1"`,
				`"name":"view2"`,
				`"type":"auth"`,
				`"type":"base"`,
				`"type":"view"`,
			},
			ExpectedEvents: map[string]int{
				"*":                        0,
				"OnCollectionsListRequest": 1,
			},
		},
		{
			Name:   "authorized as superuser + paging and sorting",
			Method: http.MethodGet,
			URL:    "/api/collections?page=2&perPage=2&sort=-created",
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"page":2`,
				`"perPage":2`,
				`"totalItems":16`,
				`"items":[{`,
				`"name":"` + core.CollectionNameMFAs + `"`,
			},
			ExpectedEvents: map[string]int{
				"*":                        0,
				"OnCollectionsListRequest": 1,
			},
		},
		{
			Name:   "authorized as superuser + invalid filter",
			Method: http.MethodGet,
			URL:    "/api/collections?filter=invalidfield~'demo2'",
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			ExpectedStatus:  400,
			ExpectedContent: []string{`"data":{}`},
			ExpectedEvents:  map[string]int{"*": 0},
		},
		{
			Name:   "authorized as superuser + valid filter",
			Method: http.MethodGet,
			URL:    "/api/collections?filter=name~'demo'",
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"page":1`,
				`"perPage":30`,
				`"totalItems":5`,
				`"items":[{`,
				`"name":"demo1"`,
				`"name":"demo2"`,
				`"name":"demo3"`,
				`"name":"demo4"`,
				`"name":"demo5"`,
			},
			ExpectedEvents: map[string]int{
				"*":                        0,
				"OnCollectionsListRequest": 1,
			},
		},
	}

	for _, scenario := range scenarios {
		scenario.Test(t)
	}
}

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

	scenarios := []tests.ApiScenario{
		{
			Name:            "unauthorized",
			Method:          http.MethodGet,
			URL:             "/api/collections/demo1",
			ExpectedStatus:  401,
			ExpectedContent: []string{`"data":{}`},
			ExpectedEvents:  map[string]int{"*": 0},
		},
		{
			Name:   "authorized as regular user",
			Method: http.MethodGet,
			URL:    "/api/collections/demo1",
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo",
			},
			ExpectedStatus:  403,
			ExpectedContent: []string{`"data":{}`},
			ExpectedEvents:  map[string]int{"*": 0},
		},
		{
			Name:   "authorized as superuser + nonexisting collection identifier",
			Method: http.MethodGet,
			URL:    "/api/collections/missing",
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			ExpectedStatus:  404,
			ExpectedContent: []string{`"data":{}`},
			ExpectedEvents:  map[string]int{"*": 0},
		},
		{
			Name:   "authorized as superuser + using the collection name",
			Method: http.MethodGet,
			URL:    "/api/collections/demo1",
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"id":"wsmn24bux7wo113"`,
				`"name":"demo1"`,
			},
			ExpectedEvents: map[string]int{
				"*":                       0,
				"OnCollectionViewRequest": 1,
			},
		},
		{
			Name:   "authorized as superuser + using the collection id",
			Method: http.MethodGet,
			URL:    "/api/collections/wsmn24bux7wo113",
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"id":"wsmn24bux7wo113"`,
				`"name":"demo1"`,
			},
			ExpectedEvents: map[string]int{
				"*":                       0,
				"OnCollectionViewRequest": 1,
			},
		},
	}

	for _, scenario := range scenarios {
		scenario.Test(t)
	}
}

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

	ensureDeletedFiles := func(app *tests.TestApp, collectionId string) {
		storageDir := filepath.Join(app.DataDir(), "storage", collectionId)

		entries, _ := os.ReadDir(storageDir)
		if len(entries) != 0 {
			t.Errorf("Expected empty/deleted dir, found %d", len(entries))
		}
	}

	scenarios := []tests.ApiScenario{
		{
			Name:            "unauthorized",
			Method:          http.MethodDelete,
			URL:             "/api/collections/demo1",
			ExpectedStatus:  401,
			ExpectedContent: []string{`"data":{}`},
			ExpectedEvents:  map[string]int{"*": 0},
		},
		{
			Name:   "authorized as regular user",
			Method: http.MethodDelete,
			URL:    "/api/collections/demo1",
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo",
			},
			ExpectedStatus:  403,
			ExpectedContent: []string{`"data":{}`},
			ExpectedEvents:  map[string]int{"*": 0},
		},
		{
			Name:   "authorized as superuser + nonexisting collection identifier",
			Method: http.MethodDelete,
			URL:    "/api/collections/missing",
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			ExpectedStatus:  404,
			ExpectedContent: []string{`"data":{}`},
			ExpectedEvents:  map[string]int{"*": 0},
		},
		{
			Name:   "authorized as superuser + using the collection name",
			Method: http.MethodDelete,
			URL:    "/api/collections/demo5",
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			Delay:          100 * time.Millisecond,
			ExpectedStatus: 204,
			ExpectedEvents: map[string]int{
				"*":                              0,
				"OnCollectionDeleteRequest":      1,
				"OnCollectionDelete":             1,
				"OnCollectionDeleteExecute":      1,
				"OnCollectionAfterDeleteSuccess": 1,
				"OnModelDelete":                  1,
				"OnModelDeleteExecute":           1,
				"OnModelAfterDeleteSuccess":      1,
			},
			AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
				ensureDeletedFiles(app, "9n89pl5vkct6330")
			},
		},
		{
			Name:   "authorized as superuser + using the collection id",
			Method: http.MethodDelete,
			URL:    "/api/collections/9n89pl5vkct6330",
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			Delay:          100 * time.Millisecond,
			ExpectedStatus: 204,
			ExpectedEvents: map[string]int{
				"*":                              0,
				"OnCollectionDeleteRequest":      1,
				"OnCollectionDelete":             1,
				"OnCollectionDeleteExecute":      1,
				"OnCollectionAfterDeleteSuccess": 1,
				"OnModelDelete":                  1,
				"OnModelDeleteExecute":           1,
				"OnModelAfterDeleteSuccess":      1,
			},
			AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
				ensureDeletedFiles(app, "9n89pl5vkct6330")
			},
		},
		{
			Name:   "authorized as superuser + trying to delete a system collection",
			Method: http.MethodDelete,
			URL:    "/api/collections/" + core.CollectionNameMFAs,
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			ExpectedStatus:  400,
			ExpectedContent: []string{`"data":{}`},
			ExpectedEvents: map[string]int{
				"*":                            0,
				"OnCollectionDeleteRequest":    1,
				"OnCollectionDelete":           1,
				"OnCollectionDeleteExecute":    1,
				"OnCollectionAfterDeleteError": 1,
				"OnModelDelete":                1,
				"OnModelDeleteExecute":         1,
				"OnModelAfterDeleteError":      1,
			},
		},
		{
			Name:   "authorized as superuser + trying to delete a referenced collection",
			Method: http.MethodDelete,
			URL:    "/api/collections/demo2",
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			ExpectedStatus:  400,
			ExpectedContent: []string{`"data":{}`},
			ExpectedEvents: map[string]int{
				"*":                            0,
				"OnCollectionDeleteRequest":    1,
				"OnCollectionDelete":           1,
				"OnCollectionDeleteExecute":    1,
				"OnCollectionAfterDeleteError": 1,
				"OnModelDelete":                1,
				"OnModelDeleteExecute":         1,
				"OnModelAfterDeleteError":      1,
			},
		},
		{
			Name:   "authorized as superuser + deleting a view",
			Method: http.MethodDelete,
			URL:    "/api/collections/view2",
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			ExpectedStatus: 204,
			ExpectedEvents: map[string]int{
				"*":                              0,
				"OnCollectionDeleteRequest":      1,
				"OnCollectionDelete":             1,
				"OnCollectionDeleteExecute":      1,
				"OnCollectionAfterDeleteSuccess": 1,
				"OnModelDelete":                  1,
				"OnModelDeleteExecute":           1,
				"OnModelAfterDeleteSuccess":      1,
			},
		},
		{
			Name:   "OnCollectionAfterDeleteSuccessRequest error response",
			Method: http.MethodDelete,
			URL:    "/api/collections/view2",
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
				app.OnCollectionDeleteRequest().BindFunc(func(e *core.CollectionRequestEvent) error {
					return errors.New("error")
				})
			},
			ExpectedStatus:  400,
			ExpectedContent: []string{`"data":{}`},
			ExpectedEvents: map[string]int{
				"*":                         0,
				"OnCollectionDeleteRequest": 1,
			},
		},
	}

	for _, scenario := range scenarios {
		scenario.Test(t)
	}
}

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

	scenarios := []tests.ApiScenario{
		{
			Name:            "unauthorized",
			Method:          http.MethodPost,
			URL:             "/api/collections",
			ExpectedStatus:  401,
			ExpectedContent: []string{`"data":{}`},
			ExpectedEvents:  map[string]int{"*": 0},
		},
		{
			Name:   "authorized as regular user",
			Method: http.MethodPost,
			URL:    "/api/collections",
			Body:   strings.NewReader(`{"name":"new","type":"base","fields":[{"type":"text","name":"test"}]}`),
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo",
			},
			ExpectedStatus:  403,
			ExpectedContent: []string{`"data":{}`},
			ExpectedEvents:  map[string]int{"*": 0},
		},
		{
			Name:   "authorized as superuser + empty data",
			Method: http.MethodPost,
			URL:    "/api/collections",
			Body:   strings.NewReader(``),
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			ExpectedStatus: 400,
			ExpectedContent: []string{
				`"data":{`,
				`"name":{"code":"validation_required"`,
			},
			NotExpectedContent: []string{
				`"fields":{`,
			},
			ExpectedEvents: map[string]int{
				"*":                            0,
				"OnCollectionCreateRequest":    1,
				"OnCollectionCreate":           1,
				"OnCollectionAfterCreateError": 1,
				"OnCollectionValidate":         1,
				"OnModelCreate":                1,
				"OnModelAfterCreateError":      1,
				"OnModelValidate":              1,
			},
		},
		{
			Name:   "authorized as superuser + invalid data (eg. existing name)",
			Method: http.MethodPost,
			URL:    "/api/collections",
			Body:   strings.NewReader(`{"name":"demo1","type":"base","fields":[{"type":"text","name":""}]}`),
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			ExpectedStatus: 400,
			ExpectedContent: []string{
				`"data":{`,
				`"fields":{`,
				`"name":{"code":"validation_collection_name_exists"`,
				`"name":{"code":"validation_required"`,
			},
			ExpectedEvents: map[string]int{
				"*":                            0,
				"OnCollectionCreateRequest":    1,
				"OnCollectionCreate":           1,
				"OnCollectionAfterCreateError": 1,
				"OnCollectionValidate":         1,
				"OnModelCreate":                1,
				"OnModelAfterCreateError":      1,
				"OnModelValidate":              1,
			},
		},
		{
			Name:   "authorized as superuser + valid data",
			Method: http.MethodPost,
			URL:    "/api/collections",
			Body:   strings.NewReader(`{"name":"new","type":"base","fields":[{"type":"text","id":"12345789","name":"test"}]}`),
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"id":`,
				`"name":"new"`,
				`"type":"base"`,
				`"system":false`,
				// ensures that id field was prepended
				`"fields":[{"autogeneratePattern":"[a-z0-9]{15}","hidden":false,"id":"text3208210256","max":15,"min":15,"name":"id","pattern":"^[a-z0-9]+$","presentable":false,"primaryKey":true,"required":true,"system":true,"type":"text"},{"autogeneratePattern":"","hidden":false,"id":"12345789","max":0,"min":0,"name":"test","pattern":"","presentable":false,"primaryKey":false,"required":false,"system":false,"type":"text"}]`,
			},
			ExpectedEvents: map[string]int{
				"*":                              0,
				"OnCollectionCreateRequest":      1,
				"OnCollectionCreate":             1,
				"OnCollectionCreateExecute":      1,
				"OnCollectionAfterCreateSuccess": 1,
				"OnCollectionValidate":           1,
				"OnModelCreate":                  1,
				"OnModelCreateExecute":           1,
				"OnModelAfterCreateSuccess":      1,
				"OnModelValidate":                1,
			},
		},
		{
			Name:   "creating auth collection (default settings merge test)",
			Method: http.MethodPost,
			URL:    "/api/collections",
			Body: strings.NewReader(`{
				"name":"new",
				"type":"auth",
				"emailChangeToken":{"duration":123},
				"fields":[
					{"type":"text","id":"12345789","name":"test"},
					{"type":"text","name":"tokenKey","system":true,"required":false,"min":10}
				]
			}`),
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"id":`,
				`"name":"new"`,
				`"type":"auth"`,
				`"system":false`,
				`"passwordAuth":{"enabled":true,"identityFields":["email"]}`,
				`"authRule":""`,
				`"manageRule":null`,
				`"name":"test"`,
				`"name":"id"`,
				`"name":"tokenKey"`,
				`"name":"password"`,
				`"name":"email"`,
				`"name":"emailVisibility"`,
				`"name":"verified"`,
				`"duration":123`,
				// should overwrite the user required option but keep the min value
				`{"autogeneratePattern":"","hidden":true,"id":"text2504183744","max":0,"min":10,"name":"tokenKey","pattern":"","presentable":false,"primaryKey":false,"required":true,"system":true,"type":"text"}`,
			},
			NotExpectedContent: []string{
				`"secret":"`,
			},
			ExpectedEvents: map[string]int{
				"*":                              0,
				"OnCollectionCreateRequest":      1,
				"OnCollectionCreate":             1,
				"OnCollectionCreateExecute":      1,
				"OnCollectionAfterCreateSuccess": 1,
				"OnCollectionValidate":           1,
				"OnModelCreate":                  1,
				"OnModelCreateExecute":           1,
				"OnModelAfterCreateSuccess":      1,
				"OnModelValidate":                1,
			},
		},
		{
			Name:   "creating base collection with reserved auth fields",
			Method: http.MethodPost,
			URL:    "/api/collections",
			Body: strings.NewReader(`{
				"name":"new",
				"type":"base",
				"fields":[
					{"type":"text","name":"email"},
					{"type":"text","name":"username"},
					{"type":"text","name":"verified"},
					{"type":"text","name":"emailVisibility"},
					{"type":"text","name":"lastResetSentAt"},
					{"type":"text","name":"lastVerificationSentAt"},
					{"type":"text","name":"tokenKey"},
					{"type":"text","name":"passwordHash"},
					{"type":"text","name":"password"},
					{"type":"text","name":"passwordConfirm"},
					{"type":"text","name":"oldPassword"}
				]
			}`),
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"name":"new"`,
				`"type":"base"`,
				`"fields":[{`,
			},
			ExpectedEvents: map[string]int{
				"*":                              0,
				"OnCollectionCreateRequest":      1,
				"OnCollectionCreate":             1,
				"OnCollectionCreateExecute":      1,
				"OnCollectionAfterCreateSuccess": 1,
				"OnCollectionValidate":           1,
				"OnModelCreate":                  1,
				"OnModelCreateExecute":           1,
				"OnModelAfterCreateSuccess":      1,
				"OnModelValidate":                1,
			},
		},
		{
			Name:   "trying to create base collection with reserved system fields",
			Method: http.MethodPost,
			URL:    "/api/collections",
			Body: strings.NewReader(`{
				"name":"new",
				"type":"base",
				"fields":[
					{"type":"text","name":"id"},
					{"type":"text","name":"expand"},
					{"type":"text","name":"collectionId"},
					{"type":"text","name":"collectionName"}
				]
			}`),
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			ExpectedStatus: 400,
			ExpectedContent: []string{
				`"data":{"fields":{`,
				`"1":{"name":{"code":"validation_not_in_invalid`,
				`"2":{"name":{"code":"validation_not_in_invalid`,
				`"3":{"name":{"code":"validation_not_in_invalid`,
			},
			ExpectedEvents: map[string]int{
				"*":                            0,
				"OnCollectionCreateRequest":    1,
				"OnCollectionCreate":           1,
				"OnCollectionAfterCreateError": 1,
				"OnCollectionValidate":         1,
				"OnModelCreate":                1,
				"OnModelAfterCreateError":      1,
				"OnModelValidate":              1,
			},
		},
		{
			Name:   "trying to create auth collection with reserved auth fields",
			Method: http.MethodPost,
			URL:    "/api/collections",
			Body: strings.NewReader(`{
				"name":"new",
				"type":"auth",
				"fields":[
					{"type":"text","name":"oldPassword"},
					{"type":"text","name":"passwordConfirm"}
				]
			}`),
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			ExpectedStatus: 400,
			ExpectedContent: []string{
				`"data":{"fields":{`,
				`"1":{"name":{"code":"validation_reserved_field_name`,
				`"2":{"name":{"code":"validation_reserved_field_name`,
			},
			ExpectedEvents: map[string]int{
				"*":                            0,
				"OnCollectionCreateRequest":    1,
				"OnCollectionCreate":           1,
				"OnCollectionAfterCreateError": 1,
				"OnCollectionValidate":         1,
				"OnModelCreate":                1,
				"OnModelAfterCreateError":      1,
				"OnModelValidate":              1,
			},
		},
		{
			Name:   "OnCollectionCreateRequest error response",
			Method: http.MethodPost,
			URL:    "/api/collections",
			Body:   strings.NewReader(`{"name":"new","type":"base"}`),
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
				app.OnCollectionCreateRequest().BindFunc(func(e *core.CollectionRequestEvent) error {
					return errors.New("error")
				})
			},
			ExpectedStatus:  400,
			ExpectedContent: []string{`"data":{}`},
			ExpectedEvents: map[string]int{
				"*":                         0,
				"OnCollectionCreateRequest": 1,
			},
		},

		// view
		// -----------------------------------------------------------
		{
			Name:   "trying to create view collection with invalid options",
			Method: http.MethodPost,
			URL:    "/api/collections",
			Body: strings.NewReader(`{
				"name":"new",
				"type":"view",
				"fields":[{"type":"text","id":"12345789","name":"ignored!@#$"}],
				"viewQuery":"invalid"
			}`),
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			ExpectedStatus: 400,
			ExpectedContent: []string{
				`"data":{`,
				`"viewQuery":{"code":"validation_invalid_view_query`,
			},
			ExpectedEvents: map[string]int{
				"*":                            0,
				"OnCollectionCreateRequest":    1,
				"OnCollectionCreate":           1,
				"OnCollectionAfterCreateError": 1,
				"OnCollectionValidate":         1,
				"OnModelCreate":                1,
				"OnModelAfterCreateError":      1,
				"OnModelValidate":              1,
			},
		},
		{
			Name:   "creating view collection",
			Method: http.MethodPost,
			URL:    "/api/collections",
			Body: strings.NewReader(`{
				"name":"new",
				"type":"view",
				"fields":[{"type":"text","id":"12345789","name":"ignored!@#$"}],
				"viewQuery": "select 1 as id from ` + core.CollectionNameSuperusers + `"
			}`),
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"name":"new"`,
				`"type":"view"`,
				`"fields":[{"autogeneratePattern":"","hidden":false,"id":"text3208210256","max":0,"min":0,"name":"id","pattern":"^[a-z0-9]+$","presentable":false,"primaryKey":true,"required":true,"system":true,"type":"text"}]`,
			},
			ExpectedEvents: map[string]int{
				"*":                              0,
				"OnCollectionCreateRequest":      1,
				"OnCollectionCreate":             1,
				"OnCollectionCreateExecute":      1,
				"OnCollectionAfterCreateSuccess": 1,
				"OnCollectionValidate":           1,
				"OnModelCreate":                  1,
				"OnModelCreateExecute":           1,
				"OnModelAfterCreateSuccess":      1,
				"OnModelValidate":                1,
			},
		},

		// indexes
		// -----------------------------------------------------------
		{
			Name:   "creating base collection with invalid indexes",
			Method: http.MethodPost,
			URL:    "/api/collections",
			Body: strings.NewReader(`{
				"name":"new",
				"type":"base",
				"fields":[
					{"type":"text","name":"test"}
				],
				"indexes": [
					"create index idx_test1 on new (test)",
					"create index idx_test2 on new (missing)"
				]
			}`),
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			ExpectedStatus: 400,
			ExpectedContent: []string{
				`"data":{`,
				`"indexes":{"1":{"code":"`,
			},
			ExpectedEvents: map[string]int{
				"*":                            0,
				"OnCollectionCreateRequest":    1,
				"OnCollectionCreate":           1,
				"OnCollectionCreateExecute":    1,
				"OnCollectionAfterCreateError": 1,
				"OnCollectionValidate":         1,
				"OnModelCreate":                1,
				"OnModelCreateExecute":         1,
				"OnModelAfterCreateError":      1,
				"OnModelValidate":              1,
			},
		},
		{
			Name:   "creating base collection with index name from another collection",
			Method: http.MethodPost,
			URL:    "/api/collections",
			Body: strings.NewReader(`{
				"name":"new",
				"type":"base",
				"fields":[
					{"type":"text","name":"test"}
				],
				"indexes": [
					"create index exist_test on new (test)"
				]
			}`),
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
				demo1, err := app.FindCollectionByNameOrId("demo1")
				if err != nil {
					t.Fatal(err)
				}
				demo1.AddIndex("exist_test", false, "updated", "")
				if err = app.Save(demo1); err != nil {
					t.Fatal(err)
				}
			},
			ExpectedStatus: 400,
			ExpectedContent: []string{
				`"data":{`,
				`"indexes":{`,
				`"0":{"code":"validation_existing_index_name"`,
			},
			ExpectedEvents: map[string]int{
				"*":                            0,
				"OnCollectionCreateRequest":    1,
				"OnCollectionCreate":           1,
				"OnCollectionAfterCreateError": 1,
				"OnCollectionValidate":         1,
				"OnModelCreate":                1,
				"OnModelAfterCreateError":      1,
				"OnModelValidate":              1,
			},
		},
		{
			Name:   "creating base collection with 2 indexes using the same name",
			Method: http.MethodPost,
			URL:    "/api/collections",
			Body: strings.NewReader(`{
				"name":"new",
				"type":"base",
				"indexes": [
					"create index duplicate_idx on new (created)",
					"create index duplicate_idx on new (updated)"
				]
			}`),
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			ExpectedStatus: 400,
			ExpectedContent: []string{
				`"data":{`,
				`"indexes":{`,
				`"1":{"code":"validation_duplicated_index_name"`,
			},
			ExpectedEvents: map[string]int{
				"*":                            0,
				"OnCollectionCreateRequest":    1,
				"OnCollectionCreate":           1,
				"OnCollectionAfterCreateError": 1,
				"OnCollectionValidate":         1,
				"OnModelCreate":                1,
				"OnModelAfterCreateError":      1,
				"OnModelValidate":              1,
			},
		},
		{
			Name:   "creating base collection with valid indexes (+ random table name)",
			Method: http.MethodPost,
			URL:    "/api/collections",
			Body: strings.NewReader(`{
				"name":"new",
				"type":"base",
				"fields":[
					{"type":"text","name":"test"}
				],
				"indexes": [
					"create index idx_test1 on new (test)",
					"create index idx_test2 on anything (id, test)"
				]
			}`),
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"name":"new"`,
				`"type":"base"`,
				`"indexes":[`,
				`idx_test1`,
				`idx_test2`,
			},
			ExpectedEvents: map[string]int{
				"*":                              0,
				"OnCollectionCreateRequest":      1,
				"OnCollectionCreate":             1,
				"OnCollectionCreateExecute":      1,
				"OnCollectionAfterCreateSuccess": 1,
				"OnCollectionValidate":           1,
				"OnModelCreate":                  1,
				"OnModelCreateExecute":           1,
				"OnModelAfterCreateSuccess":      1,
				"OnModelValidate":                1,
			},
			AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
				indexes, err := app.TableIndexes("new")
				if err != nil {
					t.Fatal(err)
				}

				expected := []string{"idx_test1", "idx_test2"}
				if len(indexes) != len(expected) {
					t.Fatalf("Expected %d indexes, got %d\n%v", len(expected), len(indexes), indexes)
				}
				for name := range indexes {
					if !list.ExistInSlice(name, expected) {
						t.Fatalf("Missing index %q", name)
					}
				}
			},
		},
	}

	for _, scenario := range scenarios {
		scenario.Test(t)
	}
}

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

	scenarios := []tests.ApiScenario{
		{
			Name:            "unauthorized",
			Method:          http.MethodPatch,
			URL:             "/api/collections/demo1",
			ExpectedStatus:  401,
			ExpectedContent: []string{`"data":{}`},
			ExpectedEvents:  map[string]int{"*": 0},
		},
		{
			Name:   "authorized as regular user",
			Method: http.MethodPatch,
			URL:    "/api/collections/demo1",
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo",
			},
			ExpectedStatus:  403,
			ExpectedContent: []string{`"data":{}`},
			ExpectedEvents:  map[string]int{"*": 0},
		},
		{
			Name:   "authorized as superuser + missing collection",
			Method: http.MethodPatch,
			URL:    "/api/collections/missing",
			Body:   strings.NewReader(`{}`),
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			ExpectedStatus:  404,
			ExpectedContent: []string{`"data":{}`},
			ExpectedEvents:  map[string]int{"*": 0},
		},
		{
			Name:   "authorized as superuser + empty body",
			Method: http.MethodPatch,
			URL:    "/api/collections/demo1",
			Body:   strings.NewReader(`{}`),
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"id":"wsmn24bux7wo113"`,
				`"name":"demo1"`,
			},
			ExpectedEvents: map[string]int{
				"*":                              0,
				"OnCollectionUpdateRequest":      1,
				"OnCollectionUpdate":             1,
				"OnCollectionUpdateExecute":      1,
				"OnCollectionAfterUpdateSuccess": 1,
				"OnCollectionValidate":           1,
				"OnModelUpdate":                  1,
				"OnModelUpdateExecute":           1,
				"OnModelAfterUpdateSuccess":      1,
				"OnModelValidate":                1,
			},
		},
		{
			Name:   "OnCollectionAfterUpdateSuccessRequest error response",
			Method: http.MethodPatch,
			URL:    "/api/collections/demo1",
			Body:   strings.NewReader(`{}`),
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
				app.OnCollectionUpdateRequest().BindFunc(func(e *core.CollectionRequestEvent) error {
					return errors.New("error")
				})
			},
			ExpectedStatus:  400,
			ExpectedContent: []string{`"data":{}`},
			ExpectedEvents: map[string]int{
				"*":                         0,
				"OnCollectionUpdateRequest": 1,
			},
		},
		{
			Name:   "authorized as superuser + invalid data (eg. existing name)",
			Method: http.MethodPatch,
			URL:    "/api/collections/demo1",
			Body: strings.NewReader(`{
				"name":"demo2",
				"type":"auth"
			}`),
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			ExpectedStatus: 400,
			ExpectedContent: []string{
				`"data":{`,
				`"name":{"code":"validation_collection_name_exists"`,
				`"type":{"code":"validation_collection_type_change"`,
			},
			ExpectedEvents: map[string]int{
				"*":                            0,
				"OnCollectionUpdateRequest":    1,
				"OnCollectionUpdate":           1,
				"OnCollectionAfterUpdateError": 1,
				"OnCollectionValidate":         1,
				"OnModelUpdate":                1,
				"OnModelAfterUpdateError":      1,
				"OnModelValidate":              1,
			},
		},
		{
			Name:   "authorized as superuser + valid data",
			Method: http.MethodPatch,
			URL:    "/api/collections/demo1",
			Body:   strings.NewReader(`{"name":"new"}`),
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"id":`,
				`"name":"new"`,
			},
			ExpectedEvents: map[string]int{
				"*":                              0,
				"OnCollectionUpdateRequest":      1,
				"OnCollectionUpdate":             1,
				"OnCollectionUpdateExecute":      1,
				"OnCollectionAfterUpdateSuccess": 1,
				"OnCollectionValidate":           1,
				"OnModelUpdate":                  1,
				"OnModelUpdateExecute":           1,
				"OnModelAfterUpdateSuccess":      1,
				"OnModelValidate":                1,
			},
			AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
				// check if the record table was renamed
				if !app.HasTable("new") {
					t.Fatal("Couldn't find record table 'new'.")
				}
			},
		},
		{
			Name:   "trying to update collection with reserved fields",
			Method: http.MethodPatch,
			URL:    "/api/collections/demo1",
			Body: strings.NewReader(`{
				"name":"new",
				"fields":[
					{"type":"text","name":"id","id":"_pbf_text_id_"},
					{"type":"text","name":"created"},
					{"type":"text","name":"updated"},
					{"type":"text","name":"expand"},
					{"type":"text","name":"collectionId"},
					{"type":"text","name":"collectionName"}
				]
			}`),
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			ExpectedStatus: 400,
			ExpectedContent: []string{
				`"data":{"fields":{`,
				`"3":{"name":{"code":"validation_not_in_invalid`,
				`"4":{"name":{"code":"validation_not_in_invalid`,
				`"5":{"name":{"code":"validation_not_in_invalid`,
			},
			NotExpectedContent: []string{
				`"0":`,
				`"1":`,
				`"2":`,
			},
			ExpectedEvents: map[string]int{
				"*":                            0,
				"OnCollectionUpdateRequest":    1,
				"OnCollectionUpdate":           1,
				"OnCollectionAfterUpdateError": 1,
				"OnCollectionValidate":         1,
				"OnModelUpdate":                1,
				"OnModelAfterUpdateError":      1,
				"OnModelValidate":              1,
			},
		},
		{
			Name:   "trying to update collection with changed/removed system fields",
			Method: http.MethodPatch,
			URL:    "/api/collections/demo1",
			Body: strings.NewReader(`{
				"name":"new",
				"fields":[
					{"type":"text","name":"created"}
				]
			}`),
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			ExpectedStatus: 400,
			ExpectedContent: []string{
				`"data":{"fields":{`,
				`"code":"validation_system_field_change"`,
			},
			ExpectedEvents: map[string]int{
				"*":                            0,
				"OnCollectionUpdateRequest":    1,
				"OnCollectionUpdate":           1,
				"OnCollectionAfterUpdateError": 1,
				"OnCollectionValidate":         1,
				"OnModelUpdate":                1,
				"OnModelAfterUpdateError":      1,
				"OnModelValidate":              1,
			},
		},
		{
			Name:   "trying to update auth collection with invalid options",
			Method: http.MethodPatch,
			URL:    "/api/collections/users",
			Body: strings.NewReader(`{
				"passwordAuth":{"identityFields": ["missing"]}
			}`),
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			ExpectedStatus: 400,
			ExpectedContent: []string{
				`"data":{`,
				`"passwordAuth":{"identityFields":{"code":"validation_missing_field"`,
			},
			ExpectedEvents: map[string]int{
				"*":                            0,
				"OnCollectionUpdateRequest":    1,
				"OnCollectionUpdate":           1,
				"OnCollectionAfterUpdateError": 1,
				"OnCollectionValidate":         1,
				"OnModelUpdate":                1,
				"OnModelAfterUpdateError":      1,
				"OnModelValidate":              1,
			},
		},

		// view
		// -----------------------------------------------------------
		{
			Name:   "trying to update view collection with invalid options",
			Method: http.MethodPatch,
			URL:    "/api/collections/view1",
			Body: strings.NewReader(`{
				"fields":[{"type":"text","id":"12345789","name":"ignored!@#$"}],
				"viewQuery":"invalid"
			}`),
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			ExpectedStatus: 400,
			ExpectedContent: []string{
				`"data":{`,
				`"viewQuery":{"code":"validation_invalid_view_query"`,
			},
			ExpectedEvents: map[string]int{
				"*":                            0,
				"OnCollectionUpdateRequest":    1,
				"OnCollectionUpdate":           1,
				"OnCollectionAfterUpdateError": 1,
				"OnCollectionValidate":         1,
				"OnModelUpdate":                1,
				"OnModelAfterUpdateError":      1,
				"OnModelValidate":              1,
			},
		},
		{
			Name:   "updating view collection",
			Method: http.MethodPatch,
			URL:    "/api/collections/view2",
			Body: strings.NewReader(`{
				"name":"view2_update",
				"fields":[{"type":"text","id":"12345789","name":"ignored!@#$"}],
				"viewQuery": "select 2 as id, created, updated, email from ` + core.CollectionNameSuperusers + `"
			}`),
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"name":"view2_update"`,
				`"type":"view"`,
				`"fields":[{`,
				`"name":"email"`,
				`"name":"id"`,
				`"name":"created"`,
				`"name":"updated"`,
			},
			ExpectedEvents: map[string]int{
				"*":                              0,
				"OnCollectionUpdateRequest":      1,
				"OnCollectionUpdate":             1,
				"OnCollectionUpdateExecute":      1,
				"OnCollectionAfterUpdateSuccess": 1,
				"OnCollectionValidate":           1,
				"OnModelUpdate":                  1,
				"OnModelUpdateExecute":           1,
				"OnModelAfterUpdateSuccess":      1,
				"OnModelValidate":                1,
			},
		},

		// indexes
		// -----------------------------------------------------------
		{
			Name:   "updating base collection with invalid indexes",
			Method: http.MethodPatch,
			URL:    "/api/collections/demo2",
			Body: strings.NewReader(`{
				"indexes": [
					"create unique idx_test1 on demo1 (text)",
					"create index idx_test2 on demo2 (id, title)"
				]
			}`),
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			ExpectedStatus: 400,
			ExpectedContent: []string{
				`"data":{`,
				`"indexes":{"0":{"code":"`,
			},
			ExpectedEvents: map[string]int{
				"*":                            0,
				"OnCollectionUpdateRequest":    1,
				"OnCollectionUpdate":           1,
				"OnCollectionAfterUpdateError": 1,
				"OnCollectionValidate":         1,
				"OnModelUpdate":                1,
				"OnModelAfterUpdateError":      1,
				"OnModelValidate":              1,
			},
		},

		{
			Name:   "updating base collection with index name from another collection",
			Method: http.MethodPatch,
			URL:    "/api/collections/demo2",
			Body: strings.NewReader(`{
				"indexes": [
					"create index exist_test on new (test)"
				]
			}`),
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
				demo1, err := app.FindCollectionByNameOrId("demo1")
				if err != nil {
					t.Fatal(err)
				}
				demo1.AddIndex("exist_test", false, "updated", "")
				if err = app.Save(demo1); err != nil {
					t.Fatal(err)
				}
			},
			ExpectedStatus: 400,
			ExpectedContent: []string{
				`"data":{`,
				`"indexes":{`,
				`"0":{"code":"validation_existing_index_name"`,
			},
			ExpectedEvents: map[string]int{
				"*":                            0,
				"OnCollectionUpdateRequest":    1,
				"OnCollectionUpdate":           1,
				"OnCollectionAfterUpdateError": 1,
				"OnCollectionValidate":         1,
				"OnModelUpdate":                1,
				"OnModelAfterUpdateError":      1,
				"OnModelValidate":              1,
			},
		},
		{
			Name:   "updating base collection with 2 indexes using the same name",
			Method: http.MethodPatch,
			URL:    "/api/collections/demo2",
			Body: strings.NewReader(`{
				"indexes": [
					"create index duplicate_idx on new (created)",
					"create index duplicate_idx on new (updated)"
				]
			}`),
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			ExpectedStatus: 400,
			ExpectedContent: []string{
				`"data":{`,
				`"indexes":{`,
				`"1":{"code":"validation_duplicated_index_name"`,
			},
			ExpectedEvents: map[string]int{
				"*":                            0,
				"OnCollectionUpdateRequest":    1,
				"OnCollectionUpdate":           1,
				"OnCollectionAfterUpdateError": 1,
				"OnCollectionValidate":         1,
				"OnModelUpdate":                1,
				"OnModelAfterUpdateError":      1,
				"OnModelValidate":              1,
			},
		},

		{
			Name:   "updating base collection with valid indexes (+ random table name)",
			Method: http.MethodPatch,
			URL:    "/api/collections/demo2",
			Body: strings.NewReader(`{
				"indexes": [
					"create unique index idx_test1 on demo2 (title)",
					"create index idx_test2 on anything (active)"
				]
			}`),
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"name":"demo2"`,
				`"indexes":[`,
				`idx_test1`,
				`idx_test2`,
			},
			ExpectedEvents: map[string]int{
				"*":                              0,
				"OnCollectionUpdateRequest":      1,
				"OnCollectionUpdate":             1,
				"OnCollectionUpdateExecute":      1,
				"OnCollectionAfterUpdateSuccess": 1,
				"OnCollectionValidate":           1,
				"OnModelUpdate":                  1,
				"OnModelUpdateExecute":           1,
				"OnModelAfterUpdateSuccess":      1,
				"OnModelValidate":                1,
			},
			AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
				indexes, err := app.TableIndexes("demo2")
				if err != nil {
					t.Fatal(err)
				}

				expected := []string{"idx_test1", "idx_test2"}
				if len(indexes) != len(expected) {
					t.Fatalf("Expected %d indexes, got %d\n%v", len(expected), len(indexes), indexes)
				}
				for name := range indexes {
					if !list.ExistInSlice(name, expected) {
						t.Fatalf("Missing index %q", name)
					}
				}
			},
		},
	}

	for _, scenario := range scenarios {
		scenario.Test(t)
	}
}

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

	scenarios := []tests.ApiScenario{
		{
			Name:            "unauthorized",
			Method:          http.MethodGet,
			URL:             "/api/collections/meta/scaffolds",
			ExpectedStatus:  401,
			ExpectedContent: []string{`"data":{}`},
			ExpectedEvents:  map[string]int{"*": 0},
		},
		{
			Name:   "authorized as regular user",
			Method: http.MethodGet,
			URL:    "/api/collections/meta/scaffolds",
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo",
			},
			ExpectedStatus:  403,
			ExpectedContent: []string{`"data":{}`},
			ExpectedEvents:  map[string]int{"*": 0},
		},
		{
			Name:   "authorized as superuser",
			Method: http.MethodGet,
			URL:    "/api/collections/meta/scaffolds",
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"id":""`,
				`"name":""`,
				`"auth":{`,
				`"base":{`,
				`"view":{`,
				`"type":"auth"`,
				`"type":"base"`,
				`"type":"view"`,
				`"fields":[{`,
				`"fields":[{`,
				`"id":"text3208210256"`,
			},
		},
	}

	for _, scenario := range scenarios {
		scenario.Test(t)
	}
}

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

	scenarios := []tests.ApiScenario{
		{
			Name:            "unauthorized",
			Method:          http.MethodDelete,
			URL:             "/api/collections/demo5/truncate",
			ExpectedStatus:  401,
			ExpectedContent: []string{`"data":{}`},
			ExpectedEvents:  map[string]int{"*": 0},
		},
		{
			Name:   "authorized as regular user",
			Method: http.MethodDelete,
			URL:    "/api/collections/demo5/truncate",
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo",
			},
			ExpectedStatus:  403,
			ExpectedContent: []string{`"data":{}`},
			ExpectedEvents:  map[string]int{"*": 0},
		},
		{
			Name:   "authorized as superuser",
			Method: http.MethodDelete,
			URL:    "/api/collections/demo5/truncate",
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			ExpectedStatus: 204,
			ExpectedEvents: map[string]int{
				"*":                          0,
				"OnModelDelete":              2,
				"OnModelDeleteExecute":       2,
				"OnModelAfterDeleteSuccess":  2,
				"OnRecordDelete":             2,
				"OnRecordDeleteExecute":      2,
				"OnRecordAfterDeleteSuccess": 2,
			},
		},
		{
			Name:   "authorized as superuser but collection with required cascade delete references",
			Method: http.MethodDelete,
			URL:    "/api/collections/demo3/truncate",
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			ExpectedStatus:  400,
			ExpectedContent: []string{`"data":{}`},
			ExpectedEvents: map[string]int{
				"*":                        0,
				"OnModelDelete":            2,
				"OnModelDeleteExecute":     2,
				"OnModelAfterDeleteError":  2,
				"OnModelUpdate":            2,
				"OnModelUpdateExecute":     2,
				"OnModelAfterUpdateError":  2,
				"OnRecordDelete":           2,
				"OnRecordDeleteExecute":    2,
				"OnRecordAfterDeleteError": 2,
				"OnRecordUpdate":           2,
				"OnRecordUpdateExecute":    2,
				"OnRecordAfterUpdateError": 2,
			},
		},
		{
			Name:   "authorized as superuser trying to truncate view collection",
			Method: http.MethodDelete,
			URL:    "/api/collections/view2/truncate",
			Headers: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
			},
			ExpectedStatus:  400,
			ExpectedContent: []string{`"data":{}`},
			ExpectedEvents:  map[string]int{"*": 0},
		},
	}

	for _, scenario := range scenarios {
		scenario.Test(t)
	}
}