package integration

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	. "github.com/containers/podman/v4/test/utils"
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

var _ = Describe("Podman login and logout", func() {
	var (
		err                      error
		authPath                 string
		certPath                 string
		certDirPath              string
		server                   string
		testImg                  string
		registriesConfWithSearch []byte
	)

	BeforeEach(func() {
		authPath = filepath.Join(podmanTest.TempDir, "auth")
		err := os.Mkdir(authPath, os.ModePerm)
		Expect(err).ToNot(HaveOccurred())

		htpasswd := SystemExec("htpasswd", []string{"-Bbn", "podmantest", "test"})
		htpasswd.WaitWithDefaultTimeout()
		Expect(htpasswd).Should(ExitCleanly())

		f, err := os.Create(filepath.Join(authPath, "htpasswd"))
		Expect(err).ToNot(HaveOccurred())
		defer f.Close()

		_, err = f.WriteString(htpasswd.OutputToString())
		Expect(err).ToNot(HaveOccurred())
		err = f.Sync()
		Expect(err).ToNot(HaveOccurred())
		port := GetPort()
		server = strings.Join([]string{"localhost", strconv.Itoa(port)}, ":")

		registriesConfWithSearch = []byte(fmt.Sprintf("[registries.search]\nregistries = ['%s']", server))

		testImg = strings.Join([]string{server, "test-alpine"}, "/")

		certDirPath = filepath.Join(os.Getenv("HOME"), ".config/containers/certs.d", server)
		err = os.MkdirAll(certDirPath, os.ModePerm)
		Expect(err).ToNot(HaveOccurred())
		cwd, _ := os.Getwd()
		certPath = filepath.Join(cwd, "../", "certs")

		setup := SystemExec("cp", []string{filepath.Join(certPath, "domain.crt"), filepath.Join(certDirPath, "ca.crt")})
		setup.WaitWithDefaultTimeout()

		session := podmanTest.Podman([]string{"run", "-d", "-p", strings.Join([]string{strconv.Itoa(port), strconv.Itoa(port)}, ":"),
			"-e", strings.Join([]string{"REGISTRY_HTTP_ADDR=0.0.0.0", strconv.Itoa(port)}, ":"), "--name", "registry", "-v",
			strings.Join([]string{authPath, "/auth:Z"}, ":"), "-e", "REGISTRY_AUTH=htpasswd", "-e",
			"REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm", "-e", "REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd",
			"-v", strings.Join([]string{certPath, "/certs:Z"}, ":"), "-e", "REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt",
			"-e", "REGISTRY_HTTP_TLS_KEY=/certs/domain.key", REGISTRY_IMAGE})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())

		if !WaitContainerReady(podmanTest, "registry", "listening on", 20, 1) {
			Skip("Cannot start docker registry.")
		}

		// collision protection: each test uses a unique authfile
		os.Setenv("REGISTRY_AUTH_FILE", filepath.Join(podmanTest.TempDir, "default-auth.json"))
	})

	AfterEach(func() {
		os.Unsetenv("REGISTRY_AUTH_FILE")
		os.RemoveAll(authPath)
		os.RemoveAll(certDirPath)
	})

	readAuthInfo := func(filePath string) map[string]interface{} {
		authBytes, err := os.ReadFile(filePath)
		Expect(err).ToNot(HaveOccurred())

		var authInfo map[string]interface{}
		err = json.Unmarshal(authBytes, &authInfo)
		Expect(err).ToNot(HaveOccurred())
		GinkgoWriter.Println(authInfo)

		const authsKey = "auths"
		Expect(authInfo).To(HaveKey(authsKey))

		auths, ok := authInfo[authsKey].(map[string]interface{})
		Expect(ok).To(BeTrue(), "authInfo[%s]", authsKey)

		return auths
	}

	It("podman login and logout", func() {
		authFile := os.Getenv("REGISTRY_AUTH_FILE")
		Expect(authFile).NotTo(BeEmpty(), "$REGISTRY_AUTH_FILE")

		session := podmanTest.Podman([]string{"login", "-u", "podmantest", "-p", "test", server})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())

		// Confirm that file was created, with the desired credentials
		auths := readAuthInfo(authFile)
		Expect(auths).To(HaveKey(server))
		// base64-encoded "podmantest:test"
		Expect(auths[server]).To(HaveKeyWithValue("auth", "cG9kbWFudGVzdDp0ZXN0"))

		session = podmanTest.Podman([]string{"push", "-q", ALPINE, testImg})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())

		session = podmanTest.Podman([]string{"logout", server})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())

		session = podmanTest.Podman([]string{"push", "-q", ALPINE, testImg})
		session.WaitWithDefaultTimeout()
		Expect(session).To(ExitWithError())
		Expect(session.ErrorToString()).To(ContainSubstring(": authentication required"))
	})

	It("podman login and logout without registry parameter", func() {
		registriesConf, err := os.CreateTemp("", "TestLoginWithoutParameter")
		Expect(err).ToNot(HaveOccurred())
		defer registriesConf.Close()
		defer os.Remove(registriesConf.Name())

		err = os.WriteFile(registriesConf.Name(), registriesConfWithSearch, os.ModePerm)
		Expect(err).ToNot(HaveOccurred())

		// Environment is per-process, so this looks very unsafe; actually it seems fine because tests are not
		// run in parallel unless they opt in by calling t.Parallel().  So don’t do that.
		oldRCP, hasRCP := os.LookupEnv("CONTAINERS_REGISTRIES_CONF")
		defer func() {
			if hasRCP {
				os.Setenv("CONTAINERS_REGISTRIES_CONF", oldRCP)
			} else {
				os.Unsetenv("CONTAINERS_REGISTRIES_CONF")
			}
		}()
		os.Setenv("CONTAINERS_REGISTRIES_CONF", registriesConf.Name())

		session := podmanTest.Podman([]string{"login", "-u", "podmantest", "-p", "test"})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())

		session = podmanTest.Podman([]string{"logout"})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())
	})

	It("podman login and logout with flag --authfile", func() {
		authFile := filepath.Join(podmanTest.TempDir, "auth.json")
		session := podmanTest.Podman([]string{"login", "--username", "podmantest", "--password", "test", "--authfile", authFile, server})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())

		readAuthInfo(authFile)

		// push should fail with nonexistent authfile
		session = podmanTest.Podman([]string{"push", "-q", "--authfile", "/tmp/nonexistent", ALPINE, testImg})
		session.WaitWithDefaultTimeout()
		Expect(session).To(ExitWithError())
		Expect(session.ErrorToString()).To(Equal("Error: credential file is not accessible: stat /tmp/nonexistent: no such file or directory"))

		session = podmanTest.Podman([]string{"push", "-q", "--authfile", authFile, ALPINE, testImg})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())

		session = podmanTest.Podman([]string{"run", "-q", "--authfile", authFile, testImg})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())

		// logout should fail with nonexistent authfile
		session = podmanTest.Podman([]string{"logout", "--authfile", "/tmp/nonexistent", server})
		session.WaitWithDefaultTimeout()
		Expect(session).To(ExitWithError())
		Expect(session.ErrorToString()).To(Equal("Error: credential file is not accessible: stat /tmp/nonexistent: no such file or directory"))

		session = podmanTest.Podman([]string{"logout", "--authfile", authFile, server})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())
	})

	It("podman login and logout --compat-auth-file flag handling", func() {
		// A minimal smoke test
		compatAuthFile := filepath.Join(podmanTest.TempDir, "config.json")
		session := podmanTest.Podman([]string{"login", "--username", "podmantest", "--password", "test", "--compat-auth-file", compatAuthFile, server})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())

		readAuthInfo(compatAuthFile)

		session = podmanTest.Podman([]string{"logout", "--compat-auth-file", compatAuthFile, server})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())

		// logout should fail with nonexistent authfile
		session = podmanTest.Podman([]string{"logout", "--compat-auth-file", "/tmp/nonexistent", server})
		session.WaitWithDefaultTimeout()
		Expect(session).To(ExitWithError())
		Expect(session.ErrorToString()).To(Equal("Error: credential file is not accessible: stat /tmp/nonexistent: no such file or directory"))

		// inconsistent command line flags are rejected
		// Pre-create the files to make sure we are not hitting the “file not found” path
		authFile := filepath.Join(podmanTest.TempDir, "auth.json")
		err := os.WriteFile(authFile, []byte("{}"), 0o700)
		Expect(err).ToNot(HaveOccurred())
		err = os.WriteFile(compatAuthFile, []byte("{}"), 0o700)
		Expect(err).ToNot(HaveOccurred())

		session = podmanTest.Podman([]string{"login", "--username", "podmantest", "--password", "test",
			"--authfile", authFile, "--compat-auth-file", compatAuthFile, server})
		session.WaitWithDefaultTimeout()
		Expect(session).To(ExitWithError())
		Expect(session.ErrorToString()).To(Equal("Error: options for paths to the credential file and to the Docker-compatible credential file can not be set simultaneously"))

		session = podmanTest.Podman([]string{"logout", "--authfile", authFile, "--compat-auth-file", compatAuthFile, server})
		session.WaitWithDefaultTimeout()
		Expect(session).To(ExitWithError())
		Expect(session.ErrorToString()).To(Equal("Error: options for paths to the credential file and to the Docker-compatible credential file can not be set simultaneously"))
	})

	It("podman manifest with --authfile", func() {
		os.Unsetenv("REGISTRY_AUTH_FILE")

		authFile := filepath.Join(podmanTest.TempDir, "auth.json")
		session := podmanTest.Podman([]string{"login", "--username", "podmantest", "--password", "test", "--authfile", authFile, server})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())

		readAuthInfo(authFile)

		session = podmanTest.Podman([]string{"manifest", "create", testImg})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())

		session = podmanTest.Podman([]string{"manifest", "push", "-q", testImg})
		session.WaitWithDefaultTimeout()
		Expect(session).To(ExitWithError())
		Expect(session.ErrorToString()).To(ContainSubstring(": authentication required"))

		session = podmanTest.Podman([]string{"manifest", "push", "-q", "--authfile", authFile, testImg})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())

		// Now remove the local manifest to trigger remote inspection
		session = podmanTest.Podman([]string{"manifest", "rm", testImg})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())

		session = podmanTest.Podman([]string{"manifest", "inspect", testImg})
		session.WaitWithDefaultTimeout()
		Expect(session).To(ExitWithError())
		Expect(session.ErrorToString()).To(ContainSubstring(": authentication required"))

		session = podmanTest.Podman([]string{"manifest", "inspect", "--authfile", authFile, testImg})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())
	})

	It("podman login and logout with --tls-verify", func() {
		session := podmanTest.Podman([]string{"login", "--username", "podmantest", "--password", "test", "--tls-verify=false", server})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())

		session = podmanTest.Podman([]string{"push", "-q", ALPINE, testImg})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())

		session = podmanTest.Podman([]string{"logout", server})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())
	})
	It("podman login and logout with --cert-dir", func() {
		certDir := filepath.Join(podmanTest.TempDir, "certs")
		err := os.MkdirAll(certDir, os.ModePerm)
		Expect(err).ToNot(HaveOccurred())

		setup := SystemExec("cp", []string{filepath.Join(certPath, "domain.crt"), filepath.Join(certDir, "ca.crt")})
		setup.WaitWithDefaultTimeout()

		session := podmanTest.Podman([]string{"login", "--username", "podmantest", "--password", "test", "--cert-dir", certDir, server})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())

		session = podmanTest.Podman([]string{"push", "-q", "--cert-dir", certDir, ALPINE, testImg})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())

		session = podmanTest.Podman([]string{"logout", server})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())
	})
	It("podman login and logout with multi registry", func() {
		certDir := filepath.Join(os.Getenv("HOME"), ".config/containers/certs.d", "localhost:9001")
		err = os.MkdirAll(certDir, os.ModePerm)
		Expect(err).ToNot(HaveOccurred())

		cwd, _ := os.Getwd()
		certPath = filepath.Join(cwd, "../", "certs")

		setup := SystemExec("cp", []string{filepath.Join(certPath, "domain.crt"), filepath.Join(certDir, "ca.crt")})
		setup.WaitWithDefaultTimeout()
		defer os.RemoveAll(certDir)

		// N/B: This second registry container shares the same auth and cert dirs
		//      as the registry started from BeforeEach().  Since this one starts
		//      second, re-labeling the volumes should keep SELinux happy.
		session := podmanTest.Podman([]string{"run", "-d", "-p", "9001:9001", "-e", "REGISTRY_HTTP_ADDR=0.0.0.0:9001", "--name", "registry1", "-v",
			strings.Join([]string{authPath, "/auth:z"}, ":"), "-e", "REGISTRY_AUTH=htpasswd", "-e",
			"REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm", "-e", "REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd",
			"-v", strings.Join([]string{certPath, "/certs:z"}, ":"), "-e", "REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt",
			"-e", "REGISTRY_HTTP_TLS_KEY=/certs/domain.key", REGISTRY_IMAGE})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())

		if !WaitContainerReady(podmanTest, "registry1", "listening on", 20, 1) {
			Skip("Cannot start docker registry.")
		}

		session = podmanTest.Podman([]string{"login", "--username", "podmantest", "--password", "test", server})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())

		session = podmanTest.Podman([]string{"push", "-q", ALPINE, testImg})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())

		session = podmanTest.Podman([]string{"push", "-q", ALPINE, "localhost:9001/test-alpine"})
		session.WaitWithDefaultTimeout()
		Expect(session).To(ExitWithError())
		Expect(session.ErrorToString()).To(ContainSubstring("/test-alpine: authentication required"))

		session = podmanTest.Podman([]string{"login", "--username", "podmantest", "--password", "test", "localhost:9001"})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())

		session = podmanTest.Podman([]string{"push", "-q", ALPINE, testImg})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())

		session = podmanTest.Podman([]string{"push", "-q", ALPINE, "localhost:9001/test-alpine"})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())

		session = podmanTest.Podman([]string{"logout", server})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())

		session = podmanTest.Podman([]string{"push", "-q", ALPINE, testImg})
		session.WaitWithDefaultTimeout()
		Expect(session).To(ExitWithError())
		Expect(session.ErrorToString()).To(ContainSubstring("/test-alpine: authentication required"))

		session = podmanTest.Podman([]string{"push", "-q", ALPINE, "localhost:9001/test-alpine"})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())

		session = podmanTest.Podman([]string{"login", "--username", "podmantest", "--password", "test", "localhost:9001"})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())

		session = podmanTest.Podman([]string{"logout", "-a"})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())

		session = podmanTest.Podman([]string{"push", "-q", ALPINE, testImg})
		session.WaitWithDefaultTimeout()
		Expect(session).To(ExitWithError())
		Expect(session.ErrorToString()).To(ContainSubstring("/test-alpine: authentication required"))

		session = podmanTest.Podman([]string{"push", "-q", ALPINE, "localhost:9001/test-alpine"})
		session.WaitWithDefaultTimeout()
		Expect(session).To(ExitWithError())
		Expect(session.ErrorToString()).To(ContainSubstring("/test-alpine: authentication required"))
	})

	It("podman login and logout with repository", func() {
		authFile := filepath.Join(podmanTest.TempDir, "auth.json")

		testRepository := server + "/podmantest"
		session := podmanTest.Podman([]string{
			"login",
			"-u", "podmantest",
			"-p", "test",
			"--authfile", authFile,
			testRepository,
		})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())

		authInfo := readAuthInfo(authFile)
		Expect(authInfo).To(HaveKey(testRepository))

		session = podmanTest.Podman([]string{
			"logout",
			"--authfile", authFile,
			testRepository,
		})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())

		authInfo = readAuthInfo(authFile)
		Expect(authInfo).NotTo(HaveKey(testRepository))
	})

	It("podman login and logout with repository and specified image", func() {
		authFile := filepath.Join(podmanTest.TempDir, "auth.json")

		testTarget := server + "/podmantest/test-alpine"
		session := podmanTest.Podman([]string{
			"login",
			"-u", "podmantest",
			"-p", "test",
			"--authfile", authFile,
			testTarget,
		})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())

		authInfo := readAuthInfo(authFile)
		Expect(authInfo).To(HaveKey(testTarget))

		session = podmanTest.Podman([]string{
			"push", "-q",
			"--authfile", authFile,
			ALPINE, testTarget,
		})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())

	})

	It("podman login and logout with repository with fallback", func() {
		authFile := filepath.Join(podmanTest.TempDir, "auth.json")

		testRepos := []string{
			server + "/podmantest",
			server,
		}
		for _, testRepo := range testRepos {
			session := podmanTest.Podman([]string{
				"login",
				"-u", "podmantest",
				"-p", "test",
				"--authfile", authFile,
				testRepo,
			})
			session.WaitWithDefaultTimeout()
			Expect(session).Should(ExitCleanly())
		}

		authInfo := readAuthInfo(authFile)
		Expect(authInfo).To(HaveKey(testRepos[0]))
		Expect(authInfo).To(HaveKey(testRepos[1]))

		session := podmanTest.Podman([]string{
			"push", "-q",
			"--authfile", authFile,
			ALPINE, testRepos[0] + "/test-image-alpine",
		})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())

		session = podmanTest.Podman([]string{
			"logout",
			"--authfile", authFile,
			testRepos[0],
		})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())

		session = podmanTest.Podman([]string{
			"push", "-q",
			"--authfile", authFile,
			ALPINE, testRepos[0] + "/test-image-alpine",
		})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())

		session = podmanTest.Podman([]string{
			"logout",
			"--authfile", authFile,
			testRepos[1],
		})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())

		authInfo = readAuthInfo(authFile)
		Expect(authInfo).NotTo(HaveKey(testRepos[0]))
		Expect(authInfo).NotTo(HaveKey(testRepos[1]))
	})

	It("podman login with http{s} prefix", func() {
		authFile := filepath.Join(podmanTest.TempDir, "auth.json")

		for _, invalidArg := range []string{
			"https://" + server + "/podmantest",
			"http://" + server + "/podmantest/image:latest",
		} {
			session := podmanTest.Podman([]string{
				"login",
				"-u", "podmantest",
				"-p", "test",
				"--authfile", authFile,
				invalidArg,
			})
			session.WaitWithDefaultTimeout()
			Expect(session).To(ExitCleanly())
		}
	})

	It("podman login and logout with repository push with invalid auth.json credentials", func() {
		authFile := filepath.Join(podmanTest.TempDir, "auth.json")
		// only `server` contains the correct login data
		err := os.WriteFile(authFile, []byte(fmt.Sprintf(`{"auths": {
			"%s/podmantest": { "auth": "cG9kbWFudGVzdDp3cm9uZw==" },
			"%s": { "auth": "cG9kbWFudGVzdDp0ZXN0" }
		}}`, server, server)), 0644)
		Expect(err).ToNot(HaveOccurred())

		session := podmanTest.Podman([]string{
			"push", "-q",
			"--authfile", authFile,
			ALPINE, server + "/podmantest/test-image",
		})
		session.WaitWithDefaultTimeout()
		Expect(session).To(ExitWithError())
		Expect(session.ErrorToString()).To(ContainSubstring("/test-image: authentication required"))

		session = podmanTest.Podman([]string{
			"push", "-q",
			"--authfile", authFile,
			ALPINE, server + "/test-image",
		})
		session.WaitWithDefaultTimeout()
		Expect(session).To(ExitCleanly())
	})

	It("podman login and logout with repository pull with wrong auth.json credentials", func() {
		authFile := filepath.Join(podmanTest.TempDir, "auth.json")

		testTarget := server + "/podmantest/test-alpine"
		session := podmanTest.Podman([]string{
			"login",
			"-u", "podmantest",
			"-p", "test",
			"--authfile", authFile,
			testTarget,
		})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())

		session = podmanTest.Podman([]string{
			"push", "-q",
			"--authfile", authFile,
			ALPINE, testTarget,
		})
		session.WaitWithDefaultTimeout()
		Expect(session).Should(ExitCleanly())

		// only `server + /podmantest` and `server` have the correct login data
		err := os.WriteFile(authFile, []byte(fmt.Sprintf(`{"auths": {
			"%s/podmantest/test-alpine": { "auth": "cG9kbWFudGVzdDp3cm9uZw==" },
			"%s/podmantest": { "auth": "cG9kbWFudGVzdDp0ZXN0" },
			"%s": { "auth": "cG9kbWFudGVzdDp0ZXN0" }
		}}`, server, server, server)), 0644)
		Expect(err).ToNot(HaveOccurred())

		session = podmanTest.Podman([]string{
			"pull", "-q",
			"--authfile", authFile,
			server + "/podmantest/test-alpine",
		})
		session.WaitWithDefaultTimeout()
		Expect(session).To(ExitWithError())
		Expect(session.ErrorToString()).To(ContainSubstring("/test-alpine: authentication required"))
	})
})
