package image

import (
	"fmt"
	"os"
	"strings"
	"testing"

	"github.com/docker/cli/e2e/internal/fixtures"
	"github.com/docker/cli/internal/test/environment"
	"github.com/docker/cli/internal/test/output"
	"gotest.tools/v3/assert"
	"gotest.tools/v3/fs"
	"gotest.tools/v3/golden"
	"gotest.tools/v3/icmd"
	"gotest.tools/v3/skip"
)

const (
	notary = "/usr/local/bin/notary"

	pubkey1  = "./testdata/notary/delgkey1.crt"
	privkey1 = "./testdata/notary/delgkey1.key"
	pubkey2  = "./testdata/notary/delgkey2.crt"
	privkey2 = "./testdata/notary/delgkey2.key"
	pubkey3  = "./testdata/notary/delgkey3.crt"
	privkey3 = "./testdata/notary/delgkey3.key"
	pubkey4  = "./testdata/notary/delgkey4.crt"
	privkey4 = "./testdata/notary/delgkey4.key"
)

func TestPushAllTags(t *testing.T) {
	skip.If(t, environment.RemoteDaemon())

	// Compared digests are linux/amd64 specific.
	// TODO: Fix this test and make it work on all platforms.
	environment.SkipIfNotPlatform(t, "linux/amd64")

	_ = createImage(t, "push-all-tags", "latest", "v1", "v1.0", "v1.0.1")
	result := icmd.RunCmd(icmd.Command("docker", "push", "--all-tags", registryPrefix+"/push-all-tags"))

	result.Assert(t, icmd.Success)
	golden.Assert(t, result.Stderr(), "push-with-content-trust-err.golden")
	output.Assert(t, result.Stdout(), map[int]func(string) error{
		0:  output.Equals("The push refers to repository [registry:5000/push-all-tags]"),
		1:  output.Equals("7cd52847ad77: Preparing"),
		3:  output.Equals("latest: digest: sha256:e2e16842c9b54d985bf1ef9242a313f36b856181f188de21313820e177002501 size: 528"),
		6:  output.Equals("v1: digest: sha256:e2e16842c9b54d985bf1ef9242a313f36b856181f188de21313820e177002501 size: 528"),
		9:  output.Equals("v1.0: digest: sha256:e2e16842c9b54d985bf1ef9242a313f36b856181f188de21313820e177002501 size: 528"),
		12: output.Equals("v1.0.1: digest: sha256:e2e16842c9b54d985bf1ef9242a313f36b856181f188de21313820e177002501 size: 528"),
	})
}

func TestPushWithContentTrust(t *testing.T) {
	skip.If(t, environment.RemoteDaemon())

	// Compared digests are linux/amd64 specific.
	// TODO: Fix this test and make it work on all platforms.
	environment.SkipIfNotPlatform(t, "linux/amd64")

	dir := fixtures.SetupConfigFile(t)
	defer dir.Remove()
	image := createImage(t, "trust-push", "latest")

	result := icmd.RunCmd(icmd.Command("docker", "push", image),
		fixtures.WithConfig(dir.Path()),
		fixtures.WithTrust,
		fixtures.WithNotary,
		fixtures.WithPassphrase("foo", "bar"),
	)
	result.Assert(t, icmd.Success)
	golden.Assert(t, result.Stderr(), "push-with-content-trust-err.golden")
	output.Assert(t, result.Stdout(), map[int]func(string) error{
		0: output.Equals("The push refers to repository [registry:5000/trust-push]"),
		1: output.Equals("7cd52847ad77: Preparing"),
		3: output.Equals("latest: digest: sha256:e2e16842c9b54d985bf1ef9242a313f36b856181f188de21313820e177002501 size: 528"),
		4: output.Equals("Signing and pushing trust metadata"),
		5: output.Equals(`Finished initializing "registry:5000/trust-push"`),
		6: output.Equals("Successfully signed registry:5000/trust-push:latest"),
	})
}

func TestPushQuietErrors(t *testing.T) {
	result := icmd.RunCmd(icmd.Command("docker", "push", "--quiet", "nosuchimage"))
	result.Assert(t, icmd.Expected{
		ExitCode: 1,
		Err:      "An image does not exist locally with the tag: nosuchimage",
	})
}

func TestPushWithContentTrustUnreachableServer(t *testing.T) {
	skip.If(t, environment.RemoteDaemon())

	dir := fixtures.SetupConfigFile(t)
	defer dir.Remove()
	image := createImage(t, "trust-push-unreachable", "latest")

	result := icmd.RunCmd(icmd.Command("docker", "push", image),
		fixtures.WithConfig(dir.Path()),
		fixtures.WithTrust,
		fixtures.WithNotaryServer("https://invalidnotaryserver"),
	)
	result.Assert(t, icmd.Expected{
		ExitCode: 1,
		Err:      "error contacting notary server",
	})
}

func TestPushWithContentTrustExistingTag(t *testing.T) {
	skip.If(t, environment.RemoteDaemon())

	dir := fixtures.SetupConfigFile(t)
	defer dir.Remove()
	image := createImage(t, "trust-push-existing", "latest")

	result := icmd.RunCmd(icmd.Command("docker", "push", image))
	result.Assert(t, icmd.Success)

	result = icmd.RunCmd(icmd.Command("docker", "push", image),
		fixtures.WithConfig(dir.Path()),
		fixtures.WithTrust,
		fixtures.WithNotary,
		fixtures.WithPassphrase("foo", "bar"),
	)
	result.Assert(t, icmd.Expected{
		Out: "Signing and pushing trust metadata",
	})

	// Re-push
	result = icmd.RunCmd(icmd.Command("docker", "push", image),
		fixtures.WithConfig(dir.Path()),
		fixtures.WithTrust,
		fixtures.WithNotary,
		fixtures.WithPassphrase("foo", "bar"),
	)
	result.Assert(t, icmd.Expected{
		Out: "Signing and pushing trust metadata",
	})
}

func TestPushWithContentTrustReleasesDelegationOnly(t *testing.T) {
	skip.If(t, environment.RemoteDaemon())

	role := "targets/releases"

	dir := fixtures.SetupConfigFile(t)
	defer dir.Remove()
	copyPrivateKey(t, dir.Join("trust", "private"), privkey1)
	notaryDir := setupNotaryConfig(t, dir)
	defer notaryDir.Remove()
	homeDir := fs.NewDir(t, "push_test_home")
	defer notaryDir.Remove()

	baseRef := fmt.Sprintf("%s/%s", registryPrefix, "trust-push-releases-delegation")
	targetRef := fmt.Sprintf("%s:%s", baseRef, "latest")

	// Init repository
	notaryInit(t, notaryDir, homeDir, baseRef)
	// Add delegation key (public key)
	notaryAddDelegation(t, notaryDir, homeDir, baseRef, role, pubkey1)
	// Publish it
	notaryPublish(t, notaryDir, homeDir, baseRef)
	// Import private key
	notaryImportPrivateKey(t, notaryDir, homeDir, baseRef, role, privkey1)

	// Tag & push with content trust
	icmd.RunCommand("docker", "pull", fixtures.AlpineImage).Assert(t, icmd.Success)
	icmd.RunCommand("docker", "tag", fixtures.AlpineImage, targetRef).Assert(t, icmd.Success)
	result := icmd.RunCmd(icmd.Command("docker", "push", targetRef),
		fixtures.WithConfig(dir.Path()),
		fixtures.WithTrust,
		fixtures.WithNotary,
		fixtures.WithPassphrase("foo", "foo"),
	)
	result.Assert(t, icmd.Expected{
		Out: "Signing and pushing trust metadata",
	})

	targetsInRole := notaryListTargetsInRole(t, notaryDir, homeDir, baseRef, role)
	assert.Assert(t, targetsInRole["latest"] == role, "%v", targetsInRole)
	targetsInRole = notaryListTargetsInRole(t, notaryDir, homeDir, baseRef, "targets")
	assert.Assert(t, targetsInRole["latest"] != "targets", "%v", targetsInRole)

	result = icmd.RunCmd(icmd.Command("docker", "pull", targetRef),
		fixtures.WithConfig(dir.Path()),
		fixtures.WithTrust,
		fixtures.WithNotary,
	)
	result.Assert(t, icmd.Success)
}

func TestPushWithContentTrustSignsAllFirstLevelRolesWeHaveKeysFor(t *testing.T) {
	skip.If(t, environment.RemoteDaemon())

	dir := fixtures.SetupConfigFile(t)
	defer dir.Remove()
	copyPrivateKey(t, dir.Join("trust", "private"), privkey1)
	copyPrivateKey(t, dir.Join("trust", "private"), privkey2)
	copyPrivateKey(t, dir.Join("trust", "private"), privkey3)
	notaryDir := setupNotaryConfig(t, dir)
	defer notaryDir.Remove()
	homeDir := fs.NewDir(t, "push_test_home")
	defer notaryDir.Remove()

	baseRef := fmt.Sprintf("%s/%s", registryPrefix, "trust-push-releases-first-roles")
	targetRef := fmt.Sprintf("%s:%s", baseRef, "latest")

	// Init repository
	notaryInit(t, notaryDir, homeDir, baseRef)
	// Add delegation key (public key)
	notaryAddDelegation(t, notaryDir, homeDir, baseRef, "targets/role1", pubkey1)
	notaryAddDelegation(t, notaryDir, homeDir, baseRef, "targets/role2", pubkey2)
	notaryAddDelegation(t, notaryDir, homeDir, baseRef, "targets/role3", pubkey3)
	notaryAddDelegation(t, notaryDir, homeDir, baseRef, "targets/role1/subrole", pubkey3)
	// Import private key
	notaryImportPrivateKey(t, notaryDir, homeDir, baseRef, "targets/role1", privkey1)
	notaryImportPrivateKey(t, notaryDir, homeDir, baseRef, "targets/role2", privkey2)
	notaryImportPrivateKey(t, notaryDir, homeDir, baseRef, "targets/role1/subrole", privkey3)
	// Publish it
	notaryPublish(t, notaryDir, homeDir, baseRef)

	// Tag & push with content trust
	icmd.RunCommand("docker", "pull", fixtures.AlpineImage).Assert(t, icmd.Success)
	icmd.RunCommand("docker", "tag", fixtures.AlpineImage, targetRef).Assert(t, icmd.Success)
	result := icmd.RunCmd(icmd.Command("docker", "push", targetRef),
		fixtures.WithConfig(dir.Path()),
		fixtures.WithTrust,
		fixtures.WithNotary,
		fixtures.WithPassphrase("foo", "foo"),
	)
	result.Assert(t, icmd.Expected{
		Out: "Signing and pushing trust metadata",
	})

	// check to make sure that the target has been added to targets/role1 and targets/role2, and
	// not targets (because there are delegations) or targets/role3 (due to missing key) or
	// targets/role1/subrole (due to it being a second level delegation)
	targetsInRole := notaryListTargetsInRole(t, notaryDir, homeDir, baseRef, "targets/role1")
	assert.Assert(t, targetsInRole["latest"] == "targets/role1", "%v", targetsInRole)
	targetsInRole = notaryListTargetsInRole(t, notaryDir, homeDir, baseRef, "targets/role2")
	assert.Assert(t, targetsInRole["latest"] == "targets/role2", "%v", targetsInRole)
	targetsInRole = notaryListTargetsInRole(t, notaryDir, homeDir, baseRef, "targets")
	assert.Assert(t, targetsInRole["latest"] != "targets", "%v", targetsInRole)

	assert.NilError(t, os.RemoveAll(dir.Join("trust")))
	// Try to pull, should fail because non of these are the release role
	// FIXME(vdemeester) should be unit test
	result = icmd.RunCmd(icmd.Command("docker", "pull", targetRef),
		fixtures.WithConfig(dir.Path()),
		fixtures.WithTrust,
		fixtures.WithNotary,
	)
	result.Assert(t, icmd.Expected{
		ExitCode: 1,
	})
}

func TestPushWithContentTrustSignsForRolesWithKeysAndValidPaths(t *testing.T) {
	skip.If(t, environment.RemoteDaemon())

	dir := fixtures.SetupConfigFile(t)
	defer dir.Remove()
	copyPrivateKey(t, dir.Join("trust", "private"), privkey1)
	copyPrivateKey(t, dir.Join("trust", "private"), privkey2)
	copyPrivateKey(t, dir.Join("trust", "private"), privkey3)
	copyPrivateKey(t, dir.Join("trust", "private"), privkey4)
	notaryDir := setupNotaryConfig(t, dir)
	defer notaryDir.Remove()
	homeDir := fs.NewDir(t, "push_test_home")
	defer notaryDir.Remove()

	baseRef := fmt.Sprintf("%s/%s", registryPrefix, "trust-push-releases-keys-valid-paths")
	targetRef := fmt.Sprintf("%s:%s", baseRef, "latest")

	// Init repository
	notaryInit(t, notaryDir, homeDir, baseRef)
	// Add delegation key (public key)
	notaryAddDelegation(t, notaryDir, homeDir, baseRef, "targets/role1", pubkey1, "l", "z")
	notaryAddDelegation(t, notaryDir, homeDir, baseRef, "targets/role2", pubkey2, "x", "y")
	notaryAddDelegation(t, notaryDir, homeDir, baseRef, "targets/role3", pubkey3, "latest")
	notaryAddDelegation(t, notaryDir, homeDir, baseRef, "targets/role4", pubkey4, "latest")
	// Import private keys (except 3rd key)
	notaryImportPrivateKey(t, notaryDir, homeDir, baseRef, "targets/role1", privkey1)
	notaryImportPrivateKey(t, notaryDir, homeDir, baseRef, "targets/role2", privkey2)
	notaryImportPrivateKey(t, notaryDir, homeDir, baseRef, "targets/role4", privkey4)
	// Publish it
	notaryPublish(t, notaryDir, homeDir, baseRef)

	// Tag & push with content trust
	icmd.RunCommand("docker", "pull", fixtures.AlpineImage).Assert(t, icmd.Success)
	icmd.RunCommand("docker", "tag", fixtures.AlpineImage, targetRef).Assert(t, icmd.Success)
	result := icmd.RunCmd(icmd.Command("docker", "push", targetRef),
		fixtures.WithConfig(dir.Path()),
		fixtures.WithTrust,
		fixtures.WithNotary,
		fixtures.WithPassphrase("foo", "foo"),
	)
	result.Assert(t, icmd.Expected{
		Out: "Signing and pushing trust metadata",
	})

	// check to make sure that the target has been added to targets/role1 and targets/role4, and
	// not targets (because there are delegations) or targets/role2 (due to path restrictions) or
	// targets/role3 (due to missing key)
	targetsInRole := notaryListTargetsInRole(t, notaryDir, homeDir, baseRef, "targets/role1")
	assert.Assert(t, targetsInRole["latest"] == "targets/role1", "%v", targetsInRole)
	targetsInRole = notaryListTargetsInRole(t, notaryDir, homeDir, baseRef, "targets/role4")
	assert.Assert(t, targetsInRole["latest"] == "targets/role4", "%v", targetsInRole)
	targetsInRole = notaryListTargetsInRole(t, notaryDir, homeDir, baseRef, "targets")
	assert.Assert(t, targetsInRole["latest"] != "targets", "%v", targetsInRole)

	assert.NilError(t, os.RemoveAll(dir.Join("trust")))
	// Try to pull, should fail because non of these are the release role
	// FIXME(vdemeester) should be unit test
	result = icmd.RunCmd(icmd.Command("docker", "pull", targetRef),
		fixtures.WithConfig(dir.Path()),
		fixtures.WithTrust,
		fixtures.WithNotary,
	)
	result.Assert(t, icmd.Expected{
		ExitCode: 1,
	})
}

func createImage(t *testing.T, repo string, tags ...string) string {
	t.Helper()
	icmd.RunCommand("docker", "pull", fixtures.AlpineImage).Assert(t, icmd.Success)

	for _, tag := range tags {
		image := fmt.Sprintf("%s/%s:%s", registryPrefix, repo, tag)
		icmd.RunCommand("docker", "tag", fixtures.AlpineImage, image).Assert(t, icmd.Success)
	}
	return fmt.Sprintf("%s/%s:%s", registryPrefix, repo, tags[0])
}

//nolint:unparam
func withNotaryPassphrase(pwd string) func(*icmd.Cmd) {
	return func(c *icmd.Cmd) {
		c.Env = append(c.Env, []string{
			"NOTARY_ROOT_PASSPHRASE=" + pwd,
			"NOTARY_TARGETS_PASSPHRASE=" + pwd,
			"NOTARY_SNAPSHOT_PASSPHRASE=" + pwd,
			"NOTARY_DELEGATION_PASSPHRASE=" + pwd,
		}...)
	}
}

func notaryImportPrivateKey(t *testing.T, notaryDir, homeDir *fs.Dir, baseRef, role, privkey string) {
	t.Helper()
	icmd.RunCmd(
		icmd.Command(notary, "-c", notaryDir.Join("client-config.json"), "key", "import", privkey, "-g", baseRef, "-r", role),
		withNotaryPassphrase("foo"),
		fixtures.WithHome(homeDir.Path()),
	).Assert(t, icmd.Success)
}

func notaryPublish(t *testing.T, notaryDir, homeDir *fs.Dir, baseRef string) {
	t.Helper()
	icmd.RunCmd(
		icmd.Command(notary, "-c", notaryDir.Join("client-config.json"), "publish", baseRef),
		withNotaryPassphrase("foo"),
		fixtures.WithHome(homeDir.Path()),
	).Assert(t, icmd.Success)
}

func notaryAddDelegation(t *testing.T, notaryDir, homeDir *fs.Dir, baseRef, role, pubkey string, paths ...string) {
	t.Helper()
	pathsArg := "--all-paths"
	if len(paths) > 0 {
		pathsArg = "--paths=" + strings.Join(paths, ",")
	}
	icmd.RunCmd(
		icmd.Command(notary, "-c", notaryDir.Join("client-config.json"), "delegation", "add", baseRef, role, pubkey, pathsArg),
		withNotaryPassphrase("foo"),
		fixtures.WithHome(homeDir.Path()),
	).Assert(t, icmd.Success)
}

func notaryInit(t *testing.T, notaryDir, homeDir *fs.Dir, baseRef string) {
	t.Helper()
	icmd.RunCmd(
		icmd.Command(notary, "-c", notaryDir.Join("client-config.json"), "init", baseRef),
		withNotaryPassphrase("foo"),
		fixtures.WithHome(homeDir.Path()),
	).Assert(t, icmd.Success)
}

func notaryListTargetsInRole(t *testing.T, notaryDir, homeDir *fs.Dir, baseRef, role string) map[string]string {
	t.Helper()
	result := icmd.RunCmd(
		icmd.Command(notary, "-c", notaryDir.Join("client-config.json"), "list", baseRef, "-r", role),
		fixtures.WithHome(homeDir.Path()),
	)
	out := result.Combined()

	// should look something like:
	//    NAME                                 DIGEST                                SIZE (BYTES)    ROLE
	// ------------------------------------------------------------------------------------------------------
	//   latest   24a36bbc059b1345b7e8be0df20f1b23caa3602e85d42fff7ecd9d0bd255de56   1377           targets

	targets := make(map[string]string)

	// no target
	lines := strings.Split(strings.TrimSpace(out), "\n")
	if len(lines) == 1 && strings.Contains(out, "No targets present in this repository.") {
		return targets
	}

	// otherwise, there is at least one target
	assert.Assert(t, len(lines) >= 3, "output is %s", out)

	for _, line := range lines[2:] {
		tokens := strings.Fields(line)
		assert.Assert(t, len(tokens) == 4)
		targets[tokens[0]] = tokens[3]
	}

	return targets
}

func setupNotaryConfig(t *testing.T, dockerConfigDir fs.Dir) *fs.Dir {
	t.Helper()
	return fs.NewDir(t, "notary_test", fs.WithMode(0o700),
		fs.WithFile("client-config.json", fmt.Sprintf(`
{
	"trust_dir": "%s",
	"remote_server": {
		"url": "%s"
	}
}`, dockerConfigDir.Join("trust"), fixtures.NotaryURL)),
	)
}

func copyPrivateKey(t *testing.T, dir, source string) {
	t.Helper()
	icmd.RunCommand("/bin/cp", source, dir).Assert(t, icmd.Success)
}
