package migratecmd_test

import (
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"testing"

	"github.com/pocketbase/pocketbase/core"
	"github.com/pocketbase/pocketbase/plugins/migratecmd"
	"github.com/pocketbase/pocketbase/tests"
	"github.com/pocketbase/pocketbase/tools/list"
	"github.com/pocketbase/pocketbase/tools/types"
)

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

	scenarios := []struct {
		lang             string
		expectedTemplate string
	}{
		{
			migratecmd.TemplateLangJS,
			`
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
  const collection = new Collection({
    "authAlert": {
      "emailTemplate": {
        "body": "<p>Hello,</p>\n<p>We noticed a login to your {APP_NAME} account from a new location:</p>\n<p><em>{ALERT_INFO}</em></p>\n<p><strong>If this wasn't you, you should immediately change your {APP_NAME} account password to revoke access from all other locations.</strong></p>\n<p>If this was you, you may disregard this email.</p>\n<p>\n  Thanks,<br/>\n  {APP_NAME} team\n</p>",
        "subject": "Login from a new location"
      },
      "enabled": true
    },
    "authRule": "",
    "authToken": {
      "duration": 432000
    },
    "confirmEmailChangeTemplate": {
      "body": "<p>Hello,</p>\n<p>Click on the button below to confirm your new email address.</p>\n<p>\n  <a class=\"btn\" href=\"{APP_URL}/_/#/auth/confirm-email-change/{TOKEN}\" target=\"_blank\" rel=\"noopener\">Confirm new email</a>\n</p>\n<p><i>If you didn't ask to change your email address, please ignore this email.</i></p>\n<p>\n  Thanks,<br/>\n  {APP_NAME} team\n</p>",
      "subject": "Confirm your {APP_NAME} new email address"
    },
    "createRule": null,
    "deleteRule": null,
    "emailChangeToken": {
      "duration": 1800
    },
    "fields": [
      {
        "autogeneratePattern": "[a-z0-9]{15}",
        "help": "",
        "hidden": false,
        "id": "text@TEST_RANDOM",
        "max": 15,
        "min": 15,
        "name": "id",
        "pattern": "^[a-z0-9]+$",
        "presentable": false,
        "primaryKey": true,
        "required": true,
        "system": true,
        "type": "text"
      },
      {
        "cost": 0,
        "help": "",
        "hidden": true,
        "id": "password@TEST_RANDOM",
        "max": 0,
        "min": 8,
        "name": "password",
        "pattern": "",
        "presentable": false,
        "required": true,
        "system": true,
        "type": "password"
      },
      {
        "autogeneratePattern": "[a-zA-Z0-9]{50}",
        "help": "",
        "hidden": true,
        "id": "text@TEST_RANDOM",
        "max": 60,
        "min": 30,
        "name": "tokenKey",
        "pattern": "",
        "presentable": false,
        "primaryKey": false,
        "required": true,
        "system": true,
        "type": "text"
      },
      {
        "exceptDomains": null,
        "help": "",
        "hidden": false,
        "id": "email@TEST_RANDOM",
        "name": "email",
        "onlyDomains": null,
        "presentable": false,
        "required": true,
        "system": true,
        "type": "email"
      },
      {
        "help": "",
        "hidden": false,
        "id": "bool@TEST_RANDOM",
        "name": "emailVisibility",
        "presentable": false,
        "required": false,
        "system": true,
        "type": "bool"
      },
      {
        "help": "",
        "hidden": false,
        "id": "bool@TEST_RANDOM",
        "name": "verified",
        "presentable": false,
        "required": false,
        "system": true,
        "type": "bool"
      }
    ],
    "fileToken": {
      "duration": 180
    },
    "id": "@TEST_RANDOM",
    "indexes": [
      "create index test on new_name (id)",
      "CREATE UNIQUE INDEX ` + "`" + `idx_tokenKey_@TEST_RANDOM` + "`" + ` ON ` + "`" + `new_name` + "`" + ` (` + "`" + `tokenKey` + "`" + `)",
      "CREATE UNIQUE INDEX ` + "`" + `idx_email_@TEST_RANDOM` + "`" + ` ON ` + "`" + `new_name` + "`" + ` (` + "`" + `email` + "`" + `) WHERE ` + "`" + `email` + "`" + ` != ''"
    ],
    "listRule": "@request.auth.id != '' && 1 > 0 || 'backtick` + "`" + `test' = 0",
    "manageRule": "1 != 2",
    "mfa": {
      "duration": 600,
      "enabled": false,
      "rule": ""
    },
    "name": "new_name",
    "oauth2": {
      "enabled": false,
      "mappedFields": {
        "avatarURL": "",
        "id": "",
        "name": "",
        "username": ""
      }
    },
    "otp": {
      "duration": 180,
      "emailTemplate": {
        "body": "<p>Hello,</p>\n<p>Your one-time password is: <strong>{OTP}</strong></p>\n<p><i>If you didn't ask for the one-time password, you can ignore this email.</i></p>\n<p>\n  Thanks,<br/>\n  {APP_NAME} team\n</p>",
        "subject": "OTP for {APP_NAME}"
      },
      "enabled": false,
      "length": 8
    },
    "passwordAuth": {
      "enabled": true,
      "identityFields": [
        "email"
      ]
    },
    "passwordResetToken": {
      "duration": 1800
    },
    "resetPasswordTemplate": {
      "body": "<p>Hello,</p>\n<p>Click on the button below to reset your password.</p>\n<p>\n  <a class=\"btn\" href=\"{APP_URL}/_/#/auth/confirm-password-reset/{TOKEN}\" target=\"_blank\" rel=\"noopener\">Reset password</a>\n</p>\n<p><i>If you didn't ask to reset your password, please ignore this email.</i></p>\n<p>\n  Thanks,<br/>\n  {APP_NAME} team\n</p>",
      "subject": "Reset your {APP_NAME} password"
    },
    "system": true,
    "type": "auth",
    "updateRule": null,
    "verificationTemplate": {
      "body": "<p>Hello,</p>\n<p>Thank you for joining us at {APP_NAME}.</p>\n<p>Click on the button below to verify your email address.</p>\n<p>\n  <a class=\"btn\" href=\"{APP_URL}/_/#/auth/confirm-verification/{TOKEN}\" target=\"_blank\" rel=\"noopener\">Verify</a>\n</p>\n<p><i>If you didn't recently register, please ignore this email.</i></p>\n<p>\n  Thanks,<br/>\n  {APP_NAME} team\n</p>",
      "subject": "Verify your {APP_NAME} email"
    },
    "verificationToken": {
      "duration": 86400
    },
    "viewRule": "id = \"1\""
  });

  return app.save(collection);
}, (app) => {
  const collection = app.findCollectionByNameOrId("@TEST_RANDOM");

  return app.delete(collection);
})
`,
		},
		{
			migratecmd.TemplateLangGo,
			`
package _test_migrations

import (
	"encoding/json"

	"github.com/pocketbase/pocketbase/core"
	m "github.com/pocketbase/pocketbase/migrations"
)

func init() {
	m.Register(func(app core.App) error {
		jsonData := ` + "`" + `{
			"authAlert": {
				"emailTemplate": {
					"body": "<p>Hello,</p>\n<p>We noticed a login to your {APP_NAME} account from a new location:</p>\n<p><em>{ALERT_INFO}</em></p>\n<p><strong>If this wasn't you, you should immediately change your {APP_NAME} account password to revoke access from all other locations.</strong></p>\n<p>If this was you, you may disregard this email.</p>\n<p>\n  Thanks,<br/>\n  {APP_NAME} team\n</p>",
					"subject": "Login from a new location"
				},
				"enabled": true
			},
			"authRule": "",
			"authToken": {
				"duration": 432000
			},
			"confirmEmailChangeTemplate": {
				"body": "<p>Hello,</p>\n<p>Click on the button below to confirm your new email address.</p>\n<p>\n  <a class=\"btn\" href=\"{APP_URL}/_/#/auth/confirm-email-change/{TOKEN}\" target=\"_blank\" rel=\"noopener\">Confirm new email</a>\n</p>\n<p><i>If you didn't ask to change your email address, please ignore this email.</i></p>\n<p>\n  Thanks,<br/>\n  {APP_NAME} team\n</p>",
				"subject": "Confirm your {APP_NAME} new email address"
			},
			"createRule": null,
			"deleteRule": null,
			"emailChangeToken": {
				"duration": 1800
			},
			"fields": [
				{
					"autogeneratePattern": "[a-z0-9]{15}",
					"help": "",
					"hidden": false,
					"id": "text@TEST_RANDOM",
					"max": 15,
					"min": 15,
					"name": "id",
					"pattern": "^[a-z0-9]+$",
					"presentable": false,
					"primaryKey": true,
					"required": true,
					"system": true,
					"type": "text"
				},
				{
					"cost": 0,
					"help": "",
					"hidden": true,
					"id": "password@TEST_RANDOM",
					"max": 0,
					"min": 8,
					"name": "password",
					"pattern": "",
					"presentable": false,
					"required": true,
					"system": true,
					"type": "password"
				},
				{
					"autogeneratePattern": "[a-zA-Z0-9]{50}",
					"help": "",
					"hidden": true,
					"id": "text@TEST_RANDOM",
					"max": 60,
					"min": 30,
					"name": "tokenKey",
					"pattern": "",
					"presentable": false,
					"primaryKey": false,
					"required": true,
					"system": true,
					"type": "text"
				},
				{
					"exceptDomains": null,
					"help": "",
					"hidden": false,
					"id": "email@TEST_RANDOM",
					"name": "email",
					"onlyDomains": null,
					"presentable": false,
					"required": true,
					"system": true,
					"type": "email"
				},
				{
					"help": "",
					"hidden": false,
					"id": "bool@TEST_RANDOM",
					"name": "emailVisibility",
					"presentable": false,
					"required": false,
					"system": true,
					"type": "bool"
				},
				{
					"help": "",
					"hidden": false,
					"id": "bool@TEST_RANDOM",
					"name": "verified",
					"presentable": false,
					"required": false,
					"system": true,
					"type": "bool"
				}
			],
			"fileToken": {
				"duration": 180
			},
			"id": "@TEST_RANDOM",
			"indexes": [
				"create index test on new_name (id)",
				"CREATE UNIQUE INDEX ` + "` + \"`\" + `" + `idx_tokenKey_@TEST_RANDOM` + "` + \"`\" + `" + ` ON ` + "` + \"`\" + `" + `new_name` + "` + \"`\" + `" + ` (` + "` + \"`\" + `" + `tokenKey` + "` + \"`\" + `" + `)",
				"CREATE UNIQUE INDEX ` + "` + \"`\" + `" + `idx_email_@TEST_RANDOM` + "` + \"`\" + `" + ` ON ` + "` + \"`\" + `" + `new_name` + "` + \"`\" + `" + ` (` + "` + \"`\" + `" + `email` + "` + \"`\" + `" + `) WHERE ` + "` + \"`\" + `" + `email` + "` + \"`\" + `" + ` != ''"
			],
			"listRule": "@request.auth.id != '' && 1 > 0 || 'backtick` + "` + \"`\" + `" + `test' = 0",
			"manageRule": "1 != 2",
			"mfa": {
				"duration": 600,
				"enabled": false,
				"rule": ""
			},
			"name": "new_name",
			"oauth2": {
				"enabled": false,
				"mappedFields": {
					"avatarURL": "",
					"id": "",
					"name": "",
					"username": ""
				}
			},
			"otp": {
				"duration": 180,
				"emailTemplate": {
					"body": "<p>Hello,</p>\n<p>Your one-time password is: <strong>{OTP}</strong></p>\n<p><i>If you didn't ask for the one-time password, you can ignore this email.</i></p>\n<p>\n  Thanks,<br/>\n  {APP_NAME} team\n</p>",
					"subject": "OTP for {APP_NAME}"
				},
				"enabled": false,
				"length": 8
			},
			"passwordAuth": {
				"enabled": true,
				"identityFields": [
					"email"
				]
			},
			"passwordResetToken": {
				"duration": 1800
			},
			"resetPasswordTemplate": {
				"body": "<p>Hello,</p>\n<p>Click on the button below to reset your password.</p>\n<p>\n  <a class=\"btn\" href=\"{APP_URL}/_/#/auth/confirm-password-reset/{TOKEN}\" target=\"_blank\" rel=\"noopener\">Reset password</a>\n</p>\n<p><i>If you didn't ask to reset your password, please ignore this email.</i></p>\n<p>\n  Thanks,<br/>\n  {APP_NAME} team\n</p>",
				"subject": "Reset your {APP_NAME} password"
			},
			"system": true,
			"type": "auth",
			"updateRule": null,
			"verificationTemplate": {
				"body": "<p>Hello,</p>\n<p>Thank you for joining us at {APP_NAME}.</p>\n<p>Click on the button below to verify your email address.</p>\n<p>\n  <a class=\"btn\" href=\"{APP_URL}/_/#/auth/confirm-verification/{TOKEN}\" target=\"_blank\" rel=\"noopener\">Verify</a>\n</p>\n<p><i>If you didn't recently register, please ignore this email.</i></p>\n<p>\n  Thanks,<br/>\n  {APP_NAME} team\n</p>",
				"subject": "Verify your {APP_NAME} email"
			},
			"verificationToken": {
				"duration": 86400
			},
			"viewRule": "id = \"1\""
		}` + "`" + `

		collection := &core.Collection{}
		if err := json.Unmarshal([]byte(jsonData), &collection); err != nil {
			return err
		}

		return app.Save(collection)
	}, func(app core.App) error {
		collection, err := app.FindCollectionByNameOrId("@TEST_RANDOM")
		if err != nil {
			return err
		}

		return app.Delete(collection)
	})
}
`,
		},
	}

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

			migrationsDir := filepath.Join(app.DataDir(), "_test_migrations")

			migratecmd.MustRegister(app, nil, migratecmd.Config{
				TemplateLang: s.lang,
				Automigrate:  true,
				Dir:          migrationsDir,
			})

			app.Bootstrap()

			collection := core.NewAuthCollection("new_name")
			collection.System = true
			collection.ListRule = types.Pointer("@request.auth.id != '' && 1 > 0 || 'backtick`test' = 0")
			collection.ViewRule = types.Pointer(`id = "1"`)
			collection.Indexes = types.JSONArray[string]{"create index test on new_name (id)"}
			collection.ManageRule = types.Pointer("1 != 2")
			//  should be ignored
			collection.OAuth2.Providers = []core.OAuth2ProviderConfig{{Name: "gitlab", ClientId: "abc", ClientSecret: "123"}}
			testSecret := strings.Repeat("a", 30)
			collection.AuthToken.Secret = testSecret
			collection.FileToken.Secret = testSecret
			collection.EmailChangeToken.Secret = testSecret
			collection.PasswordResetToken.Secret = testSecret
			collection.VerificationToken.Secret = testSecret

			// save the newly created dummy collection (with mock request event)
			event := new(core.CollectionRequestEvent)
			event.RequestEvent = &core.RequestEvent{}
			event.App = app
			event.Collection = collection
			err := app.OnCollectionCreateRequest().Trigger(event, func(e *core.CollectionRequestEvent) error {
				return e.App.Save(e.Collection)
			})
			if err != nil {
				t.Fatalf("Failed to save the created dummy collection, got: %v", err)
			}

			files, err := os.ReadDir(migrationsDir)
			if err != nil {
				t.Fatalf("Expected migrationsDir to be created, got %v", err)
			}

			if total := len(files); total != 1 {
				t.Fatalf("Expected 1 file to be generated, got %d: %v", total, files)
			}

			expectedName := "_created_new_name." + s.lang
			if !strings.Contains(files[0].Name(), expectedName) {
				t.Fatalf("Expected filename to contains %q, got %q", expectedName, files[0].Name())
			}

			fullPath := filepath.Join(migrationsDir, files[0].Name())
			content, err := os.ReadFile(fullPath)
			if err != nil {
				t.Fatalf("Failed to read the generated migration file: %v", err)
			}
			contentStr := strings.TrimSpace(string(content))

			// replace @TEST_RANDOM placeholder with a regex pattern
			expectedTemplate := strings.ReplaceAll(
				"^"+regexp.QuoteMeta(strings.TrimSpace(s.expectedTemplate))+"$",
				"@TEST_RANDOM",
				`\w+`,
			)
			if !list.ExistInSliceWithRegex(contentStr, []string{expectedTemplate}) {
				t.Fatalf("Expected template \n%v \ngot \n%v", s.expectedTemplate, contentStr)
			}
		})
	}
}

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

	scenarios := []struct {
		lang             string
		expectedTemplate string
	}{
		{
			migratecmd.TemplateLangJS,
			`
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
  const collection = app.findCollectionByNameOrId("@TEST_RANDOM");

  return app.delete(collection);
}, (app) => {
  const collection = new Collection({
    "authAlert": {
      "emailTemplate": {
        "body": "<p>Hello,</p>\n<p>We noticed a login to your {APP_NAME} account from a new location:</p>\n<p><em>{ALERT_INFO}</em></p>\n<p><strong>If this wasn't you, you should immediately change your {APP_NAME} account password to revoke access from all other locations.</strong></p>\n<p>If this was you, you may disregard this email.</p>\n<p>\n  Thanks,<br/>\n  {APP_NAME} team\n</p>",
        "subject": "Login from a new location"
      },
      "enabled": true
    },
    "authRule": "",
    "authToken": {
      "duration": 432000
    },
    "confirmEmailChangeTemplate": {
      "body": "<p>Hello,</p>\n<p>Click on the button below to confirm your new email address.</p>\n<p>\n  <a class=\"btn\" href=\"{APP_URL}/_/#/auth/confirm-email-change/{TOKEN}\" target=\"_blank\" rel=\"noopener\">Confirm new email</a>\n</p>\n<p><i>If you didn't ask to change your email address, please ignore this email.</i></p>\n<p>\n  Thanks,<br/>\n  {APP_NAME} team\n</p>",
      "subject": "Confirm your {APP_NAME} new email address"
    },
    "createRule": null,
    "deleteRule": null,
    "emailChangeToken": {
      "duration": 1800
    },
    "fields": [
      {
        "autogeneratePattern": "[a-z0-9]{15}",
        "help": "",
        "hidden": false,
        "id": "text@TEST_RANDOM",
        "max": 15,
        "min": 15,
        "name": "id",
        "pattern": "^[a-z0-9]+$",
        "presentable": false,
        "primaryKey": true,
        "required": true,
        "system": true,
        "type": "text"
      },
      {
        "cost": 0,
        "help": "",
        "hidden": true,
        "id": "password@TEST_RANDOM",
        "max": 0,
        "min": 8,
        "name": "password",
        "pattern": "",
        "presentable": false,
        "required": true,
        "system": true,
        "type": "password"
      },
      {
        "autogeneratePattern": "[a-zA-Z0-9]{50}",
        "help": "",
        "hidden": true,
        "id": "text@TEST_RANDOM",
        "max": 60,
        "min": 30,
        "name": "tokenKey",
        "pattern": "",
        "presentable": false,
        "primaryKey": false,
        "required": true,
        "system": true,
        "type": "text"
      },
      {
        "exceptDomains": null,
        "help": "",
        "hidden": false,
        "id": "email3885137012",
        "name": "email",
        "onlyDomains": null,
        "presentable": false,
        "required": true,
        "system": true,
        "type": "email"
      },
      {
        "help": "",
        "hidden": false,
        "id": "bool@TEST_RANDOM",
        "name": "emailVisibility",
        "presentable": false,
        "required": false,
        "system": true,
        "type": "bool"
      },
      {
        "help": "",
        "hidden": false,
        "id": "bool256245529",
        "name": "verified",
        "presentable": false,
        "required": false,
        "system": true,
        "type": "bool"
      }
    ],
    "fileToken": {
      "duration": 180
    },
    "id": "@TEST_RANDOM",
    "indexes": [
      "create index test on test123 (id)",
      "CREATE UNIQUE INDEX ` + "`" + `idx_tokenKey_@TEST_RANDOM` + "`" + ` ON ` + "`" + `test123` + "`" + ` (` + "`" + `tokenKey` + "`" + `)",
      "CREATE UNIQUE INDEX ` + "`" + `idx_email_@TEST_RANDOM` + "`" + ` ON ` + "`" + `test123` + "`" + ` (` + "`" + `email` + "`" + `) WHERE ` + "`" + `email` + "`" + ` != ''"
    ],
    "listRule": "@request.auth.id != '' && 1 > 0 || 'backtick` + "`" + `test' = 0",
    "manageRule": "1 != 2",
    "mfa": {
      "duration": 600,
      "enabled": false,
      "rule": ""
    },
    "name": "test123",
    "oauth2": {
      "enabled": false,
      "mappedFields": {
        "avatarURL": "",
        "id": "",
        "name": "",
        "username": ""
      }
    },
    "otp": {
      "duration": 180,
      "emailTemplate": {
        "body": "<p>Hello,</p>\n<p>Your one-time password is: <strong>{OTP}</strong></p>\n<p><i>If you didn't ask for the one-time password, you can ignore this email.</i></p>\n<p>\n  Thanks,<br/>\n  {APP_NAME} team\n</p>",
        "subject": "OTP for {APP_NAME}"
      },
      "enabled": false,
      "length": 8
    },
    "passwordAuth": {
      "enabled": true,
      "identityFields": [
        "email"
      ]
    },
    "passwordResetToken": {
      "duration": 1800
    },
    "resetPasswordTemplate": {
      "body": "<p>Hello,</p>\n<p>Click on the button below to reset your password.</p>\n<p>\n  <a class=\"btn\" href=\"{APP_URL}/_/#/auth/confirm-password-reset/{TOKEN}\" target=\"_blank\" rel=\"noopener\">Reset password</a>\n</p>\n<p><i>If you didn't ask to reset your password, please ignore this email.</i></p>\n<p>\n  Thanks,<br/>\n  {APP_NAME} team\n</p>",
      "subject": "Reset your {APP_NAME} password"
    },
    "system": false,
    "type": "auth",
    "updateRule": null,
    "verificationTemplate": {
      "body": "<p>Hello,</p>\n<p>Thank you for joining us at {APP_NAME}.</p>\n<p>Click on the button below to verify your email address.</p>\n<p>\n  <a class=\"btn\" href=\"{APP_URL}/_/#/auth/confirm-verification/{TOKEN}\" target=\"_blank\" rel=\"noopener\">Verify</a>\n</p>\n<p><i>If you didn't recently register, please ignore this email.</i></p>\n<p>\n  Thanks,<br/>\n  {APP_NAME} team\n</p>",
      "subject": "Verify your {APP_NAME} email"
    },
    "verificationToken": {
      "duration": 86400
    },
    "viewRule": "id = \"1\""
  });

  return app.save(collection);
})
`,
		},
		{
			migratecmd.TemplateLangGo,
			`
package _test_migrations

import (
	"encoding/json"

	"github.com/pocketbase/pocketbase/core"
	m "github.com/pocketbase/pocketbase/migrations"
)

func init() {
	m.Register(func(app core.App) error {
		collection, err := app.FindCollectionByNameOrId("@TEST_RANDOM")
		if err != nil {
			return err
		}

		return app.Delete(collection)
	}, func(app core.App) error {
		jsonData := ` + "`" + `{
			"authAlert": {
				"emailTemplate": {
					"body": "<p>Hello,</p>\n<p>We noticed a login to your {APP_NAME} account from a new location:</p>\n<p><em>{ALERT_INFO}</em></p>\n<p><strong>If this wasn't you, you should immediately change your {APP_NAME} account password to revoke access from all other locations.</strong></p>\n<p>If this was you, you may disregard this email.</p>\n<p>\n  Thanks,<br/>\n  {APP_NAME} team\n</p>",
					"subject": "Login from a new location"
				},
				"enabled": true
			},
			"authRule": "",
			"authToken": {
				"duration": 432000
			},
			"confirmEmailChangeTemplate": {
				"body": "<p>Hello,</p>\n<p>Click on the button below to confirm your new email address.</p>\n<p>\n  <a class=\"btn\" href=\"{APP_URL}/_/#/auth/confirm-email-change/{TOKEN}\" target=\"_blank\" rel=\"noopener\">Confirm new email</a>\n</p>\n<p><i>If you didn't ask to change your email address, please ignore this email.</i></p>\n<p>\n  Thanks,<br/>\n  {APP_NAME} team\n</p>",
				"subject": "Confirm your {APP_NAME} new email address"
			},
			"createRule": null,
			"deleteRule": null,
			"emailChangeToken": {
				"duration": 1800
			},
			"fields": [
				{
					"autogeneratePattern": "[a-z0-9]{15}",
					"help": "",
					"hidden": false,
					"id": "text@TEST_RANDOM",
					"max": 15,
					"min": 15,
					"name": "id",
					"pattern": "^[a-z0-9]+$",
					"presentable": false,
					"primaryKey": true,
					"required": true,
					"system": true,
					"type": "text"
				},
				{
					"cost": 0,
					"help": "",
					"hidden": true,
					"id": "password@TEST_RANDOM",
					"max": 0,
					"min": 8,
					"name": "password",
					"pattern": "",
					"presentable": false,
					"required": true,
					"system": true,
					"type": "password"
				},
				{
					"autogeneratePattern": "[a-zA-Z0-9]{50}",
					"help": "",
					"hidden": true,
					"id": "text@TEST_RANDOM",
					"max": 60,
					"min": 30,
					"name": "tokenKey",
					"pattern": "",
					"presentable": false,
					"primaryKey": false,
					"required": true,
					"system": true,
					"type": "text"
				},
				{
					"exceptDomains": null,
					"help": "",
					"hidden": false,
					"id": "email3885137012",
					"name": "email",
					"onlyDomains": null,
					"presentable": false,
					"required": true,
					"system": true,
					"type": "email"
				},
				{
					"help": "",
					"hidden": false,
					"id": "bool@TEST_RANDOM",
					"name": "emailVisibility",
					"presentable": false,
					"required": false,
					"system": true,
					"type": "bool"
				},
				{
					"help": "",
					"hidden": false,
					"id": "bool256245529",
					"name": "verified",
					"presentable": false,
					"required": false,
					"system": true,
					"type": "bool"
				}
			],
			"fileToken": {
				"duration": 180
			},
			"id": "@TEST_RANDOM",
			"indexes": [
				"create index test on test123 (id)",
				"CREATE UNIQUE INDEX ` + "` + \"`\" + `" + `idx_tokenKey_@TEST_RANDOM` + "` + \"`\" + `" + ` ON ` + "` + \"`\" + `" + `test123` + "` + \"`\" + `" + ` (` + "` + \"`\" + `" + `tokenKey` + "` + \"`\" + `" + `)",
				"CREATE UNIQUE INDEX ` + "` + \"`\" + `" + `idx_email_@TEST_RANDOM` + "` + \"`\" + `" + ` ON ` + "` + \"`\" + `" + `test123` + "` + \"`\" + `" + ` (` + "` + \"`\" + `" + `email` + "` + \"`\" + `" + `) WHERE ` + "` + \"`\" + `" + `email` + "` + \"`\" + `" + ` != ''"
			],
			"listRule": "@request.auth.id != '' && 1 > 0 || 'backtick` + "` + \"`\" + `" + `test' = 0",
			"manageRule": "1 != 2",
			"mfa": {
				"duration": 600,
				"enabled": false,
				"rule": ""
			},
			"name": "test123",
			"oauth2": {
				"enabled": false,
				"mappedFields": {
					"avatarURL": "",
					"id": "",
					"name": "",
					"username": ""
				}
			},
			"otp": {
				"duration": 180,
				"emailTemplate": {
					"body": "<p>Hello,</p>\n<p>Your one-time password is: <strong>{OTP}</strong></p>\n<p><i>If you didn't ask for the one-time password, you can ignore this email.</i></p>\n<p>\n  Thanks,<br/>\n  {APP_NAME} team\n</p>",
					"subject": "OTP for {APP_NAME}"
				},
				"enabled": false,
				"length": 8
			},
			"passwordAuth": {
				"enabled": true,
				"identityFields": [
					"email"
				]
			},
			"passwordResetToken": {
				"duration": 1800
			},
			"resetPasswordTemplate": {
				"body": "<p>Hello,</p>\n<p>Click on the button below to reset your password.</p>\n<p>\n  <a class=\"btn\" href=\"{APP_URL}/_/#/auth/confirm-password-reset/{TOKEN}\" target=\"_blank\" rel=\"noopener\">Reset password</a>\n</p>\n<p><i>If you didn't ask to reset your password, please ignore this email.</i></p>\n<p>\n  Thanks,<br/>\n  {APP_NAME} team\n</p>",
				"subject": "Reset your {APP_NAME} password"
			},
			"system": false,
			"type": "auth",
			"updateRule": null,
			"verificationTemplate": {
				"body": "<p>Hello,</p>\n<p>Thank you for joining us at {APP_NAME}.</p>\n<p>Click on the button below to verify your email address.</p>\n<p>\n  <a class=\"btn\" href=\"{APP_URL}/_/#/auth/confirm-verification/{TOKEN}\" target=\"_blank\" rel=\"noopener\">Verify</a>\n</p>\n<p><i>If you didn't recently register, please ignore this email.</i></p>\n<p>\n  Thanks,<br/>\n  {APP_NAME} team\n</p>",
				"subject": "Verify your {APP_NAME} email"
			},
			"verificationToken": {
				"duration": 86400
			},
			"viewRule": "id = \"1\""
		}` + "`" + `

		collection := &core.Collection{}
		if err := json.Unmarshal([]byte(jsonData), &collection); err != nil {
			return err
		}

		return app.Save(collection)
	})
}
`,
		},
	}

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

			migrationsDir := filepath.Join(app.DataDir(), "_test_migrations")

			// create dummy collection
			collection := core.NewAuthCollection("test123")
			collection.ListRule = types.Pointer("@request.auth.id != '' && 1 > 0 || 'backtick`test' = 0")
			collection.ViewRule = types.Pointer(`id = "1"`)
			collection.Indexes = types.JSONArray[string]{"create index test on test123 (id)"}
			collection.ManageRule = types.Pointer("1 != 2")
			if err := app.Save(collection); err != nil {
				t.Fatalf("Failed to save dummy collection, got: %v", err)
			}

			migratecmd.MustRegister(app, nil, migratecmd.Config{
				TemplateLang: s.lang,
				Automigrate:  true,
				Dir:          migrationsDir,
			})

			app.Bootstrap()

			// delete the newly created dummy collection (with mock request event)
			event := new(core.CollectionRequestEvent)
			event.RequestEvent = &core.RequestEvent{}
			event.App = app
			event.Collection = collection
			err := app.OnCollectionDeleteRequest().Trigger(event, func(e *core.CollectionRequestEvent) error {
				return e.App.Delete(e.Collection)
			})
			if err != nil {
				t.Fatalf("Failed to delete dummy collection, got: %v", err)
			}

			files, err := os.ReadDir(migrationsDir)
			if err != nil {
				t.Fatalf("Expected migrationsDir to be created, got: %v", err)
			}

			if total := len(files); total != 1 {
				t.Fatalf("Expected 1 file to be generated, got %d", total)
			}

			expectedName := "_deleted_test123." + s.lang
			if !strings.Contains(files[0].Name(), expectedName) {
				t.Fatalf("Expected filename to contains %q, got %q", expectedName, files[0].Name())
			}

			fullPath := filepath.Join(migrationsDir, files[0].Name())
			content, err := os.ReadFile(fullPath)
			if err != nil {
				t.Fatalf("Failed to read the generated migration file: %v", err)
			}
			contentStr := strings.TrimSpace(string(content))

			// replace @TEST_RANDOM placeholder with a regex pattern
			expectedTemplate := strings.ReplaceAll(
				"^"+regexp.QuoteMeta(strings.TrimSpace(s.expectedTemplate))+"$",
				"@TEST_RANDOM",
				`\w+`,
			)
			if !list.ExistInSliceWithRegex(contentStr, []string{expectedTemplate}) {
				t.Fatalf("Expected template \n%v \ngot \n%v", s.expectedTemplate, contentStr)
			}
		})
	}
}

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

	scenarios := []struct {
		lang             string
		expectedTemplate string
	}{
		{
			migratecmd.TemplateLangJS,
			`
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
  const collection = app.findCollectionByNameOrId("@TEST_RANDOM")

  // update collection data
  unmarshal({
    "createRule": "id = \"nil_update\"",
    "deleteRule": null,
    "fileToken": {
      "duration": 10
    },
    "indexes": [
      "create index test1 on test123_update (f1_name)",
      "CREATE UNIQUE INDEX ` + "`" + `idx_tokenKey_@TEST_RANDOM` + "`" + ` ON ` + "`" + `test123_update` + "`" + ` (` + "`" + `tokenKey` + "`" + `)",
      "CREATE UNIQUE INDEX ` + "`" + `idx_email_@TEST_RANDOM` + "`" + ` ON ` + "`" + `test123_update` + "`" + ` (` + "`" + `email` + "`" + `) WHERE ` + "`" + `email` + "`" + ` != ''"
    ],
    "listRule": "@request.auth.id != ''",
    "name": "test123_update",
    "oauth2": {
      "enabled": true
    },
    "updateRule": "id = \"2_update\""
  }, collection)

  // remove field
  collection.fields.removeById("f3_id")

  // add field
  collection.fields.addAt(8, new Field({
    "autogeneratePattern": "",
    "help": "",
    "hidden": false,
    "id": "f4_id",
    "max": 0,
    "min": 0,
    "name": "f4_name",
    "pattern": "` + "`" + `test backtick` + "`" + `123",
    "presentable": false,
    "primaryKey": false,
    "required": false,
    "system": false,
    "type": "text"
  }))

  // update field
  collection.fields.addAt(7, new Field({
    "help": "",
    "hidden": false,
    "id": "f2_id",
    "max": null,
    "min": 10,
    "name": "f2_name_new",
    "onlyInt": false,
    "presentable": false,
    "required": false,
    "system": false,
    "type": "number"
  }))

  return app.save(collection)
}, (app) => {
  const collection = app.findCollectionByNameOrId("@TEST_RANDOM")

  // update collection data
  unmarshal({
    "createRule": null,
    "deleteRule": "id = \"3\"",
    "fileToken": {
      "duration": 180
    },
    "indexes": [
      "create index test1 on test123 (f1_name)",
      "CREATE UNIQUE INDEX ` + "`" + `idx_tokenKey_@TEST_RANDOM` + "`" + ` ON ` + "`" + `test123` + "`" + ` (` + "`" + `tokenKey` + "`" + `)",
      "CREATE UNIQUE INDEX ` + "`" + `idx_email_@TEST_RANDOM` + "`" + ` ON ` + "`" + `test123` + "`" + ` (` + "`" + `email` + "`" + `) WHERE ` + "`" + `email` + "`" + ` != ''"
    ],
    "listRule": "@request.auth.id != '' && 1 != 2",
    "name": "test123",
    "oauth2": {
      "enabled": false
    },
    "updateRule": "id = \"2\""
  }, collection)

  // add field
  collection.fields.addAt(8, new Field({
    "help": "",
    "hidden": false,
    "id": "f3_id",
    "name": "f3_name",
    "presentable": false,
    "required": false,
    "system": false,
    "type": "bool"
  }))

  // remove field
  collection.fields.removeById("f4_id")

  // update field
  collection.fields.addAt(7, new Field({
    "help": "",
    "hidden": false,
    "id": "f2_id",
    "max": null,
    "min": 10,
    "name": "f2_name",
    "onlyInt": false,
    "presentable": false,
    "required": false,
    "system": false,
    "type": "number"
  }))

  return app.save(collection)
})

`,
		},
		{
			migratecmd.TemplateLangGo,
			`
package _test_migrations

import (
	"encoding/json"

	"github.com/pocketbase/pocketbase/core"
	m "github.com/pocketbase/pocketbase/migrations"
)

func init() {
	m.Register(func(app core.App) error {
		collection, err := app.FindCollectionByNameOrId("@TEST_RANDOM")
		if err != nil {
			return err
		}

		// update collection data
		if err := json.Unmarshal([]byte(` + "`" + `{
			"createRule": "id = \"nil_update\"",
			"deleteRule": null,
			"fileToken": {
				"duration": 10
			},
			"indexes": [
				"create index test1 on test123_update (f1_name)",
				"CREATE UNIQUE INDEX ` + "` + \"`\" + `" + `idx_tokenKey_@TEST_RANDOM` + "` + \"`\" + `" + ` ON ` + "` + \"`\" + `" + `test123_update` + "` + \"`\" + `" + ` (` + "` + \"`\" + `" + `tokenKey` + "` + \"`\" + `" + `)",
				"CREATE UNIQUE INDEX ` + "` + \"`\" + `" + `idx_email_@TEST_RANDOM` + "` + \"`\" + `" + ` ON ` + "` + \"`\" + `" + `test123_update` + "` + \"`\" + `" + ` (` + "` + \"`\" + `" + `email` + "` + \"`\" + `" + `) WHERE ` + "` + \"`\" + `" + `email` + "` + \"`\" + `" + ` != ''"
			],
			"listRule": "@request.auth.id != ''",
			"name": "test123_update",
			"oauth2": {
				"enabled": true
			},
			"updateRule": "id = \"2_update\""
		}` + "`" + `), &collection); err != nil {
			return err
		}

		// remove field
		collection.Fields.RemoveById("f3_id")

		// add field
		if err := collection.Fields.AddMarshaledJSONAt(8, []byte(` + "`" + `{
			"autogeneratePattern": "",
			"help": "",
			"hidden": false,
			"id": "f4_id",
			"max": 0,
			"min": 0,
			"name": "f4_name",
			"pattern": "` + "` + \"`\" + `" + `test backtick` + "` + \"`\" + `" + `123",
			"presentable": false,
			"primaryKey": false,
			"required": false,
			"system": false,
			"type": "text"
		}` + "`" + `)); err != nil {
			return err
		}

		// update field
		if err := collection.Fields.AddMarshaledJSONAt(7, []byte(` + "`" + `{
			"help": "",
			"hidden": false,
			"id": "f2_id",
			"max": null,
			"min": 10,
			"name": "f2_name_new",
			"onlyInt": false,
			"presentable": false,
			"required": false,
			"system": false,
			"type": "number"
		}` + "`" + `)); err != nil {
			return err
		}

		return app.Save(collection)
	}, func(app core.App) error {
		collection, err := app.FindCollectionByNameOrId("@TEST_RANDOM")
		if err != nil {
			return err
		}

		// update collection data
		if err := json.Unmarshal([]byte(` + "`" + `{
			"createRule": null,
			"deleteRule": "id = \"3\"",
			"fileToken": {
				"duration": 180
			},
			"indexes": [
				"create index test1 on test123 (f1_name)",
				"CREATE UNIQUE INDEX ` + "` + \"`\" + `" + `idx_tokenKey_@TEST_RANDOM` + "` + \"`\" + `" + ` ON ` + "` + \"`\" + `" + `test123` + "` + \"`\" + `" + ` (` + "` + \"`\" + `" + `tokenKey` + "` + \"`\" + `" + `)",
				"CREATE UNIQUE INDEX ` + "` + \"`\" + `" + `idx_email_@TEST_RANDOM` + "` + \"`\" + `" + ` ON ` + "` + \"`\" + `" + `test123` + "` + \"`\" + `" + ` (` + "` + \"`\" + `" + `email` + "` + \"`\" + `" + `) WHERE ` + "` + \"`\" + `" + `email` + "` + \"`\" + `" + ` != ''"
			],
			"listRule": "@request.auth.id != '' && 1 != 2",
			"name": "test123",
			"oauth2": {
				"enabled": false
			},
			"updateRule": "id = \"2\""
		}` + "`" + `), &collection); err != nil {
			return err
		}

		// add field
		if err := collection.Fields.AddMarshaledJSONAt(8, []byte(` + "`" + `{
			"help": "",
			"hidden": false,
			"id": "f3_id",
			"name": "f3_name",
			"presentable": false,
			"required": false,
			"system": false,
			"type": "bool"
		}` + "`" + `)); err != nil {
			return err
		}

		// remove field
		collection.Fields.RemoveById("f4_id")

		// update field
		if err := collection.Fields.AddMarshaledJSONAt(7, []byte(` + "`" + `{
			"help": "",
			"hidden": false,
			"id": "f2_id",
			"max": null,
			"min": 10,
			"name": "f2_name",
			"onlyInt": false,
			"presentable": false,
			"required": false,
			"system": false,
			"type": "number"
		}` + "`" + `)); err != nil {
			return err
		}

		return app.Save(collection)
	})
}
`,
		},
	}

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

			migrationsDir := filepath.Join(app.DataDir(), "_test_migrations")

			// create dummy collection
			collection := core.NewAuthCollection("test123")
			collection.ListRule = types.Pointer("@request.auth.id != '' && 1 != 2")
			collection.ViewRule = types.Pointer(`id = "1"`)
			collection.UpdateRule = types.Pointer(`id = "2"`)
			collection.CreateRule = nil
			collection.DeleteRule = types.Pointer(`id = "3"`)
			collection.Indexes = types.JSONArray[string]{"create index test1 on test123 (f1_name)"}
			collection.ManageRule = types.Pointer("1 != 2")
			collection.Fields.Add(&core.TextField{
				Id:       "f1_id",
				Name:     "f1_name",
				Required: true,
			})
			collection.Fields.Add(&core.NumberField{
				Id:   "f2_id",
				Name: "f2_name",
				Min:  types.Pointer(10.0),
			})
			collection.Fields.Add(&core.BoolField{
				Id:   "f3_id",
				Name: "f3_name",
			})

			if err := app.Save(collection); err != nil {
				t.Fatalf("Failed to save dummy collection, got %v", err)
			}

			// init plugin
			migratecmd.MustRegister(app, nil, migratecmd.Config{
				TemplateLang: s.lang,
				Automigrate:  true,
				Dir:          migrationsDir,
			})
			app.Bootstrap()

			// update the dummy collection
			collection.Name = "test123_update"
			collection.ListRule = types.Pointer("@request.auth.id != ''")
			collection.ViewRule = types.Pointer(`id = "1"`) // no change
			collection.UpdateRule = types.Pointer(`id = "2_update"`)
			collection.CreateRule = types.Pointer(`id = "nil_update"`)
			collection.DeleteRule = nil
			collection.Indexes = types.JSONArray[string]{
				"create index test1 on test123_update (f1_name)",
			}
			collection.Fields.RemoveById("f3_id")
			collection.Fields.Add(&core.TextField{
				Id:      "f4_id",
				Name:    "f4_name",
				Pattern: "`test backtick`123",
			})
			f := collection.Fields.GetById("f2_id")
			f.SetName("f2_name_new")
			collection.OAuth2.Enabled = true
			collection.FileToken.Duration = 10
			//  should be ignored
			collection.OAuth2.Providers = []core.OAuth2ProviderConfig{{Name: "gitlab", ClientId: "abc", ClientSecret: "123"}}
			testSecret := strings.Repeat("b", 30)
			collection.AuthToken.Secret = testSecret
			collection.FileToken.Secret = testSecret
			collection.EmailChangeToken.Secret = testSecret
			collection.PasswordResetToken.Secret = testSecret
			collection.VerificationToken.Secret = testSecret

			// save the changes and trigger automigrate (with mock request event)
			event := new(core.CollectionRequestEvent)
			event.RequestEvent = &core.RequestEvent{}
			event.App = app
			event.Collection = collection
			err := app.OnCollectionUpdateRequest().Trigger(event, func(e *core.CollectionRequestEvent) error {
				return e.App.Save(e.Collection)
			})
			if err != nil {
				t.Fatalf("Failed to save dummy collection changes, got %v", err)
			}

			files, err := os.ReadDir(migrationsDir)
			if err != nil {
				t.Fatalf("Expected migrationsDir to be created, got: %v", err)
			}

			if total := len(files); total != 1 {
				t.Fatalf("Expected 1 file to be generated, got %d", total)
			}

			expectedName := "_updated_test123." + s.lang
			if !strings.Contains(files[0].Name(), expectedName) {
				t.Fatalf("Expected filename to contains %q, got %q", expectedName, files[0].Name())
			}

			fullPath := filepath.Join(migrationsDir, files[0].Name())
			content, err := os.ReadFile(fullPath)
			if err != nil {
				t.Fatalf("Failed to read the generated migration file: %v", err)
			}
			contentStr := strings.TrimSpace(string(content))

			// replace @TEST_RANDOM placeholder with a regex pattern
			expectedTemplate := strings.ReplaceAll(
				"^"+regexp.QuoteMeta(strings.TrimSpace(s.expectedTemplate))+"$",
				"@TEST_RANDOM",
				`\w+`,
			)
			if !list.ExistInSliceWithRegex(contentStr, []string{expectedTemplate}) {
				t.Fatalf("Expected template \n%v \ngot \n%v", s.expectedTemplate, contentStr)
			}
		})
	}
}

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

	scenarios := []struct {
		lang string
	}{
		{
			migratecmd.TemplateLangJS,
		},
		{
			migratecmd.TemplateLangGo,
		},
	}

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

			migrationsDir := filepath.Join(app.DataDir(), "_test_migrations")

			// create dummy collection
			collection := core.NewAuthCollection("test123")

			if err := app.Save(collection); err != nil {
				t.Fatalf("Failed to save dummy collection, got %v", err)
			}

			// init plugin
			migratecmd.MustRegister(app, nil, migratecmd.Config{
				TemplateLang: s.lang,
				Automigrate:  true,
				Dir:          migrationsDir,
			})
			app.Bootstrap()

			//  should be ignored
			collection.OAuth2.Providers = []core.OAuth2ProviderConfig{{Name: "gitlab", ClientId: "abc", ClientSecret: "123"}}
			testSecret := strings.Repeat("b", 30)
			collection.AuthToken.Secret = testSecret
			collection.FileToken.Secret = testSecret
			collection.EmailChangeToken.Secret = testSecret
			collection.PasswordResetToken.Secret = testSecret
			collection.VerificationToken.Secret = testSecret

			// resave without other changes and trigger automigrate (with mock request event)
			event := new(core.CollectionRequestEvent)
			event.RequestEvent = &core.RequestEvent{}
			event.App = app
			event.Collection = collection
			err := app.OnCollectionUpdateRequest().Trigger(event, func(e *core.CollectionRequestEvent) error {
				return e.App.Save(e.Collection)
			})
			if err != nil {
				t.Fatalf("Failed to save dummy collection update, got %v", err)
			}

			files, _ := os.ReadDir(migrationsDir)
			if total := len(files); total != 0 {
				t.Fatalf("Expected 0 files to be generated, got %d", total)
			}
		})
	}
}
