package apis_test

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

	"github.com/labstack/echo/v5"
	"github.com/pocketbase/pocketbase/models"
	"github.com/pocketbase/pocketbase/tests"
)

func TestRecordCrudList(t *testing.T) {
	scenarios := []tests.ApiScenario{
		{
			Name:            "missing collection",
			Method:          http.MethodGet,
			Url:             "/api/collections/missing/records",
			ExpectedStatus:  404,
			ExpectedContent: []string{`"data":{}`},
		},
		{
			Name:            "unauthenticated trying to access nil rule collection (aka. need admin auth)",
			Method:          http.MethodGet,
			Url:             "/api/collections/demo1/records",
			ExpectedStatus:  403,
			ExpectedContent: []string{`"data":{}`},
		},
		{
			Name:   "authenticated record trying to access nil rule collection (aka. need admin auth)",
			Method: http.MethodGet,
			Url:    "/api/collections/demo1/records",
			RequestHeaders: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc",
			},
			ExpectedStatus:  403,
			ExpectedContent: []string{`"data":{}`},
		},
		{
			Name:            "public collection but with admin only filter/sort (aka. @collection)",
			Method:          http.MethodGet,
			Url:             "/api/collections/demo2/records?filter=@collection.demo2.title='test1'",
			ExpectedStatus:  403,
			ExpectedContent: []string{`"data":{}`},
		},
		{
			Name:            "public collection but with ENCODED admin only filter/sort (aka. @collection)",
			Method:          http.MethodGet,
			Url:             "/api/collections/demo2/records?filter=%40collection.demo2.title%3D%27test1%27",
			ExpectedStatus:  403,
			ExpectedContent: []string{`"data":{}`},
		},
		{
			Name:           "public collection",
			Method:         http.MethodGet,
			Url:            "/api/collections/demo2/records",
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"page":1`,
				`"perPage":30`,
				`"totalPages":1`,
				`"totalItems":3`,
				`"items":[{`,
				`"id":"0yxhwia2amd8gec"`,
				`"id":"achvryl401bhse3"`,
				`"id":"llvuca81nly1qls"`,
			},
			ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
		},
		{
			Name:           "public collection (using the collection id)",
			Method:         http.MethodGet,
			Url:            "/api/collections/sz5l5z67tg7gku0/records",
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"page":1`,
				`"perPage":30`,
				`"totalPages":1`,
				`"totalItems":3`,
				`"items":[{`,
				`"id":"0yxhwia2amd8gec"`,
				`"id":"achvryl401bhse3"`,
				`"id":"llvuca81nly1qls"`,
			},
			ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
		},
		{
			Name:   "authorized as admin trying to access nil rule collection (aka. need admin auth)",
			Method: http.MethodGet,
			Url:    "/api/collections/demo1/records",
			RequestHeaders: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"page":1`,
				`"perPage":30`,
				`"totalPages":1`,
				`"totalItems":3`,
				`"items":[{`,
				`"id":"al1h9ijdeojtsjy"`,
				`"id":"84nmscqy84lsi1t"`,
				`"id":"imy661ixudk5izi"`,
			},
			ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
		},
		{
			Name:   "valid query params",
			Method: http.MethodGet,
			Url:    "/api/collections/demo1/records?filter=text~'test'&sort=-bool",
			RequestHeaders: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"page":1`,
				`"perPage":30`,
				`"totalItems":2`,
				`"items":[{`,
				`"id":"al1h9ijdeojtsjy"`,
				`"id":"84nmscqy84lsi1t"`,
			},
			ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
		},
		{
			Name:   "invalid filter",
			Method: http.MethodGet,
			Url:    "/api/collections/demo1/records?filter=invalid~'test'",
			RequestHeaders: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
			},
			ExpectedStatus:  400,
			ExpectedContent: []string{`"data":{}`},
		},
		{
			Name:   "expand relations",
			Method: http.MethodGet,
			Url:    "/api/collections/demo1/records?expand=rel_one,rel_many.rel,missing&perPage=2&sort=created",
			RequestHeaders: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"page":1`,
				`"perPage":2`,
				`"totalPages":2`,
				`"totalItems":3`,
				`"items":[{`,
				`"collectionName":"demo1"`,
				`"id":"84nmscqy84lsi1t"`,
				`"id":"al1h9ijdeojtsjy"`,
				`"expand":{`,
				`"rel_one":""`,
				`"rel_one":{"`,
				`"rel_many":[{`,
				`"rel":{`,
				`"rel":""`,
				`"json":[1,2,3]`,
				`"select_many":["optionB","optionC"]`,
				`"select_many":["optionB"]`,
				// subrel items
				`"id":"0yxhwia2amd8gec"`,
				`"id":"llvuca81nly1qls"`,
				// email visibility should be ignored for admins even in expanded rels
				`"email":"test@example.com"`,
				`"email":"test2@example.com"`,
				`"email":"test3@example.com"`,
			},
			ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
		},
		{
			Name:   "authenticated record model that DOESN'T match the collection list rule",
			Method: http.MethodGet,
			Url:    "/api/collections/demo3/records",
			RequestHeaders: map[string]string{
				// users, test@example.com
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"page":1`,
				`"perPage":30`,
				`"totalItems":0`,
				`"items":[]`,
			},
			ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
		},
		{
			Name:   "authenticated record that matches the collection list rule",
			Method: http.MethodGet,
			Url:    "/api/collections/demo3/records",
			RequestHeaders: map[string]string{
				// clients, test@example.com
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImdrMzkwcWVnczR5NDd3biIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoidjg1MXE0cjc5MHJoa25sIiwiZXhwIjoyMjA4OTg1MjYxfQ.q34IWXrRWsjLvbbVNRfAs_J4SoTHloNBfdGEiLmy-D8",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"page":1`,
				`"perPage":30`,
				`"totalPages":1`,
				`"totalItems":4`,
				`"items":[{`,
				`"id":"1tmknxy2868d869"`,
				`"id":"lcl9d87w22ml6jy"`,
				`"id":"7nwo8tuiatetxdm"`,
				`"id":"mk5fmymtx4wsprk"`,
			},
			ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
		},
		{
			Name:           ":rule modifer",
			Method:         http.MethodGet,
			Url:            "/api/collections/demo5/records",
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"page":1`,
				`"perPage":30`,
				`"totalPages":1`,
				`"totalItems":1`,
				`"items":[{`,
				`"id":"qjeql998mtp1azp"`,
			},
			ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
		},
		{
			Name:           "multi-match - at least one of",
			Method:         http.MethodGet,
			Url:            "/api/collections/demo4/records?filter=" + url.QueryEscape("rel_many_no_cascade_required.files:length?=2"),
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"page":1`,
				`"perPage":30`,
				`"totalPages":1`,
				`"totalItems":1`,
				`"items":[{`,
				`"id":"qzaqccwrmva4o1n"`,
			},
			ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
		},
		{
			Name:           "multi-match - all",
			Method:         http.MethodGet,
			Url:            "/api/collections/demo4/records?filter=" + url.QueryEscape("rel_many_no_cascade_required.files:length=2"),
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"page":1`,
				`"perPage":30`,
				`"totalPages":0`,
				`"totalItems":0`,
				`"items":[]`,
			},
			ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
		},

		// auth collection
		// -----------------------------------------------------------
		{
			Name:           "check email visibility as guest",
			Method:         http.MethodGet,
			Url:            "/api/collections/nologin/records",
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"page":1`,
				`"perPage":30`,
				`"totalPages":1`,
				`"totalItems":3`,
				`"items":[{`,
				`"id":"phhq3wr65cap535"`,
				`"id":"dc49k6jgejn40h3"`,
				`"id":"oos036e9xvqeexy"`,
				`"email":"test2@example.com"`,
				`"emailVisibility":true`,
				`"emailVisibility":false`,
			},
			NotExpectedContent: []string{
				`"tokenKey"`,
				`"passwordHash"`,
				`"email":"test@example.com"`,
				`"email":"test3@example.com"`,
			},
			ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
		},
		{
			Name:   "check email visibility as any authenticated record",
			Method: http.MethodGet,
			Url:    "/api/collections/nologin/records",
			RequestHeaders: map[string]string{
				// clients, test@example.com
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImdrMzkwcWVnczR5NDd3biIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoidjg1MXE0cjc5MHJoa25sIiwiZXhwIjoyMjA4OTg1MjYxfQ.q34IWXrRWsjLvbbVNRfAs_J4SoTHloNBfdGEiLmy-D8",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"page":1`,
				`"perPage":30`,
				`"totalPages":1`,
				`"totalItems":3`,
				`"items":[{`,
				`"id":"phhq3wr65cap535"`,
				`"id":"dc49k6jgejn40h3"`,
				`"id":"oos036e9xvqeexy"`,
				`"email":"test2@example.com"`,
				`"emailVisibility":true`,
				`"emailVisibility":false`,
			},
			NotExpectedContent: []string{
				`"tokenKey"`,
				`"passwordHash"`,
				`"email":"test@example.com"`,
				`"email":"test3@example.com"`,
			},
			ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
		},
		{
			Name:   "check email visibility as manage auth record",
			Method: http.MethodGet,
			Url:    "/api/collections/nologin/records",
			RequestHeaders: map[string]string{
				// users, test@example.com
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"page":1`,
				`"perPage":30`,
				`"totalPages":1`,
				`"totalItems":3`,
				`"items":[{`,
				`"id":"phhq3wr65cap535"`,
				`"id":"dc49k6jgejn40h3"`,
				`"id":"oos036e9xvqeexy"`,
				`"email":"test@example.com"`,
				`"email":"test2@example.com"`,
				`"email":"test3@example.com"`,
				`"emailVisibility":true`,
				`"emailVisibility":false`,
			},
			NotExpectedContent: []string{
				`"tokenKey"`,
				`"passwordHash"`,
			},
			ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
		},
		{
			Name:   "check email visibility as admin",
			Method: http.MethodGet,
			Url:    "/api/collections/nologin/records",
			RequestHeaders: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"page":1`,
				`"perPage":30`,
				`"totalPages":1`,
				`"totalItems":3`,
				`"items":[{`,
				`"id":"phhq3wr65cap535"`,
				`"id":"dc49k6jgejn40h3"`,
				`"id":"oos036e9xvqeexy"`,
				`"email":"test@example.com"`,
				`"email":"test2@example.com"`,
				`"email":"test3@example.com"`,
				`"emailVisibility":true`,
				`"emailVisibility":false`,
			},
			NotExpectedContent: []string{
				`"tokenKey"`,
				`"passwordHash"`,
			},
			ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
		},
		{
			Name:   "check self email visibility resolver",
			Method: http.MethodGet,
			Url:    "/api/collections/nologin/records",
			RequestHeaders: map[string]string{
				// nologin, test@example.com
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImRjNDlrNmpnZWpuNDBoMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoia3B2NzA5c2sybHFicWs4IiwiZXhwIjoyMjA4OTg1MjYxfQ.DOYSon3x1-C0hJbwjEU6dp2-6oLeEa8bOlkyP1CinyM",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"page":1`,
				`"perPage":30`,
				`"totalPages":1`,
				`"totalItems":3`,
				`"items":[{`,
				`"id":"phhq3wr65cap535"`,
				`"id":"dc49k6jgejn40h3"`,
				`"id":"oos036e9xvqeexy"`,
				`"email":"test2@example.com"`,
				`"email":"test@example.com"`,
				`"emailVisibility":true`,
				`"emailVisibility":false`,
			},
			NotExpectedContent: []string{
				`"tokenKey"`,
				`"passwordHash"`,
				`"email":"test3@example.com"`,
			},
			ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
		},

		// view collection
		// -----------------------------------------------------------
		{
			Name:           "public view records",
			Method:         http.MethodGet,
			Url:            "/api/collections/view2/records?filter=state=false",
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"page":1`,
				`"perPage":30`,
				`"totalPages":1`,
				`"totalItems":2`,
				`"items":[{`,
				`"id":"al1h9ijdeojtsjy"`,
				`"id":"imy661ixudk5izi"`,
			},
			NotExpectedContent: []string{
				`"created"`,
				`"updated"`,
			},
			ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
		},
		{
			Name:           "guest that doesn't match the view collection list rule",
			Method:         http.MethodGet,
			Url:            "/api/collections/view1/records",
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"page":1`,
				`"perPage":30`,
				`"totalPages":0`,
				`"totalItems":0`,
				`"items":[]`,
			},
			ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
		},
		{
			Name:   "authenticated record that matches the view collection list rule",
			Method: http.MethodGet,
			Url:    "/api/collections/view1/records",
			RequestHeaders: map[string]string{
				// users, test@example.com
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"page":1`,
				`"perPage":30`,
				`"totalPages":1`,
				`"totalItems":1`,
				`"items":[{`,
				`"id":"84nmscqy84lsi1t"`,
				`"bool":true`,
			},
			ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
		},
	}

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

func TestRecordCrudView(t *testing.T) {
	scenarios := []tests.ApiScenario{
		{
			Name:            "missing collection",
			Method:          http.MethodGet,
			Url:             "/api/collections/missing/records/0yxhwia2amd8gec",
			ExpectedStatus:  404,
			ExpectedContent: []string{`"data":{}`},
		},
		{
			Name:            "missing record",
			Method:          http.MethodGet,
			Url:             "/api/collections/demo2/records/missing",
			ExpectedStatus:  404,
			ExpectedContent: []string{`"data":{}`},
		},
		{
			Name:            "unauthenticated trying to access nil rule collection (aka. need admin auth)",
			Method:          http.MethodGet,
			Url:             "/api/collections/demo1/records/imy661ixudk5izi",
			ExpectedStatus:  403,
			ExpectedContent: []string{`"data":{}`},
		},
		{
			Name:   "authenticated record trying to access nil rule collection (aka. need admin auth)",
			Method: http.MethodGet,
			Url:    "/api/collections/demo1/records/imy661ixudk5izi",
			RequestHeaders: map[string]string{
				// users, test@example.com
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc",
			},
			ExpectedStatus:  403,
			ExpectedContent: []string{`"data":{}`},
		},
		{
			Name:   "authenticated record that doesn't match the collection view rule",
			Method: http.MethodGet,
			Url:    "/api/collections/users/records/bgs820n361vj1qd",
			RequestHeaders: map[string]string{
				// users, test@example.com
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc",
			},
			ExpectedStatus:  404,
			ExpectedContent: []string{`"data":{}`},
		},
		{
			Name:           "public collection view",
			Method:         http.MethodGet,
			Url:            "/api/collections/demo2/records/0yxhwia2amd8gec",
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"id":"0yxhwia2amd8gec"`,
				`"collectionName":"demo2"`,
			},
			ExpectedEvents: map[string]int{"OnRecordViewRequest": 1},
		},
		{
			Name:           "public collection view (using the collection id)",
			Method:         http.MethodGet,
			Url:            "/api/collections/sz5l5z67tg7gku0/records/0yxhwia2amd8gec",
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"id":"0yxhwia2amd8gec"`,
				`"collectionName":"demo2"`,
			},
			ExpectedEvents: map[string]int{"OnRecordViewRequest": 1},
		},
		{
			Name:   "authorized as admin trying to access nil rule collection view (aka. need admin auth)",
			Method: http.MethodGet,
			Url:    "/api/collections/demo1/records/imy661ixudk5izi",
			RequestHeaders: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"id":"imy661ixudk5izi"`,
				`"collectionName":"demo1"`,
			},
			ExpectedEvents: map[string]int{"OnRecordViewRequest": 1},
		},
		{
			Name:   "authenticated record that does match the collection view rule",
			Method: http.MethodGet,
			Url:    "/api/collections/users/records/4q1xlclmfloku33",
			RequestHeaders: map[string]string{
				// users, test@example.com
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"id":"4q1xlclmfloku33"`,
				`"collectionName":"users"`,
				// owners can always view their email
				`"emailVisibility":false`,
				`"email":"test@example.com"`,
			},
			ExpectedEvents: map[string]int{"OnRecordViewRequest": 1},
		},
		{
			Name:   "expand relations",
			Method: http.MethodGet,
			Url:    "/api/collections/demo1/records/al1h9ijdeojtsjy?expand=rel_one,rel_many.rel,missing&perPage=2&sort=created",
			RequestHeaders: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"id":"al1h9ijdeojtsjy"`,
				`"collectionName":"demo1"`,
				`"rel_many":[{`,
				`"rel_one":{`,
				`"collectionName":"users"`,
				`"id":"bgs820n361vj1qd"`,
				`"expand":{"rel":{`,
				`"id":"0yxhwia2amd8gec"`,
				`"collectionName":"demo2"`,
			},
			ExpectedEvents: map[string]int{"OnRecordViewRequest": 1},
		},

		// auth collection
		// -----------------------------------------------------------
		{
			Name:           "check email visibility as guest",
			Method:         http.MethodGet,
			Url:            "/api/collections/nologin/records/oos036e9xvqeexy",
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"id":"oos036e9xvqeexy"`,
				`"emailVisibility":false`,
				`"verified":true`,
			},
			NotExpectedContent: []string{
				`"tokenKey"`,
				`"passwordHash"`,
				`"email":"test3@example.com"`,
			},
			ExpectedEvents: map[string]int{"OnRecordViewRequest": 1},
		},
		{
			Name:   "check email visibility as any authenticated record",
			Method: http.MethodGet,
			Url:    "/api/collections/nologin/records/oos036e9xvqeexy",
			RequestHeaders: map[string]string{
				// clients, test@example.com
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImdrMzkwcWVnczR5NDd3biIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoidjg1MXE0cjc5MHJoa25sIiwiZXhwIjoyMjA4OTg1MjYxfQ.q34IWXrRWsjLvbbVNRfAs_J4SoTHloNBfdGEiLmy-D8",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"id":"oos036e9xvqeexy"`,
				`"emailVisibility":false`,
				`"verified":true`,
			},
			NotExpectedContent: []string{
				`"tokenKey"`,
				`"passwordHash"`,
				`"email":"test3@example.com"`,
			},
			ExpectedEvents: map[string]int{"OnRecordViewRequest": 1},
		},
		{
			Name:   "check email visibility as manage auth record",
			Method: http.MethodGet,
			Url:    "/api/collections/nologin/records/oos036e9xvqeexy",
			RequestHeaders: map[string]string{
				// users, test@example.com
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"id":"oos036e9xvqeexy"`,
				`"emailVisibility":false`,
				`"email":"test3@example.com"`,
				`"verified":true`,
			},
			ExpectedEvents: map[string]int{"OnRecordViewRequest": 1},
		},
		{
			Name:   "check email visibility as admin",
			Method: http.MethodGet,
			Url:    "/api/collections/nologin/records/oos036e9xvqeexy",
			RequestHeaders: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"id":"oos036e9xvqeexy"`,
				`"emailVisibility":false`,
				`"email":"test3@example.com"`,
				`"verified":true`,
			},
			NotExpectedContent: []string{
				`"tokenKey"`,
				`"passwordHash"`,
			},
			ExpectedEvents: map[string]int{"OnRecordViewRequest": 1},
		},
		{
			Name:   "check self email visibility resolver",
			Method: http.MethodGet,
			Url:    "/api/collections/nologin/records/dc49k6jgejn40h3",
			RequestHeaders: map[string]string{
				// nologin, test@example.com
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImRjNDlrNmpnZWpuNDBoMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoia3B2NzA5c2sybHFicWs4IiwiZXhwIjoyMjA4OTg1MjYxfQ.DOYSon3x1-C0hJbwjEU6dp2-6oLeEa8bOlkyP1CinyM",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"id":"dc49k6jgejn40h3"`,
				`"email":"test@example.com"`,
				`"emailVisibility":false`,
				`"verified":false`,
			},
			NotExpectedContent: []string{
				`"tokenKey"`,
				`"passwordHash"`,
			},
			ExpectedEvents: map[string]int{"OnRecordViewRequest": 1},
		},

		// view collection
		// -----------------------------------------------------------
		{
			Name:           "public view record",
			Method:         http.MethodGet,
			Url:            "/api/collections/view2/records/84nmscqy84lsi1t",
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"id":"84nmscqy84lsi1t"`,
				`"state":true`,
				`"file_many":["`,
				`"rel_many":["`,
			},
			NotExpectedContent: []string{
				`"created"`,
				`"updated"`,
			},
			ExpectedEvents: map[string]int{"OnRecordViewRequest": 1},
		},
		{
			Name:            "guest that doesn't match the view collection view rule",
			Method:          http.MethodGet,
			Url:             "/api/collections/view1/records/84nmscqy84lsi1t",
			ExpectedStatus:  404,
			ExpectedContent: []string{`"data":{}`},
		},
		{
			Name:   "authenticated record that matches the view collection view rule",
			Method: http.MethodGet,
			Url:    "/api/collections/view1/records/84nmscqy84lsi1t",
			RequestHeaders: map[string]string{
				// users, test@example.com
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"id":"84nmscqy84lsi1t"`,
				`"bool":true`,
				`"text":"`,
			},
			ExpectedEvents: map[string]int{"OnRecordViewRequest": 1},
		},
	}

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

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

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

	scenarios := []tests.ApiScenario{
		{
			Name:            "missing collection",
			Method:          http.MethodDelete,
			Url:             "/api/collections/missing/records/0yxhwia2amd8gec",
			ExpectedStatus:  404,
			ExpectedContent: []string{`"data":{}`},
		},
		{
			Name:            "missing record",
			Method:          http.MethodDelete,
			Url:             "/api/collections/demo2/records/missing",
			ExpectedStatus:  404,
			ExpectedContent: []string{`"data":{}`},
		},
		{
			Name:            "unauthenticated trying to delete nil rule collection (aka. need admin auth)",
			Method:          http.MethodDelete,
			Url:             "/api/collections/demo1/records/imy661ixudk5izi",
			ExpectedStatus:  403,
			ExpectedContent: []string{`"data":{}`},
		},
		{
			Name:   "authenticated record trying to delete nil rule collection (aka. need admin auth)",
			Method: http.MethodDelete,
			Url:    "/api/collections/demo1/records/imy661ixudk5izi",
			RequestHeaders: map[string]string{
				// users, test@example.com
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc",
			},
			ExpectedStatus:  403,
			ExpectedContent: []string{`"data":{}`},
		},
		{
			Name:   "authenticated record that doesn't match the collection delete rule",
			Method: http.MethodDelete,
			Url:    "/api/collections/users/records/bgs820n361vj1qd",
			RequestHeaders: map[string]string{
				// users, test@example.com
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc",
			},
			ExpectedStatus:  404,
			ExpectedContent: []string{`"data":{}`},
		},
		{
			Name:            "trying to delete a view collection record",
			Method:          http.MethodDelete,
			Url:             "/api/collections/view1/records/imy661ixudk5izi",
			ExpectedStatus:  400,
			ExpectedContent: []string{`"data":{}`},
		},
		{
			Name:           "public collection record delete",
			Method:         http.MethodDelete,
			Url:            "/api/collections/nologin/records/dc49k6jgejn40h3",
			ExpectedStatus: 204,
			ExpectedEvents: map[string]int{
				"OnModelAfterDelete":          1,
				"OnModelBeforeDelete":         1,
				"OnRecordAfterDeleteRequest":  1,
				"OnRecordBeforeDeleteRequest": 1,
			},
		},
		{
			Name:           "public collection record delete (using the collection id as identifier)",
			Method:         http.MethodDelete,
			Url:            "/api/collections/kpv709sk2lqbqk8/records/dc49k6jgejn40h3",
			ExpectedStatus: 204,
			ExpectedEvents: map[string]int{
				"OnModelAfterDelete":          1,
				"OnModelBeforeDelete":         1,
				"OnRecordAfterDeleteRequest":  1,
				"OnRecordBeforeDeleteRequest": 1,
			},
		},
		{
			Name:   "authorized as admin trying to delete nil rule collection view (aka. need admin auth)",
			Method: http.MethodDelete,
			Url:    "/api/collections/clients/records/o1y0dd0spd786md",
			RequestHeaders: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
			},
			ExpectedStatus: 204,
			ExpectedEvents: map[string]int{
				"OnModelAfterDelete":          1,
				"OnModelBeforeDelete":         1,
				"OnRecordAfterDeleteRequest":  1,
				"OnRecordBeforeDeleteRequest": 1,
			},
		},
		{
			Name:   "authenticated record that match the collection delete rule",
			Method: http.MethodDelete,
			Url:    "/api/collections/users/records/4q1xlclmfloku33",
			RequestHeaders: map[string]string{
				// users, test@example.com
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc",
			},
			Delay:          100 * time.Millisecond,
			ExpectedStatus: 204,
			ExpectedEvents: map[string]int{
				"OnModelAfterDelete":          3, // +2 because of the external auths
				"OnModelBeforeDelete":         3, // +2 because of the external auths
				"OnModelAfterUpdate":          1,
				"OnModelBeforeUpdate":         1,
				"OnRecordAfterDeleteRequest":  1,
				"OnRecordBeforeDeleteRequest": 1,
			},
			AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
				ensureDeletedFiles(app, "_pb_users_auth_", "4q1xlclmfloku33")

				// check if all the external auths records were deleted
				collection, _ := app.Dao().FindCollectionByNameOrId("users")
				record := models.NewRecord(collection)
				record.Id = "4q1xlclmfloku33"
				externalAuths, err := app.Dao().FindAllExternalAuthsByRecord(record)
				if err != nil {
					t.Errorf("Failed to fetch external auths: %v", err)
				}
				if len(externalAuths) > 0 {
					t.Errorf("Expected the linked external auths to be deleted, got %d", len(externalAuths))
				}
			},
		},
		{
			Name:            "@request :isset (rule failure check)",
			Method:          http.MethodDelete,
			Url:             "/api/collections/demo5/records/la4y2w4o98acwuj",
			ExpectedStatus:  404,
			ExpectedContent: []string{`"data":{}`},
		},
		{
			Name:           "@request :isset (rule pass check)",
			Method:         http.MethodDelete,
			Url:            "/api/collections/demo5/records/la4y2w4o98acwuj?test=1",
			ExpectedStatus: 204,
			ExpectedEvents: map[string]int{
				"OnModelAfterDelete":          1,
				"OnModelBeforeDelete":         1,
				"OnRecordAfterDeleteRequest":  1,
				"OnRecordBeforeDeleteRequest": 1,
			},
		},

		// cascade delete checks
		// -----------------------------------------------------------
		{
			Name:   "trying to delete a record while being part of a non-cascade required relation",
			Method: http.MethodDelete,
			Url:    "/api/collections/demo3/records/7nwo8tuiatetxdm",
			RequestHeaders: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
			},
			ExpectedStatus:  400,
			ExpectedContent: []string{`"data":{}`},
			ExpectedEvents: map[string]int{
				"OnRecordBeforeDeleteRequest": 1,
				"OnModelBeforeUpdate":         2, // self_rel_many update of test1 record + rel_one_cascade demo4 cascaded in demo5
				"OnModelBeforeDelete":         2, // the record itself + rel_one_cascade of test1 record
			},
		},
		{
			Name:   "delete a record with non-cascade references",
			Method: http.MethodDelete,
			Url:    "/api/collections/demo3/records/1tmknxy2868d869",
			RequestHeaders: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
			},
			ExpectedStatus: 204,
			ExpectedEvents: map[string]int{
				"OnModelBeforeDelete":         1,
				"OnModelAfterDelete":          1,
				"OnModelBeforeUpdate":         2,
				"OnModelAfterUpdate":          2,
				"OnRecordBeforeDeleteRequest": 1,
				"OnRecordAfterDeleteRequest":  1,
			},
		},
		{
			Name:   "delete a record with cascade references",
			Method: http.MethodDelete,
			Url:    "/api/collections/users/records/oap640cot4yru2s",
			RequestHeaders: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
			},
			Delay:          100 * time.Millisecond,
			ExpectedStatus: 204,
			ExpectedEvents: map[string]int{
				"OnModelBeforeDelete":         2,
				"OnModelAfterDelete":          2,
				"OnModelBeforeUpdate":         2,
				"OnModelAfterUpdate":          2,
				"OnRecordBeforeDeleteRequest": 1,
				"OnRecordAfterDeleteRequest":  1,
			},
			AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
				recId := "84nmscqy84lsi1t"
				rec, _ := app.Dao().FindRecordById("demo1", recId, nil)
				if rec != nil {
					t.Errorf("Expected record %s to be cascade deleted", recId)
				}
				ensureDeletedFiles(app, "wsmn24bux7wo113", recId)
				ensureDeletedFiles(app, "_pb_users_auth_", "oap640cot4yru2s")
			},
		},
	}

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

func TestRecordCrudCreate(t *testing.T) {
	formData, mp, err := tests.MockMultipartData(map[string]string{
		"title": "title_test",
	}, "files")
	if err != nil {
		t.Fatal(err)
	}

	scenarios := []tests.ApiScenario{
		{
			Name:            "missing collection",
			Method:          http.MethodPost,
			Url:             "/api/collections/missing/records",
			ExpectedStatus:  404,
			ExpectedContent: []string{`"data":{}`},
		},
		{
			Name:            "guest trying to access nil-rule collection",
			Method:          http.MethodPost,
			Url:             "/api/collections/demo1/records",
			ExpectedStatus:  403,
			ExpectedContent: []string{`"data":{}`},
		},
		{
			Name:   "auth record trying to access nil-rule collection",
			Method: http.MethodPost,
			Url:    "/api/collections/demo1/records",
			RequestHeaders: map[string]string{
				// users, test@example.com
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc",
			},
			ExpectedStatus:  403,
			ExpectedContent: []string{`"data":{}`},
		},
		{
			Name:            "trying to create a new view collection record",
			Method:          http.MethodPost,
			Url:             "/api/collections/view1/records",
			Body:            strings.NewReader(`{"text":"new"}`),
			ExpectedStatus:  400,
			ExpectedContent: []string{`"data":{}`},
		},
		{
			Name:            "submit nil body",
			Method:          http.MethodPost,
			Url:             "/api/collections/demo2/records",
			Body:            nil,
			ExpectedStatus:  400,
			ExpectedContent: []string{`"data":{}`},
		},
		{
			Name:            "submit invalid format",
			Method:          http.MethodPost,
			Url:             "/api/collections/demo2/records",
			Body:            strings.NewReader(`{"`),
			ExpectedStatus:  400,
			ExpectedContent: []string{`"data":{}`},
		},
		{
			Name:           "submit empty json body",
			Method:         http.MethodPost,
			Url:            "/api/collections/nologin/records",
			Body:           strings.NewReader(`{}`),
			ExpectedStatus: 400,
			ExpectedContent: []string{
				`"data":{`,
				`"email":{"code":"validation_required"`,
				`"password":{"code":"validation_required"`,
				`"passwordConfirm":{"code":"validation_required"`,
			},
		},
		{
			Name:           "guest submit in public collection",
			Method:         http.MethodPost,
			Url:            "/api/collections/demo2/records",
			Body:           strings.NewReader(`{"title":"new"}`),
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"id":`,
				`"title":"new"`,
				`"active":false`,
			},
			ExpectedEvents: map[string]int{
				"OnRecordBeforeCreateRequest": 1,
				"OnRecordAfterCreateRequest":  1,
				"OnModelBeforeCreate":         1,
				"OnModelAfterCreate":          1,
			},
		},
		{
			Name:            "guest trying to submit in restricted collection",
			Method:          http.MethodPost,
			Url:             "/api/collections/demo3/records",
			Body:            strings.NewReader(`{"title":"test123"}`),
			ExpectedStatus:  400,
			ExpectedContent: []string{`"data":{}`},
		},
		{
			Name:   "auth record submit in restricted collection (rule failure check)",
			Method: http.MethodPost,
			Url:    "/api/collections/demo3/records",
			Body:   strings.NewReader(`{"title":"test123"}`),
			RequestHeaders: map[string]string{
				// users, test@example.com
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc",
			},
			ExpectedStatus:  400,
			ExpectedContent: []string{`"data":{}`},
		},
		{
			Name:   "auth record submit in restricted collection (rule pass check) + expand relations",
			Method: http.MethodPost,
			Url:    "/api/collections/demo4/records?expand=missing,rel_one_no_cascade,rel_many_no_cascade_required",
			Body: strings.NewReader(`{
				"title":"test123",
				"rel_one_no_cascade":"mk5fmymtx4wsprk",
				"rel_one_no_cascade_required":"7nwo8tuiatetxdm",
				"rel_one_cascade":"mk5fmymtx4wsprk",
				"rel_many_no_cascade":"mk5fmymtx4wsprk",
				"rel_many_no_cascade_required":["7nwo8tuiatetxdm","lcl9d87w22ml6jy"],
				"rel_many_cascade":"lcl9d87w22ml6jy"
			}`),
			RequestHeaders: map[string]string{
				// users, test@example.com
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"id":`,
				`"title":"test123"`,
				`"rel_one_no_cascade":"mk5fmymtx4wsprk"`,
				`"rel_one_no_cascade_required":"7nwo8tuiatetxdm"`,
				`"rel_one_cascade":"mk5fmymtx4wsprk"`,
				`"rel_many_no_cascade":["mk5fmymtx4wsprk"]`,
				`"rel_many_no_cascade_required":["7nwo8tuiatetxdm","lcl9d87w22ml6jy"]`,
				`"rel_many_cascade":["lcl9d87w22ml6jy"]`,
			},
			NotExpectedContent: []string{
				// the users auth records don't have access to view the demo3 expands
				`"expand":{`,
				`"missing"`,
				`"id":"mk5fmymtx4wsprk"`,
				`"id":"7nwo8tuiatetxdm"`,
				`"id":"lcl9d87w22ml6jy"`,
			},
			ExpectedEvents: map[string]int{
				"OnRecordBeforeCreateRequest": 1,
				"OnRecordAfterCreateRequest":  1,
				"OnModelBeforeCreate":         1,
				"OnModelAfterCreate":          1,
			},
		},
		{
			Name:   "admin submit in restricted collection (rule skip check) + expand relations",
			Method: http.MethodPost,
			Url:    "/api/collections/demo4/records?expand=missing,rel_one_no_cascade,rel_many_no_cascade_required",
			Body: strings.NewReader(`{
				"title":"test123",
				"rel_one_no_cascade":"mk5fmymtx4wsprk",
				"rel_one_no_cascade_required":"7nwo8tuiatetxdm",
				"rel_one_cascade":"mk5fmymtx4wsprk",
				"rel_many_no_cascade":"mk5fmymtx4wsprk",
				"rel_many_no_cascade_required":["7nwo8tuiatetxdm","lcl9d87w22ml6jy"],
				"rel_many_cascade":"lcl9d87w22ml6jy"
			}`),
			RequestHeaders: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"id":`,
				`"title":"test123"`,
				`"rel_one_no_cascade":"mk5fmymtx4wsprk"`,
				`"rel_one_no_cascade_required":"7nwo8tuiatetxdm"`,
				`"rel_one_cascade":"mk5fmymtx4wsprk"`,
				`"rel_many_no_cascade":["mk5fmymtx4wsprk"]`,
				`"rel_many_no_cascade_required":["7nwo8tuiatetxdm","lcl9d87w22ml6jy"]`,
				`"rel_many_cascade":["lcl9d87w22ml6jy"]`,
				`"expand":{`,
				`"id":"mk5fmymtx4wsprk"`,
				`"id":"7nwo8tuiatetxdm"`,
				`"id":"lcl9d87w22ml6jy"`,
			},
			NotExpectedContent: []string{
				`"missing"`,
			},
			ExpectedEvents: map[string]int{
				"OnRecordBeforeCreateRequest": 1,
				"OnRecordAfterCreateRequest":  1,
				"OnModelBeforeCreate":         1,
				"OnModelAfterCreate":          1,
			},
		},
		{
			Name:   "submit via multipart form data",
			Method: http.MethodPost,
			Url:    "/api/collections/demo3/records",
			Body:   formData,
			RequestHeaders: map[string]string{
				"Content-Type":  mp.FormDataContentType(),
				"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"id":"`,
				`"title":"title_test"`,
				`"files":["`,
			},
			ExpectedEvents: map[string]int{
				"OnRecordBeforeCreateRequest": 1,
				"OnRecordAfterCreateRequest":  1,
				"OnModelBeforeCreate":         1,
				"OnModelAfterCreate":          1,
			},
		},

		// ID checks
		// -----------------------------------------------------------
		{
			Name:   "invalid custom insertion id (less than 15 chars)",
			Method: http.MethodPost,
			Url:    "/api/collections/demo3/records",
			Body: strings.NewReader(`{
				"id": "12345678901234",
				"title": "test"
			}`),
			RequestHeaders: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
			},
			ExpectedStatus: 400,
			ExpectedContent: []string{
				`"id":{"code":"validation_length_invalid"`,
			},
		},
		{
			Name:   "invalid custom insertion id (more than 15 chars)",
			Method: http.MethodPost,
			Url:    "/api/collections/demo3/records",
			Body: strings.NewReader(`{
				"id": "1234567890123456",
				"title": "test"
			}`),
			RequestHeaders: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
			},
			ExpectedStatus: 400,
			ExpectedContent: []string{
				`"id":{"code":"validation_length_invalid"`,
			},
		},
		{
			Name:   "valid custom insertion id (exactly 15 chars)",
			Method: http.MethodPost,
			Url:    "/api/collections/demo3/records",
			Body: strings.NewReader(`{
				"id": "123456789012345",
				"title": "test"
			}`),
			RequestHeaders: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"id":"123456789012345"`,
				`"title":"test"`,
			},
			ExpectedEvents: map[string]int{
				"OnRecordBeforeCreateRequest": 1,
				"OnRecordAfterCreateRequest":  1,
				"OnModelBeforeCreate":         1,
				"OnModelAfterCreate":          1,
			},
		},
		{
			Name:   "valid custom insertion id existing in another non-auth collection",
			Method: http.MethodPost,
			Url:    "/api/collections/demo3/records",
			Body: strings.NewReader(`{
				"id": "0yxhwia2amd8gec",
				"title": "test"
			}`),
			RequestHeaders: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"id":"0yxhwia2amd8gec"`,
				`"title":"test"`,
			},
			ExpectedEvents: map[string]int{
				"OnRecordBeforeCreateRequest": 1,
				"OnRecordAfterCreateRequest":  1,
				"OnModelBeforeCreate":         1,
				"OnModelAfterCreate":          1,
			},
		},
		{
			Name:   "valid custom insertion auth id duplicating in another auth collection",
			Method: http.MethodPost,
			Url:    "/api/collections/users/records",
			Body: strings.NewReader(`{
				"id":"o1y0dd0spd786md",
				"title":"test",
				"password":"1234567890",
				"passwordConfirm":"1234567890"
			}`),
			RequestHeaders: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
			},
			ExpectedStatus:  400,
			ExpectedContent: []string{`"data":{}`},
			ExpectedEvents: map[string]int{
				"OnRecordBeforeCreateRequest": 1,
			},
		},

		// fields modifier checks
		// -----------------------------------------------------------
		{
			Name:   "trying to delete a record while being part of a non-cascade required relation",
			Method: http.MethodDelete,
			Url:    "/api/collections/demo3/records/7nwo8tuiatetxdm",
			RequestHeaders: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
			},
			ExpectedStatus:  400,
			ExpectedContent: []string{`"data":{}`},
			ExpectedEvents: map[string]int{
				"OnRecordBeforeDeleteRequest": 1,
				"OnModelBeforeUpdate":         2, // self_rel_many update of test1 record + rel_one_cascade demo4 cascaded in demo5
				"OnModelBeforeDelete":         2, // the record itself + rel_one_cascade of test1 record
			},
		},

		// check whether if @request.data modifer fields are properly resolved
		// -----------------------------------------------------------
		{
			Name:   "@request.data.field with compute modifers (rule failure check)",
			Method: http.MethodPost,
			Url:    "/api/collections/demo5/records",
			Body: strings.NewReader(`{
				"total":1,
				"total+":4,
				"total-":1
			}`),
			ExpectedStatus: 400,
			ExpectedContent: []string{
				`"data":{}`,
			},
		},
		{
			Name:   "@request.data.field with compute modifers (rule pass check)",
			Method: http.MethodPost,
			Url:    "/api/collections/demo5/records",
			Body: strings.NewReader(`{
				"total":1,
				"total+":3,
				"total-":1
			}`),
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"id":"`,
				`"collectionName":"demo5"`,
				`"total":3`,
			},
			ExpectedEvents: map[string]int{
				"OnModelAfterCreate":          1,
				"OnModelBeforeCreate":         1,
				"OnRecordAfterCreateRequest":  1,
				"OnRecordBeforeCreateRequest": 1,
			},
		},

		// auth records
		// -----------------------------------------------------------
		{
			Name:   "auth record with invalid data",
			Method: http.MethodPost,
			Url:    "/api/collections/users/records",
			Body: strings.NewReader(`{
				"id":"o1y0pd786mq",
				"username":"Users75657",
				"email":"invalid",
				"password":"1234567",
				"passwordConfirm":"1234560"
			}`),
			RequestHeaders: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
			},
			ExpectedStatus: 400,
			ExpectedContent: []string{
				`"data":{`,
				`"id":{"code":"validation_length_invalid"`,
				`"username":{"code":"validation_invalid_username"`, // for duplicated case-insensitive username
				`"email":{"code":"validation_is_email"`,
				`"password":{"code":"validation_length_out_of_range"`,
				`"passwordConfirm":{"code":"validation_values_mismatch"`,
			},
			NotExpectedContent: []string{
				// schema fields are not checked if the base fields has errors
				`"rel":{"code":`,
			},
		},
		{
			Name:   "auth record with valid base fields but invalid schema data",
			Method: http.MethodPost,
			Url:    "/api/collections/users/records",
			Body: strings.NewReader(`{
				"password":"12345678",
				"passwordConfirm":"12345678",
				"rel":"invalid"
			}`),
			RequestHeaders: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
			},
			ExpectedStatus: 400,
			ExpectedContent: []string{
				`"data":{`,
				`"rel":{"code":`,
			},
		},
		{
			Name:   "auth record with valid data and explicitly verified state by guest",
			Method: http.MethodPost,
			Url:    "/api/collections/users/records",
			Body: strings.NewReader(`{
				"password":"12345678",
				"passwordConfirm":"12345678",
				"verified":true
			}`),
			ExpectedStatus: 400,
			ExpectedContent: []string{
				`"data":{`,
				`"verified":{"code":`,
			},
		},
		{
			Name:   "auth record with valid data and explicitly verified state by random user",
			Method: http.MethodPost,
			Url:    "/api/collections/users/records",
			RequestHeaders: map[string]string{
				// users, test@example.com
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc",
			},
			Body: strings.NewReader(`{
				"password":"12345678",
				"passwordConfirm":"12345678",
				"emailVisibility":true,
				"verified":true
			}`),
			ExpectedStatus: 400,
			ExpectedContent: []string{
				`"data":{`,
				`"verified":{"code":`,
			},
			NotExpectedContent: []string{
				`"emailVisibility":{"code":`,
			},
		},
		{
			Name:   "auth record with valid data by admin",
			Method: http.MethodPost,
			Url:    "/api/collections/users/records",
			Body: strings.NewReader(`{
				"id":"o1o1y0pd78686mq",
				"username":"test.valid",
				"email":"new@example.com",
				"password":"12345678",
				"passwordConfirm":"12345678",
				"rel":"achvryl401bhse3",
				"emailVisibility":true,
				"verified":true
			}`),
			RequestHeaders: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"id":"o1o1y0pd78686mq"`,
				`"username":"test.valid"`,
				`"email":"new@example.com"`,
				`"rel":"achvryl401bhse3"`,
				`"emailVisibility":true`,
				`"verified":true`,
			},
			NotExpectedContent: []string{
				`"tokenKey"`,
				`"password"`,
				`"passwordConfirm"`,
				`"passwordHash"`,
			},
			ExpectedEvents: map[string]int{
				"OnModelAfterCreate":          1,
				"OnModelBeforeCreate":         1,
				"OnRecordAfterCreateRequest":  1,
				"OnRecordBeforeCreateRequest": 1,
			},
		},
		{
			Name:   "auth record with valid data by auth record with manage access",
			Method: http.MethodPost,
			Url:    "/api/collections/nologin/records",
			Body: strings.NewReader(`{
				"email":"new@example.com",
				"password":"12345678",
				"passwordConfirm":"12345678",
				"name":"test_name",
				"emailVisibility":true,
				"verified":true
			}`),
			RequestHeaders: map[string]string{
				// users, test@example.com
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"id":"`,
				`"username":"`,
				`"email":"new@example.com"`,
				`"name":"test_name"`,
				`"emailVisibility":true`,
				`"verified":true`,
			},
			NotExpectedContent: []string{
				`"tokenKey"`,
				`"password"`,
				`"passwordConfirm"`,
				`"passwordHash"`,
			},
			ExpectedEvents: map[string]int{
				"OnModelAfterCreate":          1,
				"OnModelBeforeCreate":         1,
				"OnRecordAfterCreateRequest":  1,
				"OnRecordBeforeCreateRequest": 1,
			},
		},
	}

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

func TestRecordCrudUpdate(t *testing.T) {
	formData, mp, err := tests.MockMultipartData(map[string]string{
		"title": "title_test",
	}, "files")
	if err != nil {
		t.Fatal(err)
	}

	scenarios := []tests.ApiScenario{
		{
			Name:            "missing collection",
			Method:          http.MethodPatch,
			Url:             "/api/collections/missing/records/0yxhwia2amd8gec",
			ExpectedStatus:  404,
			ExpectedContent: []string{`"data":{}`},
		},
		{
			Name:            "guest trying to access nil-rule collection record",
			Method:          http.MethodPatch,
			Url:             "/api/collections/demo1/records/imy661ixudk5izi",
			ExpectedStatus:  403,
			ExpectedContent: []string{`"data":{}`},
		},
		{
			Name:   "auth record trying to access nil-rule collection",
			Method: http.MethodPatch,
			Url:    "/api/collections/demo1/records/imy661ixudk5izi",
			RequestHeaders: map[string]string{
				// users, test@example.com
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc",
			},
			ExpectedStatus:  403,
			ExpectedContent: []string{`"data":{}`},
		},
		{
			Name:            "submit invalid body",
			Method:          http.MethodPatch,
			Url:             "/api/collections/demo2/records/0yxhwia2amd8gec",
			Body:            strings.NewReader(`{"`),
			ExpectedStatus:  400,
			ExpectedContent: []string{`"data":{}`},
		},
		{
			Name:            "trying to update a view collection record",
			Method:          http.MethodPatch,
			Url:             "/api/collections/view1/records/imy661ixudk5izi",
			Body:            strings.NewReader(`{"text":"new"}`),
			ExpectedStatus:  400,
			ExpectedContent: []string{`"data":{}`},
		},
		{
			Name:            "submit nil body",
			Method:          http.MethodPatch,
			Url:             "/api/collections/demo2/records/0yxhwia2amd8gec",
			Body:            nil,
			ExpectedStatus:  400,
			ExpectedContent: []string{`"data":{}`},
		},
		{
			Name:           "submit empty body (aka. no fields change)",
			Method:         http.MethodPatch,
			Url:            "/api/collections/demo2/records/0yxhwia2amd8gec",
			Body:           strings.NewReader(`{}`),
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"collectionName":"demo2"`,
				`"id":"0yxhwia2amd8gec"`,
			},
			ExpectedEvents: map[string]int{
				"OnModelAfterUpdate":          1,
				"OnModelBeforeUpdate":         1,
				"OnRecordAfterUpdateRequest":  1,
				"OnRecordBeforeUpdateRequest": 1,
			},
		},
		{
			Name:           "trigger field validation",
			Method:         http.MethodPatch,
			Url:            "/api/collections/demo2/records/0yxhwia2amd8gec",
			Body:           strings.NewReader(`{"title":"a"}`),
			ExpectedStatus: 400,
			ExpectedContent: []string{
				`data":{`,
				`"title":{"code":"validation_min_text_constraint"`,
			},
		},
		{
			Name:           "guest submit in public collection",
			Method:         http.MethodPatch,
			Url:            "/api/collections/demo2/records/0yxhwia2amd8gec",
			Body:           strings.NewReader(`{"title":"new"}`),
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"id":"0yxhwia2amd8gec"`,
				`"title":"new"`,
				`"active":true`,
			},
			ExpectedEvents: map[string]int{
				"OnRecordBeforeUpdateRequest": 1,
				"OnRecordAfterUpdateRequest":  1,
				"OnModelBeforeUpdate":         1,
				"OnModelAfterUpdate":          1,
			},
		},
		{
			Name:            "guest trying to submit in restricted collection",
			Method:          http.MethodPatch,
			Url:             "/api/collections/demo3/records/mk5fmymtx4wsprk",
			Body:            strings.NewReader(`{"title":"new"}`),
			ExpectedStatus:  404,
			ExpectedContent: []string{`"data":{}`},
		},
		{
			Name:   "auth record submit in restricted collection (rule failure check)",
			Method: http.MethodPatch,
			Url:    "/api/collections/demo3/records/mk5fmymtx4wsprk",
			Body:   strings.NewReader(`{"title":"new"}`),
			RequestHeaders: map[string]string{
				// users, test@example.com
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc",
			},
			ExpectedStatus:  404,
			ExpectedContent: []string{`"data":{}`},
		},
		{
			Name:   "auth record submit in restricted collection (rule pass check) + expand relations",
			Method: http.MethodPatch,
			Url:    "/api/collections/demo4/records/i9naidtvr6qsgb4?expand=missing,rel_one_no_cascade,rel_many_no_cascade_required",
			Body: strings.NewReader(`{
				"title":"test123",
				"rel_one_no_cascade":"mk5fmymtx4wsprk",
				"rel_one_no_cascade_required":"7nwo8tuiatetxdm",
				"rel_one_cascade":"mk5fmymtx4wsprk",
				"rel_many_no_cascade":"mk5fmymtx4wsprk",
				"rel_many_no_cascade_required":["7nwo8tuiatetxdm","lcl9d87w22ml6jy"],
				"rel_many_cascade":"lcl9d87w22ml6jy"
			}`),
			RequestHeaders: map[string]string{
				// users, test@example.com
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"id":"i9naidtvr6qsgb4"`,
				`"title":"test123"`,
				`"rel_one_no_cascade":"mk5fmymtx4wsprk"`,
				`"rel_one_no_cascade_required":"7nwo8tuiatetxdm"`,
				`"rel_one_cascade":"mk5fmymtx4wsprk"`,
				`"rel_many_no_cascade":["mk5fmymtx4wsprk"]`,
				`"rel_many_no_cascade_required":["7nwo8tuiatetxdm","lcl9d87w22ml6jy"]`,
				`"rel_many_cascade":["lcl9d87w22ml6jy"]`,
			},
			NotExpectedContent: []string{
				// the users auth records don't have access to view the demo3 expands
				`"expand":{`,
				`"missing"`,
				`"id":"mk5fmymtx4wsprk"`,
				`"id":"7nwo8tuiatetxdm"`,
				`"id":"lcl9d87w22ml6jy"`,
			},
			ExpectedEvents: map[string]int{
				"OnRecordBeforeUpdateRequest": 1,
				"OnRecordAfterUpdateRequest":  1,
				"OnModelBeforeUpdate":         1,
				"OnModelAfterUpdate":          1,
			},
		},
		{
			Name:   "admin submit in restricted collection (rule skip check) + expand relations",
			Method: http.MethodPatch,
			Url:    "/api/collections/demo4/records/i9naidtvr6qsgb4?expand=missing,rel_one_no_cascade,rel_many_no_cascade_required",
			Body: strings.NewReader(`{
				"title":"test123",
				"rel_one_no_cascade":"mk5fmymtx4wsprk",
				"rel_one_no_cascade_required":"7nwo8tuiatetxdm",
				"rel_one_cascade":"mk5fmymtx4wsprk",
				"rel_many_no_cascade":"mk5fmymtx4wsprk",
				"rel_many_no_cascade_required":["7nwo8tuiatetxdm","lcl9d87w22ml6jy"],
				"rel_many_cascade":"lcl9d87w22ml6jy"
			}`),
			RequestHeaders: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"id":"i9naidtvr6qsgb4"`,
				`"title":"test123"`,
				`"rel_one_no_cascade":"mk5fmymtx4wsprk"`,
				`"rel_one_no_cascade_required":"7nwo8tuiatetxdm"`,
				`"rel_one_cascade":"mk5fmymtx4wsprk"`,
				`"rel_many_no_cascade":["mk5fmymtx4wsprk"]`,
				`"rel_many_no_cascade_required":["7nwo8tuiatetxdm","lcl9d87w22ml6jy"]`,
				`"rel_many_cascade":["lcl9d87w22ml6jy"]`,
				`"expand":{`,
				`"id":"mk5fmymtx4wsprk"`,
				`"id":"7nwo8tuiatetxdm"`,
				`"id":"lcl9d87w22ml6jy"`,
			},
			NotExpectedContent: []string{
				`"missing"`,
			},
			ExpectedEvents: map[string]int{
				"OnRecordBeforeUpdateRequest": 1,
				"OnRecordAfterUpdateRequest":  1,
				"OnModelBeforeUpdate":         1,
				"OnModelAfterUpdate":          1,
			},
		},
		{
			Name:   "submit via multipart form data",
			Method: http.MethodPatch,
			Url:    "/api/collections/demo3/records/mk5fmymtx4wsprk",
			Body:   formData,
			RequestHeaders: map[string]string{
				"Content-Type":  mp.FormDataContentType(),
				"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"id":"mk5fmymtx4wsprk"`,
				`"title":"title_test"`,
				`"files":["`,
			},
			ExpectedEvents: map[string]int{
				"OnRecordBeforeUpdateRequest": 1,
				"OnRecordAfterUpdateRequest":  1,
				"OnModelBeforeUpdate":         1,
				"OnModelAfterUpdate":          1,
			},
		},
		{
			Name:   "try to change the id of an existing record",
			Method: http.MethodPatch,
			Url:    "/api/collections/demo3/records/mk5fmymtx4wsprk",
			Body: strings.NewReader(`{
				"id": "mk5fmymtx4wspra"
			}`),
			RequestHeaders: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
			},
			ExpectedStatus: 400,
			ExpectedContent: []string{
				`"data":{`,
				`"id":{"code":"validation_in_invalid"`,
			},
		},

		// check whether if @request.data modifer fields are properly resolved
		// -----------------------------------------------------------
		{
			Name:   "@request.data.field with compute modifers (rule failure check)",
			Method: http.MethodPatch,
			Url:    "/api/collections/demo5/records/la4y2w4o98acwuj",
			Body: strings.NewReader(`{
				"total+":3,
				"total-":1
			}`),
			ExpectedStatus: 404,
			ExpectedContent: []string{
				`"data":{}`,
			},
		},
		{
			Name:   "@request.data.field with compute modifers (rule pass check)",
			Method: http.MethodPatch,
			Url:    "/api/collections/demo5/records/la4y2w4o98acwuj",
			Body: strings.NewReader(`{
				"total+":2,
				"total-":1
			}`),
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"id":"la4y2w4o98acwuj"`,
				`"collectionName":"demo5"`,
				`"total":3`,
			},
			ExpectedEvents: map[string]int{
				"OnModelAfterUpdate":          1,
				"OnModelBeforeUpdate":         1,
				"OnRecordAfterUpdateRequest":  1,
				"OnRecordBeforeUpdateRequest": 1,
			},
		},

		// auth records
		// -----------------------------------------------------------
		{
			Name:   "auth record with invalid data",
			Method: http.MethodPatch,
			Url:    "/api/collections/users/records/bgs820n361vj1qd",
			Body: strings.NewReader(`{
				"username":"Users75657",
				"email":"invalid",
				"password":"1234567",
				"passwordConfirm":"1234560",
				"verified":false
			}`),
			RequestHeaders: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
			},
			ExpectedStatus: 400,
			ExpectedContent: []string{
				`"data":{`,
				`"username":{"code":"validation_invalid_username"`, // for duplicated case-insensitive username
				`"email":{"code":"validation_is_email"`,
				`"password":{"code":"validation_length_out_of_range"`,
				`"passwordConfirm":{"code":"validation_values_mismatch"`,
			},
			NotExpectedContent: []string{
				// admins are allowed to change the verified state
				`"verified"`,
				// schema fields are not checked if the base fields has errors
				`"rel":{"code":`,
			},
		},
		{
			Name:   "auth record with valid base fields but invalid schema data",
			Method: http.MethodPatch,
			Url:    "/api/collections/users/records/bgs820n361vj1qd",
			Body: strings.NewReader(`{
				"password":"12345678",
				"passwordConfirm":"12345678",
				"rel":"invalid"
			}`),
			RequestHeaders: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
			},
			ExpectedStatus: 400,
			ExpectedContent: []string{
				`"data":{`,
				`"rel":{"code":`,
			},
		},
		{
			Name:   "try to change account managing fields by guest",
			Method: http.MethodPatch,
			Url:    "/api/collections/nologin/records/phhq3wr65cap535",
			Body: strings.NewReader(`{
				"password":"12345678",
				"passwordConfirm":"12345678",
				"emailVisibility":true,
				"verified":true
			}`),
			ExpectedStatus: 400,
			ExpectedContent: []string{
				`"data":{`,
				`"verified":{"code":`,
				`"oldPassword":{"code":`,
			},
			NotExpectedContent: []string{
				`"emailVisibility":{"code":`,
			},
		},
		{
			Name:   "try to change account managing fields by auth record (owner)",
			Method: http.MethodPatch,
			Url:    "/api/collections/users/records/4q1xlclmfloku33",
			RequestHeaders: map[string]string{
				// users, test@example.com
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc",
			},
			Body: strings.NewReader(`{
				"password":"12345678",
				"passwordConfirm":"12345678",
				"emailVisibility":true,
				"verified":true
			}`),
			ExpectedStatus: 400,
			ExpectedContent: []string{
				`"data":{`,
				`"verified":{"code":`,
				`"oldPassword":{"code":`,
			},
			NotExpectedContent: []string{
				`"emailVisibility":{"code":`,
			},
		},
		{
			Name:   "try to change account managing fields by auth record with managing rights",
			Method: http.MethodPatch,
			Url:    "/api/collections/nologin/records/phhq3wr65cap535",
			Body: strings.NewReader(`{
				"email":"new@example.com",
				"password":"12345678",
				"passwordConfirm":"12345678",
				"name":"test_name",
				"emailVisibility":true,
				"verified":true
			}`),
			RequestHeaders: map[string]string{
				// users, test@example.com
				"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoUmVjb3JkIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyMjA4OTg1MjYxfQ.UwD8JvkbQtXpymT09d7J6fdA0aP9g4FJ1GPh_ggEkzc",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"email":"new@example.com"`,
				`"name":"test_name"`,
				`"emailVisibility":true`,
				`"verified":true`,
			},
			NotExpectedContent: []string{
				`"tokenKey"`,
				`"password"`,
				`"passwordConfirm"`,
				`"passwordHash"`,
			},
			ExpectedEvents: map[string]int{
				"OnModelAfterUpdate":          1,
				"OnModelBeforeUpdate":         1,
				"OnRecordAfterUpdateRequest":  1,
				"OnRecordBeforeUpdateRequest": 1,
			},
			AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
				record, _ := app.Dao().FindRecordById("nologin", "phhq3wr65cap535")
				if !record.ValidatePassword("12345678") {
					t.Fatal("Password update failed.")
				}
			},
		},
		{
			Name:   "update auth record with valid data by admin",
			Method: http.MethodPatch,
			Url:    "/api/collections/users/records/oap640cot4yru2s",
			Body: strings.NewReader(`{
				"username":"test.valid",
				"email":"new@example.com",
				"password":"12345678",
				"passwordConfirm":"12345678",
				"rel":"achvryl401bhse3",
				"emailVisibility":true,
				"verified":false
			}`),
			RequestHeaders: map[string]string{
				"Authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhZG1pbiIsImV4cCI6MjIwODk4NTI2MX0.M1m--VOqGyv0d23eeUc0r9xE8ZzHaYVmVFw1VZW6gT8",
			},
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"username":"test.valid"`,
				`"email":"new@example.com"`,
				`"rel":"achvryl401bhse3"`,
				`"emailVisibility":true`,
				`"verified":false`,
			},
			NotExpectedContent: []string{
				`"tokenKey"`,
				`"password"`,
				`"passwordConfirm"`,
				`"passwordHash"`,
			},
			ExpectedEvents: map[string]int{
				"OnModelAfterUpdate":          1,
				"OnModelBeforeUpdate":         1,
				"OnRecordAfterUpdateRequest":  1,
				"OnRecordBeforeUpdateRequest": 1,
			},
			AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
				record, _ := app.Dao().FindRecordById("users", "oap640cot4yru2s")
				if !record.ValidatePassword("12345678") {
					t.Fatal("Password update failed.")
				}
			},
		},
		{
			Name:   "update auth record with valid data by guest (empty update filter)",
			Method: http.MethodPatch,
			Url:    "/api/collections/nologin/records/dc49k6jgejn40h3",
			Body: strings.NewReader(`{
				"username":"test_new",
				"emailVisibility":true,
				"name":"test"
			}`),
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"username":"test_new"`,
				`"email":"test@example.com"`, // the email should be visible since we updated the emailVisibility
				`"emailVisibility":true`,
				`"verified":false`,
				`"name":"test"`,
			},
			NotExpectedContent: []string{
				`"tokenKey"`,
				`"password"`,
				`"passwordConfirm"`,
				`"passwordHash"`,
			},
			ExpectedEvents: map[string]int{
				"OnModelAfterUpdate":          1,
				"OnModelBeforeUpdate":         1,
				"OnRecordAfterUpdateRequest":  1,
				"OnRecordBeforeUpdateRequest": 1,
			},
		},
		{
			Name:   "success password change with oldPassword",
			Method: http.MethodPatch,
			Url:    "/api/collections/nologin/records/dc49k6jgejn40h3",
			Body: strings.NewReader(`{
				"password":"123456789",
				"passwordConfirm":"123456789",
				"oldPassword":"1234567890"
			}`),
			ExpectedStatus: 200,
			ExpectedContent: []string{
				`"id":"dc49k6jgejn40h3"`,
			},
			NotExpectedContent: []string{
				`"tokenKey"`,
				`"password"`,
				`"passwordConfirm"`,
				`"passwordHash"`,
			},
			ExpectedEvents: map[string]int{
				"OnModelAfterUpdate":          1,
				"OnModelBeforeUpdate":         1,
				"OnRecordAfterUpdateRequest":  1,
				"OnRecordBeforeUpdateRequest": 1,
			},
			AfterTestFunc: func(t *testing.T, app *tests.TestApp, e *echo.Echo) {
				record, _ := app.Dao().FindRecordById("nologin", "dc49k6jgejn40h3")
				if !record.ValidatePassword("123456789") {
					t.Fatal("Password update failed.")
				}
			},
		},
	}

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