//go:build sync && scrub && metrics && search && userprefs && mgmt && imagetrust && events
// +build sync,scrub,metrics,search,userprefs,mgmt,imagetrust,events

package server_test

import (
	"fmt"
	"net/http"
	"os"
	"testing"
	"time"

	. "github.com/smartystreets/goconvey/convey"
	"gopkg.in/resty.v1"

	zerr "zotregistry.dev/zot/errors"
	"zotregistry.dev/zot/pkg/api/config"
	cli "zotregistry.dev/zot/pkg/cli/server"
	. "zotregistry.dev/zot/pkg/test/common"
)

const readLogFileTimeout = 5 * time.Second

func TestVerifyExtensionsConfig(t *testing.T) {
	oldArgs := os.Args

	defer func() { os.Args = oldArgs }()

	Convey("Test verify CVE warn for remote storage", t, func(c C) {
		tmpfile, err := os.CreateTemp("", "zot-test*.json")
		So(err, ShouldBeNil)
		defer os.Remove(tmpfile.Name()) // clean up

		content := fmt.Sprintf(`{
			"storage":{
				"rootDirectory":"%s",
				"dedupe":true,
				"remoteCache":false,
				"storageDriver":{
					"name":"s3",
					"rootdirectory":"/zot",
					"region":"us-east-2",
					"bucket":"zot-storage",
					"secure":true,
					"skipverify":false
				}
			},
			"http":{
				"address":"127.0.0.1",
				"port":"8080"
			},
			"extensions":{
				"search": {
					"enable": true,
					"cve": {
						"updateInterval": "24h"
					}
				}
			}
		}`, t.TempDir())

		err = os.WriteFile(tmpfile.Name(), []byte(content), 0o0600)
		So(err, ShouldBeNil)

		os.Args = []string{"cli_test", "verify", tmpfile.Name()}

		So(cli.NewServerRootCmd().Execute(), ShouldNotBeNil)

		content = fmt.Sprintf(`{
			"storage":{
				"rootDirectory":"%s",
				"dedupe":true,
				"remoteCache":false,
				"subPaths":{
					"/a": {
						"rootDirectory": "%s",
						"dedupe": false,
						"storageDriver":{
							"name":"s3",
							"rootdirectory":"/zot-a",
							"region":"us-east-2",
							"bucket":"zot-storage",
							"secure":true,
							"skipverify":false
						}
					}
				}
			},
			"http":{
				"address":"127.0.0.1",
				"port":"8080"
			},
			"extensions":{
				"search": {
					"enable": true,
					"cve": {
						"updateInterval": "24h"
					}
				}
			}
		}`, t.TempDir(), t.TempDir())
		err = os.WriteFile(tmpfile.Name(), []byte(content), 0o0600)
		So(err, ShouldBeNil)

		os.Args = []string{"cli_test", "verify", tmpfile.Name()}

		So(cli.NewServerRootCmd().Execute(), ShouldNotBeNil)
	})

	Convey("Test verify w/ sync and w/o filesystem storage", t, func(c C) {
		tmpfile, err := os.CreateTemp("", "zot-test*.json")
		So(err, ShouldBeNil)
		defer os.Remove(tmpfile.Name()) // clean up

		content := fmt.Sprintf(`{"storage":{"rootDirectory":"%s", "storageDriver": {"name": "s3"}},
							"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
							"auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}},
							"extensions":{"sync": {"registries": [{"urls":["localhost:9999"],
							"maxRetries": 1, "retryDelay": "10s"}]}}}`, t.TempDir())
		_, err = tmpfile.WriteString(content)
		So(err, ShouldBeNil)
		err = tmpfile.Close()
		So(err, ShouldBeNil)
		os.Args = []string{"cli_test", "verify", tmpfile.Name()}

		So(cli.NewServerRootCmd().Execute(), ShouldNotBeNil)
	})

	Convey("Test verify w/ sync and w/ filesystem storage", t, func(c C) {
		tmpfile, err := os.CreateTemp("", "zot-test*.json")
		So(err, ShouldBeNil)
		defer os.Remove(tmpfile.Name()) // clean up

		content := fmt.Sprintf(`{"storage":{"rootDirectory":"%s"},
							"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
							"auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}},
							"extensions":{"sync": {"registries": [{"urls":["localhost:9999"],
							"maxRetries": 1, "retryDelay": "10s"}]}}}`, t.TempDir())
		_, err = tmpfile.WriteString(content)
		So(err, ShouldBeNil)
		err = tmpfile.Close()
		So(err, ShouldBeNil)

		os.Args = []string{"cli_test", "verify", tmpfile.Name()}

		So(cli.NewServerRootCmd().Execute(), ShouldBeNil)
	})

	Convey("Test verify with bad sync prefixes", t, func(c C) {
		tmpfile, err := os.CreateTemp("", "zot-test*.json")
		So(err, ShouldBeNil)
		defer os.Remove(tmpfile.Name()) // clean up

		content := fmt.Sprintf(`{"storage":{"rootDirectory":"%s"},
							"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
							"auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}},
							"extensions":{"sync": {"registries": [{"urls":["localhost:9999"],
							"maxRetries": 1, "retryDelay": "10s",
							"content": [{"prefix":"[repo^&["}]}]}}}`, t.TempDir())
		_, err = tmpfile.WriteString(content)
		So(err, ShouldBeNil)
		err = tmpfile.Close()
		So(err, ShouldBeNil)

		os.Args = []string{"cli_test", "verify", tmpfile.Name()}

		So(cli.NewServerRootCmd().Execute(), ShouldNotBeNil)
	})

	Convey("Test verify with bad sync content config", t, func(c C) {
		tmpfile, err := os.CreateTemp("", "zot-test*.json")
		So(err, ShouldBeNil)
		defer os.Remove(tmpfile.Name()) // clean up

		content := fmt.Sprintf(`{"storage":{"rootDirectory":"%s"},
							"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
							"auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}},
							"extensions":{"sync": {"registries": [{"urls":["localhost:9999"],
							"maxRetries": 1, "retryDelay": "10s",
							"content": [{"prefix":"zot-repo","stripPrefix":true,"destination":"/"}]}]}}}`, t.TempDir())
		_, err = tmpfile.WriteString(content)
		So(err, ShouldBeNil)
		err = tmpfile.Close()
		So(err, ShouldBeNil)
		os.Args = []string{"cli_test", "verify", tmpfile.Name()}

		So(cli.NewServerRootCmd().Execute(), ShouldNotBeNil)
	})

	Convey("Test verify with good sync content config", t, func(c C) {
		tmpfile, err := os.CreateTemp("", "zot-test*.json")
		So(err, ShouldBeNil)
		defer os.Remove(tmpfile.Name()) // clean up

		content := fmt.Sprintf(`{"storage":{"rootDirectory":"%s"},
							"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
							"auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}},
							"extensions":{"sync": {"registries": [{"urls":["localhost:9999"],
							"maxRetries": 1, "retryDelay": "10s",
							"content": [{"prefix":"zot-repo/*","stripPrefix":true,"destination":"/"}]}]}}}`, t.TempDir())
		_, err = tmpfile.WriteString(content)
		So(err, ShouldBeNil)
		err = tmpfile.Close()
		So(err, ShouldBeNil)
		os.Args = []string{"cli_test", "verify", tmpfile.Name()}
		err = cli.NewServerRootCmd().Execute()
		So(err, ShouldBeNil)
	})

	Convey("Test verify sync config default tls value", t, func(c C) {
		tmpfile, err := os.CreateTemp("", "zot-test*.json")
		So(err, ShouldBeNil)
		defer os.Remove(tmpfile.Name()) // clean up

		content := fmt.Sprintf(`{"storage":{"rootDirectory":"%s"},
							"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
							"auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}},
							"extensions":{"sync": {"registries": [{"urls":["localhost:9999"],
							"maxRetries": 1, "retryDelay": "10s",
							"content": [{"prefix":"repo**"}]}]}}}`, t.TempDir())
		_, err = tmpfile.WriteString(content)
		So(err, ShouldBeNil)
		err = tmpfile.Close()
		So(err, ShouldBeNil)
		os.Args = []string{"cli_test", "verify", tmpfile.Name()}
		err = cli.NewServerRootCmd().Execute()
		So(err, ShouldBeNil)
	})

	Convey("Test verify sync without retry options", t, func(c C) {
		tmpfile, err := os.CreateTemp("", "zot-test*.json")
		So(err, ShouldBeNil)
		defer os.Remove(tmpfile.Name()) // clean up

		content := fmt.Sprintf(`{"storage":{"rootDirectory":"%s"},
							"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
							"auth":{"htpasswd":{"path":"test/data/htpasswd"},"failDelay":1}},
							"extensions":{"sync": {"registries": [{"urls":["localhost:9999"],
							"maxRetries": 10, "content": [{"prefix":"repo**"}]}]}}}`, t.TempDir())
		_, err = tmpfile.WriteString(content)
		So(err, ShouldBeNil)
		err = tmpfile.Close()
		So(err, ShouldBeNil)
		os.Args = []string{"cli_test", "verify", tmpfile.Name()}

		So(cli.NewServerRootCmd().Execute(), ShouldNotBeNil)
	})
}

func TestValidateExtensionsConfig(t *testing.T) {
	Convey("Legacy extensions should not error", t, func(c C) {
		config := config.New()
		tmpfile, err := os.CreateTemp("", "zot-test*.json")
		So(err, ShouldBeNil)

		defer os.Remove(tmpfile.Name())

		content := []byte(`{
			"storage": {
				"rootDirectory": "%/tmp/zot"
			},
			"http": {
				"address": "127.0.0.1",
				"port": "8080"
			},
			"log": {
				"level": "debug"
			},
			"extensions": {
				"mgmt": {
					"enable": "true"
				},
				"apikey": {
					"enable": "true"
				}
			}
		}`)
		err = os.WriteFile(tmpfile.Name(), content, 0o0600)
		So(err, ShouldBeNil)
		err = cli.LoadConfiguration(config, tmpfile.Name())
		So(err, ShouldBeNil)
	})

	Convey("Test missing extensions for UI to work", t, func(c C) {
		config := config.New()

		tmpfile, err := os.CreateTemp("", "zot-test*.json")
		So(err, ShouldBeNil)

		defer os.Remove(tmpfile.Name())

		content := []byte(`{
			"storage": {
				"rootDirectory": "%/tmp/zot"
			},
			"http": {
				"address": "127.0.0.1",
				"port": "8080"
			},
			"log": {
				"level": "debug"
			},
			"extensions": {
				"ui": {
					"enable": "true"
				}
			}
		}`)
		err = os.WriteFile(tmpfile.Name(), content, 0o0600)
		So(err, ShouldBeNil)
		err = cli.LoadConfiguration(config, tmpfile.Name())
		So(err, ShouldNotBeNil)
	})

	Convey("Test enabling UI extension with all prerequisites", t, func(c C) {
		config := config.New()
		tmpfile, err := os.CreateTemp("", "zot-test*.json")
		So(err, ShouldBeNil)

		defer os.Remove(tmpfile.Name())

		content := []byte(`{
			"storage": {
				"rootDirectory": "%/tmp/zot"
			},
			"http": {
				"address": "127.0.0.1",
				"port": "8080"
			},
			"log": {
				"level": "debug"
			},
			"extensions": {
				"ui": {
					"enable": "true"
				},
				"search": {
					"enable": "true"
				}
			}
		}`)
		err = os.WriteFile(tmpfile.Name(), content, 0o0600)
		So(err, ShouldBeNil)
		err = cli.LoadConfiguration(config, tmpfile.Name())
		So(err, ShouldBeNil)
	})

	Convey("Test extension are implicitly enabled", t, func(c C) {
		config := config.New()
		tmpfile, err := os.CreateTemp("", "zot-test*.json")
		So(err, ShouldBeNil)

		defer os.Remove(tmpfile.Name())

		content := []byte(`{
			"storage": {
				"rootDirectory": "%/tmp/zot"
			},
			"http": {
				"address": "127.0.0.1",
				"port": "8080"
			},
			"log": {
				"level": "debug"
			},
			"extensions": {
				"ui": {},
				"search": {},
				"metrics": {},
				"trust": {},
				"scrub": {}
			}
		}`)
		err = os.WriteFile(tmpfile.Name(), content, 0o0600)
		So(err, ShouldBeNil)
		err = cli.LoadConfiguration(config, tmpfile.Name())
		So(err, ShouldBeNil)
		So(config.Extensions.UI, ShouldNotBeNil)
		So(*config.Extensions.UI.Enable, ShouldBeTrue)
		So(config.Extensions.Search, ShouldNotBeNil)
		So(*config.Extensions.Search.Enable, ShouldBeTrue)
		So(config.Extensions.Trust, ShouldNotBeNil)
		So(*config.Extensions.Trust.Enable, ShouldBeTrue)
		So(*config.Extensions.Metrics, ShouldNotBeNil)
		So(*config.Extensions.Metrics.Enable, ShouldBeTrue)
		So(config.Extensions.Scrub, ShouldNotBeNil)
		So(*config.Extensions.Scrub.Enable, ShouldBeTrue)
	})
}

func TestServeExtensions(t *testing.T) {
	oldArgs := os.Args

	defer func() { os.Args = oldArgs }()

	Convey("config file with no extensions", t, func(c C) {
		port := GetFreePort()
		baseURL := GetBaseURL(port)
		logFile, err := os.CreateTemp("", "zot-log*.txt")
		So(err, ShouldBeNil)

		defer os.Remove(logFile.Name()) // clean up

		tmpFile := t.TempDir()

		content := fmt.Sprintf(`{
			"storage": {
				"rootDirectory": "%s"
			},
			"http": {
				"address": "127.0.0.1",
				"port": "%s"
			},
			"log": {
				"level": "debug",
				"output": "%s"
			}
		}`, tmpFile, port, logFile.Name())

		cfgfile, err := os.CreateTemp("", "zot-test*.json")
		So(err, ShouldBeNil)
		defer os.Remove(cfgfile.Name()) // clean up

		_, err = cfgfile.WriteString(content)
		So(err, ShouldBeNil)
		err = cfgfile.Close()
		So(err, ShouldBeNil)

		os.Args = []string{"cli_test", "serve", cfgfile.Name()}

		go func() {
			Convey("run", t, func() {
				err = cli.NewServerRootCmd().Execute()
				So(err, ShouldBeNil)
			})
		}()

		WaitTillServerReady(baseURL)

		data, err := os.ReadFile(logFile.Name())

		So(err, ShouldBeNil)
		So(string(data), ShouldContainSubstring, "\"Extensions\":null")
	})

	Convey("config file with empty extensions", t, func(c C) {
		port := GetFreePort()
		baseURL := GetBaseURL(port)
		logFile, err := os.CreateTemp("", "zot-log*.txt")
		So(err, ShouldBeNil)

		defer os.Remove(logFile.Name()) // clean up

		tmpFile := t.TempDir()

		content := fmt.Sprintf(`{
			"storage": {
				"rootDirectory": "%s"
			},
			"http": {
				"address": "127.0.0.1",
				"port": "%s"
			},
			"log": {
				"level": "debug",
				"output": "%s"
			},
			"extensions": {
			}
		}`, tmpFile, port, logFile.Name())

		cfgfile, err := os.CreateTemp("", "zot-test*.json")
		So(err, ShouldBeNil)
		defer os.Remove(cfgfile.Name()) // clean up

		_, err = cfgfile.WriteString(content)
		So(err, ShouldBeNil)
		err = cfgfile.Close()
		So(err, ShouldBeNil)

		os.Args = []string{"cli_test", "serve", cfgfile.Name()}

		go func() {
			Convey("run", t, func() {
				err = cli.NewServerRootCmd().Execute()
				So(err, ShouldBeNil)
			})
		}()

		WaitTillServerReady(baseURL)
		data, err := os.ReadFile(logFile.Name())
		So(err, ShouldBeNil)
		So(string(data), ShouldContainSubstring,
			"\"Extensions\":{\"Search\":null,\"Sync\":null,\"Metrics\":null,\"Scrub\":null,\"Lint\":null,\"UI\":null,\"Mgmt\":null") //nolint:lll // gofumpt conflicts with lll
	})
}

func testWithMetricsEnabled(t *testing.T, rootDir string, cfgContentFormat string) {
	t.Helper()
	port := GetFreePort()
	baseURL := GetBaseURL(port)
	logFile, err := os.CreateTemp("", "zot-log*.txt")
	So(err, ShouldBeNil)

	defer os.Remove(logFile.Name()) // clean up

	content := fmt.Sprintf(cfgContentFormat, rootDir, port, logFile.Name())
	cfgfile, err := os.CreateTemp("", "zot-test*.json")
	So(err, ShouldBeNil)

	defer os.Remove(cfgfile.Name()) // clean up

	_, err = cfgfile.WriteString(content)
	So(err, ShouldBeNil)
	err = cfgfile.Close()
	So(err, ShouldBeNil)

	os.Args = []string{"cli_test", "serve", cfgfile.Name()}

	go func() {
		Convey("run", t, func() {
			err = cli.NewServerRootCmd().Execute()
			So(err, ShouldBeNil)
		})
	}()
	WaitTillServerReady(baseURL)

	resp, err := resty.R().Get(baseURL + "/metrics")
	So(err, ShouldBeNil)
	So(resp, ShouldNotBeNil)
	So(resp.StatusCode(), ShouldEqual, http.StatusOK)

	respStr := string(resp.Body())
	So(respStr, ShouldContainSubstring, "zot_info")

	data, err := os.ReadFile(logFile.Name())
	So(err, ShouldBeNil)
	So(string(data), ShouldContainSubstring,
		"\"Metrics\":{\"Enable\":true,\"Prometheus\":{\"Path\":\"/metrics\"}}")
}

func TestServeMetricsExtension(t *testing.T) {
	oldArgs := os.Args

	defer func() { os.Args = oldArgs }()

	Convey("no explicit enable", t, func(c C) {
		tmpFile := t.TempDir()

		content := `{
			"storage": {
				"rootDirectory": "%s"
			},
			"http": {
				"address": "127.0.0.1",
				"port": "%s"
			},
			"log": {
				"level": "debug",
				"output": "%s"
			},
			"extensions": {
				"metrics": {
				}
			}
		}`
		testWithMetricsEnabled(t, tmpFile, content)
	})

	Convey("no explicit enable but with prometheus parameter", t, func(c C) {
		tmpFile := t.TempDir()

		content := `{
			"storage": {
				"rootDirectory": "%s"
			},
			"http": {
				"address": "127.0.0.1",
				"port": "%s"
			},
			"log": {
				"level": "debug",
				"output": "%s"
			},
			"extensions": {
				"metrics": {
					"prometheus": {
						"path": "/metrics"
					}
				}
			}
		}`
		testWithMetricsEnabled(t, tmpFile, content)
	})

	Convey("with explicit enable, but without prometheus parameter", t, func(c C) {
		tmpFile := t.TempDir()

		content := `{
			"storage": {
				"rootDirectory": "%s"
			},
			"http": {
				"address": "127.0.0.1",
				"port": "%s"
			},
			"log": {
				"level": "debug",
				"output": "%s"
			},
			"extensions": {
				"metrics": {
					"enable": true
				}
			}
		}`
		testWithMetricsEnabled(t, tmpFile, content)
	})

	Convey("with explicit disable", t, func(c C) {
		port := GetFreePort()
		baseURL := GetBaseURL(port)
		logFile, err := os.CreateTemp("", "zot-log*.txt")
		So(err, ShouldBeNil)
		tmpFile := t.TempDir()

		defer os.Remove(logFile.Name()) // clean up

		content := fmt.Sprintf(`{
					"storage": {
						"rootDirectory": "%s"
					},
					"http": {
						"address": "127.0.0.1",
						"port": "%s"
					},
					"log": {
						"level": "debug",
						"output": "%s"
					},
					"extensions": {
						"metrics": {
							"enable": false
						}
					}
				}`, tmpFile, port, logFile.Name())

		cfgfile, err := os.CreateTemp("", "zot-test*.json")
		So(err, ShouldBeNil)
		defer os.Remove(cfgfile.Name()) // clean up

		_, err = cfgfile.WriteString(content)
		So(err, ShouldBeNil)
		err = cfgfile.Close()
		So(err, ShouldBeNil)

		os.Args = []string{"cli_test", "serve", cfgfile.Name()}

		go func() {
			Convey("run", t, func() {
				err = cli.NewServerRootCmd().Execute()
				So(err, ShouldBeNil)
			})
		}()

		WaitTillServerReady(baseURL)

		resp, err := resty.R().Get(baseURL + "/metrics")
		So(err, ShouldBeNil)
		So(resp, ShouldNotBeNil)
		So(resp.StatusCode(), ShouldEqual, http.StatusNotFound)

		data, err := os.ReadFile(logFile.Name())
		So(err, ShouldBeNil)
		So(string(data), ShouldContainSubstring,
			"\"Metrics\":{\"Enable\":false,\"Prometheus\":{\"Path\":\"/metrics\"}}") //nolint:lll // gofumpt conflicts with lll
	})
}

func TestServeSyncExtension(t *testing.T) {
	oldArgs := os.Args

	defer func() { os.Args = oldArgs }()

	Convey("sync implicitly enabled", t, func(c C) {
		content := `{
				"storage": {
					"rootDirectory": "%s"
				},
				"http": {
					"address": "127.0.0.1",
					"port": "%s"
				},
				"log": {
					"level": "debug",
					"output": "%s"
				},
				"extensions": {
					"sync": {
						"registries": [{
							"urls": ["http://localhost:8080"],
							"tlsVerify": false,
							"onDemand": true,
							"maxRetries": 3,
							"retryDelay": "15m",
							"certDir": "",
							"content":[
								{
									"prefix": "zot-test",
									"tags": {
										"regex": ".*",
										"semver": true
									}
								}
							]
						}]
					}
				}
			}`

		logPath, err := runCLIWithConfig(t.TempDir(), content)
		So(err, ShouldBeNil)
		data, err := os.ReadFile(logPath)
		So(err, ShouldBeNil)

		defer os.Remove(logPath) // clean up

		So(string(data), ShouldContainSubstring,
			"\"Extensions\":{\"Search\":null,\"Sync\":{\"Enable\":true")
	})

	Convey("sync explicitly enabled", t, func(c C) {
		content := `{
				"storage": {
					"rootDirectory": "%s"
				},
				"http": {
					"address": "127.0.0.1",
					"port": "%s"
				},
				"log": {
					"level": "debug",
					"output": "%s"
				},
				"extensions": {
					"sync": {
						"enable": true,
						"registries": [{
							"urls": ["http://localhost:8080"],
							"tlsVerify": false,
							"onDemand": true,
							"maxRetries": 3,
							"retryDelay": "15m",
							"certDir": "",
							"content":[
								{
									"prefix": "zot-test",
									"tags": {
										"regex": ".*",
										"semver": true
									}
								}
							]
						}]
					}
				}
			}`

		logPath, err := runCLIWithConfig(t.TempDir(), content)
		So(err, ShouldBeNil)
		data, err := os.ReadFile(logPath)
		So(err, ShouldBeNil)

		defer os.Remove(logPath) // clean up

		So(string(data), ShouldContainSubstring,
			"\"Extensions\":{\"Search\":null,\"Sync\":{\"Enable\":true")
	})

	Convey("sync explicitly disabled", t, func(c C) {
		content := `{
				"storage": {
					"rootDirectory": "%s"
				},
				"http": {
					"address": "127.0.0.1",
					"port": "%s"
				},
				"log": {
					"level": "debug",
					"output": "%s"
				},
				"extensions": {
					"sync": {
						"enable": false,
						"registries": [{
							"urls": ["http://127.0.0.1:8080"],
							"tlsVerify": false,
							"certDir": "",
							"maxRetries": 3,
							"retryDelay": "15m"
						}]
					}
				}
			}`

		logPath, err := runCLIWithConfig(t.TempDir(), content)
		So(err, ShouldBeNil)
		data, err := os.ReadFile(logPath)
		So(err, ShouldBeNil)

		defer os.Remove(logPath) // clean up

		So(string(data), ShouldContainSubstring,
			"\"Extensions\":{\"Search\":null,\"Sync\":{\"Enable\":false")
	})
}

func TestServeScrubExtension(t *testing.T) {
	oldArgs := os.Args

	defer func() { os.Args = oldArgs }()

	Convey("scrub implicitly enabled", t, func(c C) {
		content := `{
					"storage": {
						"rootDirectory": "%s"
					},
					"http": {
						"address": "127.0.0.1",
						"port": "%s"
					},
					"log": {
						"level": "debug",
						"output": "%s"
					},
					"extensions": {
						"scrub": {
						}
					}
				}`

		logPath, err := runCLIWithConfig(t.TempDir(), content)
		So(err, ShouldBeNil)
		data, err := os.ReadFile(logPath)
		So(err, ShouldBeNil)

		defer os.Remove(logPath) // clean up

		dataStr := string(data)
		So(dataStr, ShouldContainSubstring,
			"\"Extensions\":{\"Search\":null,\"Sync\":null,\"Metrics\":null,\"Scrub\":{\"Enable\":true,\"Interval\":86400000000000},\"Lint\":null") //nolint:lll // gofumpt conflicts with lll
		So(dataStr, ShouldNotContainSubstring,
			"scrub interval set to too-short interval < 2h, changing scrub duration to 2 hours and continuing.")
	})

	Convey("scrub implicitly enabled, but with scrub interval param set", t, func(c C) {
		content := `{
					"storage": {
						"rootDirectory": "%s"
					},
					"http": {
						"address": "127.0.0.1",
						"port": "%s"
					},
					"log": {
						"level": "debug",
						"output": "%s"
					},
					"extensions": {
						"scrub": {
							"interval": "1h"
						}
					}
				}`

		logPath, err := runCLIWithConfig(t.TempDir(), content)
		So(err, ShouldBeNil)
		data, err := os.ReadFile(logPath)
		So(err, ShouldBeNil)

		defer os.Remove(logPath) // clean up

		// Even if in config we specified scrub interval=1h, the minimum interval is 2h
		dataStr := string(data)
		So(dataStr, ShouldContainSubstring, "\"Scrub\":{\"Enable\":true,\"Interval\":3600000000000}")
		So(dataStr, ShouldContainSubstring,
			"scrub interval set to too-short interval < 2h, changing scrub duration to 2 hours and continuing.")
	})

	Convey("scrub explicitly enabled, but without scrub interval param set", t, func(c C) {
		content := `{
					"storage": {
						"rootDirectory": "%s"
					},
					"http": {
						"address": "127.0.0.1",
						"port": "%s"
					},
					"log": {
						"level": "debug",
						"output": "%s"
					},
					"extensions": {
						"scrub": {
							"enable": true
						}
					}
				}`

		logPath, err := runCLIWithConfig(t.TempDir(), content)
		So(err, ShouldBeNil)
		data, err := os.ReadFile(logPath)
		So(err, ShouldBeNil)

		defer os.Remove(logPath) // clean up

		dataStr := string(data)
		So(dataStr, ShouldContainSubstring,
			"\"Extensions\":{\"Search\":null,\"Sync\":null,\"Metrics\":null,\"Scrub\":{\"Enable\":true,\"Interval\":86400000000000},\"Lint\":null") //nolint:lll // gofumpt conflicts with lll
		So(dataStr, ShouldNotContainSubstring,
			"scrub interval set to too-short interval < 2h, changing scrub duration to 2 hours and continuing.")
	})

	Convey("scrub explicitly disabled", t, func(c C) {
		content := `{
				"storage": {
					"rootDirectory": "%s"
				},
				"http": {
					"address": "127.0.0.1",
					"port": "%s"
				},
				"log": {
					"level": "debug",
					"output": "%s"
				},
				"extensions": {
					"scrub": {
						"enable": false
					}
				}
			}`

		logPath, err := runCLIWithConfig(t.TempDir(), content)
		So(err, ShouldBeNil)
		data, err := os.ReadFile(logPath)
		So(err, ShouldBeNil)

		defer os.Remove(logPath) // clean up

		dataStr := string(data)
		So(dataStr, ShouldContainSubstring, "\"Scrub\":{\"Enable\":false,\"Interval\":86400000000000}")
		So(dataStr, ShouldContainSubstring, "scrub config not provided, skipping scrub")
		So(dataStr, ShouldNotContainSubstring,
			"scrub interval set to too-short interval < 2h, changing scrub duration to 2 hours and continuing.")
	})
}

func TestServeLintExtension(t *testing.T) {
	oldArgs := os.Args

	defer func() { os.Args = oldArgs }()

	Convey("lint enabled", t, func(c C) {
		content := `{
					"storage": {
						"rootDirectory": "%s"
					},
					"http": {
						"address": "127.0.0.1",
						"port": "%s"
					},
					"log": {
						"level": "debug",
						"output": "%s"
					},
					"extensions": {
						"lint": {
							"enable": "true",
							"mandatoryAnnotations": ["annot1"]
						}
					}
				}`

		logPath, err := runCLIWithConfig(t.TempDir(), content)
		So(err, ShouldBeNil)
		data, err := os.ReadFile(logPath)
		So(err, ShouldBeNil)

		defer os.Remove(logPath) // clean up

		So(string(data), ShouldContainSubstring,
			"\"Extensions\":{\"Search\":null,\"Sync\":null,\"Metrics\":null,\"Scrub\":null,\"Lint\":{\"Enable\":true,\"MandatoryAnnotations\":") //nolint:lll // gofumpt conflicts with lll
	})

	Convey("lint enabled", t, func(c C) {
		content := `{
					"storage": {
						"rootDirectory": "%s"
					},
					"http": {
						"address": "127.0.0.1",
						"port": "%s"
					},
					"log": {
						"level": "debug",
						"output": "%s"
					},
					"extensions": {
						"lint": {
							"enable": "false"
						}
					}
				}`

		logPath, err := runCLIWithConfig(t.TempDir(), content)
		So(err, ShouldBeNil)
		data, err := os.ReadFile(logPath)
		So(err, ShouldBeNil)

		defer os.Remove(logPath) // clean up

		So(string(data), ShouldContainSubstring,
			"\"Extensions\":{\"Search\":null,\"Sync\":null,\"Metrics\":null,\"Scrub\":null,\"Lint\":{\"Enable\":false,\"MandatoryAnnotations\":null}") //nolint:lll // gofumpt conflicts with lll
	})
}

func TestServeSearchEnabled(t *testing.T) {
	oldArgs := os.Args

	defer func() { os.Args = oldArgs }()

	Convey("search implicitly enabled", t, func(c C) {
		content := `{
					"storage": {
						"rootDirectory": "%s"
					},
					"http": {
						"address": "127.0.0.1",
						"port": "%s"
					},
					"log": {
						"level": "debug",
						"output": "%s"
					},
					"extensions": {
						"search": {
						}
					}
				}`

		tempDir := t.TempDir()
		logPath, err := runCLIWithConfig(tempDir, content)
		So(err, ShouldBeNil)
		// to avoid data race when multiple go routines write to trivy DB instance.
		defer os.Remove(logPath) // clean up

		substring := `"Extensions":{"Search":{"Enable":true,"CVE":null}`

		found, err := ReadLogFileAndSearchString(logPath, substring, readLogFileTimeout)

		if !found {
			data, err := os.ReadFile(logPath)
			So(err, ShouldBeNil)
			t.Log(string(data))
		}

		So(found, ShouldBeTrue)
		So(err, ShouldBeNil)
	})
}

func TestServeSearchEnabledDefaultCVEDB(t *testing.T) {
	oldArgs := os.Args

	defer func() { os.Args = oldArgs }()

	Convey("search implicitly enabled with CVE param set", t, func(c C) {
		content := `{
					"storage": {
						"rootDirectory": "%s"
					},
					"http": {
						"address": "127.0.0.1",
						"port": "%s"
					},
					"log": {
						"level": "debug",
						"output": "%s"
					},
					"extensions": {
						"search": {
							"cve": {
								"updateInterval": "1h"
							}
						}
					}
				}`

		tempDir := t.TempDir()
		logPath, err := runCLIWithConfig(tempDir, content)
		So(err, ShouldBeNil)

		defer os.Remove(logPath) // clean up

		// to avoid data race when multiple go routines write to trivy DB instance.
		WaitTillTrivyDBDownloadStarted(tempDir)

		// The default config handling logic will convert the 1h interval to a 2h interval
		substring := "\"Search\":{\"Enable\":true,\"CVE\":{\"UpdateInterval\":7200000000000,\"Trivy\":" +
			"{\"DBRepository\":\"ghcr.io/aquasecurity/trivy-db\",\"JavaDBRepository\":\"ghcr.io/aquasecurity/trivy-java-db\"}}}"

		found, err := ReadLogFileAndSearchString(logPath, substring, readLogFileTimeout)

		defer func() {
			if !found {
				data, err := os.ReadFile(logPath)
				So(err, ShouldBeNil)
				t.Log(string(data))
			}
		}()

		So(found, ShouldBeTrue)
		So(err, ShouldBeNil)

		found, err = ReadLogFileAndSearchString(logPath, "updating cve-db", readLogFileTimeout)
		So(found, ShouldBeTrue)
		So(err, ShouldBeNil)
	})
}

func TestServeSearchEnabledNoCVE(t *testing.T) {
	oldArgs := os.Args

	defer func() { os.Args = oldArgs }()

	Convey("search explicitly enabled, but CVE parameter not set", t, func(c C) {
		content := `{
				"storage": {
					"rootDirectory": "%s"
				},
				"http": {
					"address": "127.0.0.1",
					"port": "%s"
				},
				"log": {
					"level": "debug",
					"output": "%s"
				},
				"extensions": {
					"search": {
						"enable": true
					}
				}
			}`

		tempDir := t.TempDir()
		logPath, err := runCLIWithConfig(tempDir, content)
		So(err, ShouldBeNil)

		defer os.Remove(logPath) // clean up

		substring := `"Extensions":{"Search":{"Enable":true,"CVE":null}` //nolint:lll // gofumpt conflicts with lll
		found, err := ReadLogFileAndSearchString(logPath, substring, readLogFileTimeout)

		if !found {
			data, err := os.ReadFile(logPath)
			So(err, ShouldBeNil)
			t.Log(string(data))
		}

		So(found, ShouldBeTrue)
		So(err, ShouldBeNil)
	})
}

func TestServeSearchDisabled(t *testing.T) {
	oldArgs := os.Args

	defer func() { os.Args = oldArgs }()

	Convey("search explicitly disabled", t, func(c C) {
		content := `{
				"storage": {
					"rootDirectory": "%s"
				},
				"http": {
					"address": "127.0.0.1",
					"port": "%s"
				},
				"log": {
					"level": "debug",
					"output": "%s"
				},
				"extensions": {
					"search": {
						"enable": false,
						"cve": {
							"updateInterval": "3h"
						}
					}
				}
			}`

		logPath, err := runCLIWithConfig(t.TempDir(), content)
		So(err, ShouldBeNil)
		data, err := os.ReadFile(logPath)
		So(err, ShouldBeNil)

		defer os.Remove(logPath) // clean up

		dataStr := string(data)
		So(dataStr, ShouldContainSubstring,
			`"Search":{"Enable":false,"CVE":{"UpdateInterval":10800000000000,"Trivy":null}`)
		So(dataStr, ShouldContainSubstring, "cve config not provided, skipping cve-db update")
		So(dataStr, ShouldNotContainSubstring,
			"CVE update interval set to too-short interval < 2h, changing update duration to 2 hours and continuing.")
	})
}

func TestServeMgmtExtension(t *testing.T) {
	oldArgs := os.Args

	defer func() { os.Args = oldArgs }()

	Convey("Mgmt implicitly enabled", t, func(c C) {
		content := `{
					"storage": {
						"rootDirectory": "%s"
					},
					"http": {
						"address": "127.0.0.1",
						"port": "%s"
					},
					"log": {
						"level": "debug",
						"output": "%s"
					},
					"extensions": {
						"search": {
							"enable": true
						}
					}
				}`

		logPath, err := runCLIWithConfig(t.TempDir(), content)
		So(err, ShouldBeNil)
		defer os.Remove(logPath) // clean up

		found, err := ReadLogFileAndSearchString(logPath, "setting up mgmt routes", 10*time.Second)

		if !found {
			data, err := os.ReadFile(logPath)
			So(err, ShouldBeNil)
			t.Log(string(data))
		}

		So(err, ShouldBeNil)
		So(found, ShouldBeTrue)
	})

	Convey("Mgmt disabled - Search unconfigured", t, func(c C) {
		content := `{
					"storage": {
						"rootDirectory": "%s"
					},
					"http": {
						"address": "127.0.0.1",
						"port": "%s"
					},
					"log": {
						"level": "debug",
						"output": "%s"
					},
					"extensions": {
					}
				}`

		logPath, err := runCLIWithConfig(t.TempDir(), content)
		So(err, ShouldBeNil)
		defer os.Remove(logPath) // clean up

		found, err := ReadLogFileAndSearchString(logPath,
			"skip enabling the mgmt route as the config prerequisites are not met", 10*time.Second)

		if !found {
			data, err := os.ReadFile(logPath)
			So(err, ShouldBeNil)
			t.Log(string(data))
		}

		So(err, ShouldBeNil)
		So(found, ShouldBeTrue)
	})

	Convey("Mgmt disabled - extensions missing", t, func(c C) {
		content := `{
					"storage": {
						"rootDirectory": "%s"
					},
					"http": {
						"address": "127.0.0.1",
						"port": "%s"
					},
					"log": {
						"level": "debug",
						"output": "%s"
					}
				}`

		logPath, err := runCLIWithConfig(t.TempDir(), content)
		So(err, ShouldBeNil)
		defer os.Remove(logPath) // clean up

		found, err := ReadLogFileAndSearchString(logPath,
			"skip enabling the mgmt route as the config prerequisites are not met", 10*time.Second)

		if !found {
			data, err := os.ReadFile(logPath)
			So(err, ShouldBeNil)
			t.Log(string(data))
		}

		So(err, ShouldBeNil)
		So(found, ShouldBeTrue)
	})
}

func TestServeImageTrustExtension(t *testing.T) {
	oldArgs := os.Args

	defer func() { os.Args = oldArgs }()

	Convey("Trust explicitly disabled", t, func(c C) {
		content := `{
					"storage": {
						"rootDirectory": "%s"
					},
					"http": {
						"address": "127.0.0.1",
						"port": "%s"
					},
					"log": {
						"level": "debug",
						"output": "%s"
					},
					"extensions": {
						"trust": {
							"enable": false
						}
					}
				}`

		logPath, err := runCLIWithConfig(t.TempDir(), content)
		So(err, ShouldBeNil)
		defer os.Remove(logPath) // clean up

		found, err := ReadLogFileAndSearchString(logPath,
			"skip enabling the image trust routes as the config prerequisites are not met", 10*time.Second)

		if !found {
			data, err := os.ReadFile(logPath)
			So(err, ShouldBeNil)
			t.Log(string(data))
		}

		So(err, ShouldBeNil)
		So(found, ShouldBeTrue)
	})

	Convey("Trust explicitly enabled - but cosign and notation disabled", t, func(c C) {
		content := `{
					"storage": {
						"rootDirectory": "%s"
					},
					"http": {
						"address": "127.0.0.1",
						"port": "%s"
					},
					"log": {
						"level": "debug",
						"output": "%s"
					},
					"extensions": {
						"trust": {
							"enable": true
						}
					}
				}`

		logPath, err := runCLIWithConfig(t.TempDir(), content)
		So(err, ShouldBeNil)
		defer os.Remove(logPath) // clean up

		found, err := ReadLogFileAndSearchString(logPath,
			"skip enabling the image trust routes as the config prerequisites are not met", 10*time.Second)

		if !found {
			data, err := os.ReadFile(logPath)
			So(err, ShouldBeNil)
			t.Log(string(data))
		}

		So(err, ShouldBeNil)
		So(found, ShouldBeTrue)
	})

	Convey("Trust explicitly enabled -  cosign and notation enabled", t, func(c C) {
		content := `{
					"storage": {
						"rootDirectory": "%s"
					},
					"http": {
						"address": "127.0.0.1",
						"port": "%s"
					},
					"log": {
						"level": "debug",
						"output": "%s"
					},
					"extensions": {
						"trust": {
							"enable": true,
							"cosign": true,
							"notation": true
						}
					}
				}`

		logPath, err := runCLIWithConfig(t.TempDir(), content)
		So(err, ShouldBeNil)
		defer os.Remove(logPath) // clean up

		found, err := ReadLogFileAndSearchString(logPath,
			"setting up image trust routes", 10*time.Second)

		defer func() {
			if !found {
				data, err := os.ReadFile(logPath)
				So(err, ShouldBeNil)
				t.Log(string(data))
			}
		}()

		So(err, ShouldBeNil)
		So(found, ShouldBeTrue)

		found, err = ReadLogFileAndSearchString(logPath,
			"setting up notation route", 10*time.Second)
		So(err, ShouldBeNil)
		So(found, ShouldBeTrue)

		found, err = ReadLogFileAndSearchString(logPath,
			"setting up cosign route", 10*time.Second)
		So(err, ShouldBeNil)
		So(found, ShouldBeTrue)
	})
}

func TestOverlappingSyncRetentionConfig(t *testing.T) {
	oldArgs := os.Args

	defer func() { os.Args = oldArgs }()

	Convey("Test verify without overlapping sync and retention", t, func(c C) {
		tmpfile, err := os.CreateTemp("", "zot-test*.json")
		So(err, ShouldBeNil)
		defer os.Remove(tmpfile.Name()) // clean up

		content := `{
			"distSpecVersion": "1.1.1",
			"storage": {
				"rootDirectory": "%s",
				"gc": true,
				"gcDelay": "2h",
				"gcInterval": "1h",
				"retention": {
					"policies": [
						{
							"repositories": ["infra/*", "prod/*"],
							"deleteReferrers": false,
							"keepTags": [{
								"patterns": ["v4.*", ".*-prod"]
							},
							{
								"patterns": ["v3.*", ".*-prod"],
								"pulledWithin": "168h"
							}]
						}
					]
				}
			},
			"http": {
				"address": "127.0.0.1",
				"port": "%s"
			},
			"log": {
				"level": "debug",
				"output": "%s"
			},
			"extensions": {
				"sync": {
					"enable": true,
					"registries": [
						{
							"urls": [
								"https://registry1:5000"
							],
							"content": [
								{
									"prefix": "infra/*",
									"tags": {
										"regex": "v4.*",
										"semver": true
									}
								}
							]
						}
					]
				}
			}
		}`

		logPath, err := runCLIWithConfig(t.TempDir(), content)
		So(err, ShouldBeNil)
		data, err := os.ReadFile(logPath)
		So(err, ShouldBeNil)

		defer os.Remove(logPath) // clean up

		So(string(data), ShouldNotContainSubstring, "overlapping sync content")
	})

	Convey("Test verify with overlapping sync and retention - retention would remove v4 tags", t, func(c C) {
		tmpfile, err := os.CreateTemp("", "zot-test*.json")
		So(err, ShouldBeNil)
		defer os.Remove(tmpfile.Name()) // clean up

		content := `{
			"distSpecVersion": "1.1.1",
			"storage": {
				"rootDirectory": "%s",
				"gc": true,
				"gcDelay": "2h",
				"gcInterval": "1h",
				"retention": {
					"policies": [
						{
							"repositories": ["infra/*", "prod/*"],
							"keepTags": [{
								"patterns": ["v2.*", ".*-prod"]
							},
							{
								"patterns": ["v3.*", ".*-prod"]
							}]
						}
					]
				}
			},
			"http": {
				"address": "127.0.0.1",
				"port": "%s"
			},
			"log": {
				"level": "debug",
				"output": "%s"
			},
			"extensions": {
				"sync": {
					"enable": true,
					"registries": [
						{
							"urls": [
								"https://registry1:5000"
							],
							"content": [
								{
									"prefix": "infra/*",
									"tags": {
										"regex": "4.*",
										"semver": true
									}
								}
							]
						}
					]
				}
			}
		}`

		logPath, err := runCLIWithConfig(t.TempDir(), content)
		So(err, ShouldBeNil)
		data, err := os.ReadFile(logPath)
		So(err, ShouldBeNil)

		defer os.Remove(logPath) // clean up

		So(string(data), ShouldContainSubstring, "overlapping sync content\":{\"Prefix\":\"infra/*")
	})

	Convey("Test verify with overlapping sync and retention - retention would remove tags from repo", t, func(c C) {
		tmpfile, err := os.CreateTemp("", "zot-test*.json")
		So(err, ShouldBeNil)
		defer os.Remove(tmpfile.Name()) // clean up

		content := `{
			"distSpecVersion": "1.1.1",
			"storage": {
				"rootDirectory": "%s",
				"gc": true,
				"gcDelay": "2h",
				"gcInterval": "1h",
				"retention": {
					"dryRun": false,
					"delay": "24h",
					"policies": [
						{
							"repositories": ["tmp/**"],
							"keepTags": [{
								"patterns": ["v1.*"]
							}]
						}
					]
				}
			},
			"http": {
				"address": "127.0.0.1",
				"port": "%s"
			},
			"log": {
				"level": "debug",
				"output": "%s"
			},
			"extensions": {
				"sync": {
					"enable": true,
					"registries": [
						{
							"urls": [
								"https://registry1:5000"
							],
							"content": [
								{
									"prefix": "**",
									"destination": "/tmp",
									"stripPrefix": true
								}
							]
						}
					]
				}
			}
		}
		`

		logPath, err := runCLIWithConfig(t.TempDir(), content)
		So(err, ShouldBeNil)
		data, err := os.ReadFile(logPath)
		So(err, ShouldBeNil)

		defer os.Remove(logPath) // clean up

		So(string(data), ShouldContainSubstring, "overlapping sync content\":{\"Prefix\":\"**")
	})

	Convey("Test verify with overlapping sync and retention - retention would remove tags from subpath", t, func(c C) {
		tmpfile, err := os.CreateTemp("", "zot-test*.json")
		So(err, ShouldBeNil)
		defer os.Remove(tmpfile.Name()) // clean up

		content := `{
			"distSpecVersion": "1.1.1",
			"storage": {
				"rootDirectory": "%s",
				"gc": true,
				"gcDelay": "2h",
				"gcInterval": "1h",
				"subPaths": {
					"/synced": {
						"rootDirectory": "/tmp/zot2",
						"dedupe": true,
						"retention": {
							"policies": [
								{
									"repositories": ["infra/*", "prod/*"],
									"deleteReferrers": false,
									"keepTags": [{
									}]
								}
							]
						}
					}
				}
			},
			"http": {
				"address": "127.0.0.1",
				"port": "%s"
			},
			"log": {
				"level": "debug",
				"output": "%s"
			},
			"extensions": {
				"sync": {
					"enable": true,
					"registries": [
						{
							"urls": [
								"https://registry1:5000"
							],
							"content": [
								{
									"prefix": "prod/*",
									"destination": "/synced"
								}
							]
						}
					]
				}
			}
		}
		`

		logPath, err := runCLIWithConfig(t.TempDir(), content)
		So(err, ShouldBeNil)
		data, err := os.ReadFile(logPath)
		So(err, ShouldBeNil)

		defer os.Remove(logPath) // clean up

		So(string(data), ShouldContainSubstring, "overlapping sync content\":{\"Prefix\":\"prod/*")
	})
}

func TestSyncWithRemoteStorageConfig(t *testing.T) {
	oldArgs := os.Args

	defer func() { os.Args = oldArgs }()

	Convey("Test verify sync with remote storage works if sync.tmpdir is provided", t, func(c C) {
		tmpfile, err := os.CreateTemp("", "zot-test*.json")
		So(err, ShouldBeNil)
		defer os.Remove(tmpfile.Name()) // clean up

		content := `{
			"distSpecVersion": "1.1.1",
			"storage": {
				"rootDirectory": "%s",
				"dedupe": false,
				"remoteCache": false,
				"storageDriver": {
					"name": "s3",
					"rootdirectory": "/zot",
					"region": "us-east-2",
					"regionendpoint": "localhost:4566",
					"bucket": "zot-storage",
					"secure": false,
					"skipverify": false
				}
			},
			"http": {
				"address": "0.0.0.0",
				"port": "%s"
			},
			"log": {
				"level": "debug",
				"output": "%s"
			},
			"extensions": {
				"sync": {
					"downloadDir": "/tmp/sync",
					"registries": [
						{
							"urls": [
								"http://localhost:9000"
							],
							"onDemand": true,
							"tlsVerify": false,
							"content": [
								{
									"prefix": "**"
								}
							]
						}
					]
				}
			}
		}`

		logPath, err := runCLIWithConfig(t.TempDir(), content)
		So(err, ShouldBeNil)

		data, err := os.ReadFile(logPath)
		So(err, ShouldBeNil)

		defer os.Remove(logPath) // clean up

		So(string(data), ShouldNotContainSubstring,
			"using both sync and remote storage features needs config.Extensions.Sync.DownloadDir to be specified")
	})

	Convey("Test verify sync with remote storage panics if sync.tmpdir is not provided", t, func(c C) {
		port := GetFreePort()
		logFile, err := os.CreateTemp("", "zot-log*.txt")
		So(err, ShouldBeNil)

		defer os.Remove(logFile.Name()) // clean up

		tmpfile, err := os.CreateTemp("", "zot-test*.json")
		So(err, ShouldBeNil)

		defer os.Remove(tmpfile.Name()) // clean up

		content := fmt.Sprintf(`{
			"distSpecVersion": "1.1.1",
			"storage": {
				"rootDirectory": "%s",
				"dedupe": false,
				"remoteCache": false,
				"storageDriver": {
					"name": "s3",
					"rootdirectory": "/zot",
					"region": "us-east-2",
					"regionendpoint": "localhost:4566",
					"bucket": "zot-storage",
					"secure": false,
					"skipverify": false
				}
			},
			"http": {
				"address": "0.0.0.0",
				"port": "%s"
			},
			"log": {
				"level": "debug",
				"output": "%s"
			},
			"extensions": {
				"sync": {
					"registries": [
						{
							"urls": [
								"http://localhost:9000"
							],
							"onDemand": true,
							"tlsVerify": false,
							"content": [
								{
									"prefix": "**"
								}
							]
						}
					]
				}
			}
		}`, t.TempDir(), port, logFile.Name())

		err = os.WriteFile(tmpfile.Name(), []byte(content), 0o0600)
		So(err, ShouldBeNil)

		os.Args = []string{"cli_test", "serve", tmpfile.Name()}
		err = cli.NewServerRootCmd().Execute()
		So(err, ShouldNotBeNil)

		data, err := os.ReadFile(logFile.Name())
		So(err, ShouldBeNil)

		defer os.Remove(logFile.Name()) // clean up

		So(string(data), ShouldContainSubstring,
			"using both sync and remote storage features needs config.Extensions.Sync.DownloadDir to be specified")
	})

	Convey("Test verify sync with remote storage on subpath panics if sync.tmpdir is not provided", t, func(c C) {
		port := GetFreePort()
		logFile, err := os.CreateTemp("", "zot-log*.txt")
		So(err, ShouldBeNil)

		defer os.Remove(logFile.Name()) // clean up

		tmpfile, err := os.CreateTemp("", "zot-test*.json")
		So(err, ShouldBeNil)

		defer os.Remove(tmpfile.Name()) // clean up

		content := fmt.Sprintf(`{
			"distSpecVersion": "1.1.1",
			"storage": {
				"rootDirectory": "%s",
				"subPaths":{
					"/a": {
						"rootDirectory": "%s",
						"dedupe": false,
						"remoteCache": false,
						"storageDriver":{
							"name":"s3",
							"rootdirectory":"/zot-a",
							"region":"us-east-2",
							"bucket":"zot-storage",
							"secure":true,
							"skipverify":true
						}
					}
				}
			},
			"http": {
				"address": "0.0.0.0",
				"port": "%s"
			},
			"log": {
				"level": "debug",
				"output": "%s"
			},
			"extensions": {
				"sync": {
					"registries": [
						{
							"urls": [
								"http://localhost:9000"
							],
							"onDemand": true,
							"tlsVerify": false,
							"content": [
								{
									"prefix": "**"
								}
							]
						}
					]
				}
			}
		}`, t.TempDir(), t.TempDir(), port, logFile.Name())

		err = os.WriteFile(tmpfile.Name(), []byte(content), 0o0600)
		So(err, ShouldBeNil)

		os.Args = []string{"cli_test", "serve", tmpfile.Name()}
		err = cli.NewServerRootCmd().Execute()
		So(err, ShouldNotBeNil)

		data, err := os.ReadFile(logFile.Name())
		So(err, ShouldBeNil)
		defer os.Remove(logFile.Name()) // clean up

		So(string(data), ShouldContainSubstring,
			"using both sync and remote storage features needs config.Extensions.Sync.DownloadDir to be specified")
	})
}

func TestEventsExtension(t *testing.T) {
	oldArgs := os.Args

	defer func() { os.Args = oldArgs }()

	Convey("Events explicitly disabled", t, func(c C) {
		content := `{
				"storage": {
					"rootDirectory": "%s"
				},
				"http": {
					"address": "127.0.0.1",
					"port": "%s"
				},
				"log": {
					"level": "debug",
					"output": "%s"
				},
				"extensions": {
					"events": {
						"enable": false
					}
				}
			}`

		logPath, err := runCLIWithConfig(t.TempDir(), content)
		So(err, ShouldBeNil)
		defer os.Remove(logPath) // clean up

		found, err := ReadLogFileAndSearchString(logPath,
			"events disabled in configuration", 10*time.Second)

		if !found {
			data, err := os.ReadFile(logPath)
			So(err, ShouldBeNil)
			t.Log(string(data))
		}

		So(err, ShouldBeNil)
		So(found, ShouldBeTrue)
	})

	Convey("Unsupported event sink", t, func(c C) {
		content := `{
					"storage": {
						"rootDirectory": "%s"
					},
					"http": {
						"address": "127.0.0.1",
						"port": "%s"
					},
					"log": {
						"level": "debug",
						"output": "%s"
					},
					"extensions": {
						"events": {
							"enable": true,
							"sinks": [{
								"type": "unsupported"
							}]
						}
					}
				}`

		logPath, err := runCLIWithConfig(t.TempDir(), content)
		defer func(p string) {
			if p != "" {
				os.Remove(p)
			}
		}(logPath) // clean up
		So(err, ShouldNotBeNil)
		So(err.Error(), ShouldContainSubstring, zerr.ErrUnsupportedEventSink.Error())
	})
}
