package integration

import (
	"reflect"
	"strings"
	"testing"
	"time"

	"github.com/openshift/origin/pkg/dockerregistry"
)

const (
	pulpRegistryName        = "registry.access.redhat.com"
	dockerHubV2RegistryName = "index.docker.io"
	dockerHubV1RegistryName = "registry.hub.docker.com"
	quayRegistryName        = "quay.io"

	maxRetryCount = 4
	retryAfter    = time.Millisecond * 500
)

var (
	// Below are lists of error patterns for use with `retryOnErrors` utility.

	// unreachableErrorPatterns will match following error examples:
	//   Get https://registry.com/v2/: dial tcp registry.com:443: i/o timeout
	//   Get https://registry.com/v2/: dial tcp: lookup registry.com: no such host
	//   Get https://registry.com/v2/: dial tcp registry.com:443: getsockopt: connection refused
	//   Get https://registry.com/v2/: read tcp 127.0.0.1:39849->registry.com:443: read: connection reset by peer
	//   Get https://registry.com/v2/: net/http: request cancelled while waiting for connection
	//   Get https://registry.com/v2/: net/http: TLS handshake timeout
	//   the registry "https://registry.com/v2/" could not be reached
	unreachableErrorPatterns = []string{
		"dial tcp",
		"read tcp",
		"net/http",
		"could not be reached",
	}

	// imageNotFoundErrorPatterns will match following error examples:
	//   the image "..." in repository "..." was not found and may have been deleted
	//   tag "..." has not been set on repository "..."
	// use only with non-internal registry
	imageNotFoundErrorPatterns = []string{
		"was not found and may have been deleted",
		"has not been set on repository",
	}
)

// retryOnErrors invokes given function several times until it succeeds,
// returns unexpected error or a maximum number of attempts is reached. It
// should be used to wrap calls to remote registry to prevent test failures
// because of short-term outages or image updates.
func retryOnErrors(t *testing.T, errorPatterns []string, f func() error) error {
	timeout := retryAfter
	attempt := 0
	for err := f(); err != nil; err = f() {
		match := false
		for _, pattern := range errorPatterns {
			if strings.Contains(err.Error(), pattern) {
				match = true
				break
			}
		}

		if !match || attempt >= maxRetryCount {
			return err
		}

		t.Logf("caught error \"%v\", retrying in %s", err, timeout.String())
		time.Sleep(timeout)
		timeout = timeout * 2
		attempt += 1
	}
	return nil
}

// retryWhenUnreachable is a convenient wrapper for retryOnErrors that makes it
// retry when the registry is not reachable. Additional error patterns may
// follow.
func retryWhenUnreachable(t *testing.T, f func() error, errorPatterns ...string) error {
	return retryOnErrors(t, append(errorPatterns, unreachableErrorPatterns...), f)
}

func TestRegistryClientConnect(t *testing.T) {
	c := dockerregistry.NewClient(10*time.Second, true)
	conn, err := c.Connect("docker.io", false)
	if err != nil {
		t.Fatal(err)
	}
	for _, s := range []string{"index.docker.io", "https://docker.io", "https://index.docker.io"} {
		otherConn, err := c.Connect(s, false)
		if err != nil {
			t.Errorf("%s: can't connect: %v", s, err)
			continue
		}
		if !reflect.DeepEqual(otherConn, conn) {
			t.Errorf("%s: did not reuse connection: %#v %#v", s, conn, otherConn)
		}
	}

	otherConn, err := c.Connect("index.docker.io:443", false)
	if err != nil || reflect.DeepEqual(otherConn, conn) {
		t.Errorf("should not have reused index.docker.io:443: %v", err)
	}

	if _, err := c.Connect("http://ba%3/", false); err == nil {
		t.Error("Unexpected non-error")
	}
}

func TestRegistryClientConnectPulpRegistry(t *testing.T) {
	c := dockerregistry.NewClient(10*time.Second, true)
	conn, err := c.Connect(pulpRegistryName, false)
	if err != nil {
		t.Fatal(err)
	}

	var image *dockerregistry.Image
	err = retryWhenUnreachable(t, func() error {
		image, err = conn.ImageByTag("library", "rhel", "latest")
		return err
	}, imageNotFoundErrorPatterns...)
	if err != nil {
		if strings.Contains(err.Error(), "x509: certificate has expired or is not yet valid") {
			t.Skip("SKIPPING: due to expired certificate of %s: %v", pulpRegistryName, err)
		}
		t.Skip("pulp is failing")
		//t.Fatal(err)
	}
	if len(image.ID) == 0 {
		t.Fatalf("image had no ID: %#v", image)
	}
}

func TestRegistryClientDockerHubV2(t *testing.T) {
	c := dockerregistry.NewClient(10*time.Second, true)
	conn, err := c.Connect(dockerHubV2RegistryName, false)
	if err != nil {
		t.Fatal(err)
	}

	var image *dockerregistry.Image
	err = retryWhenUnreachable(t, func() error {
		image, err = conn.ImageByTag("kubernetes", "guestbook", "latest")
		return err
	})
	if err != nil {
		t.Fatal(err)
	}
	if len(image.ID) == 0 {
		t.Fatalf("image had no ID: %#v", image)
	}
}

func TestRegistryClientDockerHubV1(t *testing.T) {
	c := dockerregistry.NewClient(10*time.Second, true)
	// a v1 only path
	conn, err := c.Connect(dockerHubV1RegistryName, false)
	if err != nil {
		t.Fatal(err)
	}

	var image *dockerregistry.Image
	err = retryWhenUnreachable(t, func() error {
		image, err = conn.ImageByTag("kubernetes", "guestbook", "latest")
		return err
	})
	if err != nil {
		t.Fatal(err)
	}
	if len(image.ID) == 0 {
		t.Fatalf("image had no ID: %#v", image)
	}
}

func TestRegistryClientRegistryNotFound(t *testing.T) {
	conn, err := dockerregistry.NewClient(10*time.Second, true).Connect("localhost:65000", false)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := conn.ImageByID("foo", "bar", "baz"); !dockerregistry.IsRegistryNotFound(err) {
		t.Error(err)
	}
}

func doTestRegistryClientImage(t *testing.T, registry, reponame, version string) {
	conn, err := dockerregistry.NewClient(10*time.Second, version == "v2").Connect(registry, false)
	if err != nil {
		t.Fatal(err)
	}

	err = retryWhenUnreachable(t, func() error {
		_, err := conn.ImageByTag("openshift", "origin-not-found", "latest")
		return err
	})
	if err == nil || (!dockerregistry.IsRepositoryNotFound(err) && !dockerregistry.IsTagNotFound(err)) {
		t.Errorf("%s: unexpected error: %v", version, err)
	}

	var image *dockerregistry.Image
	err = retryWhenUnreachable(t, func() error {
		image, err = conn.ImageByTag("openshift", reponame, "latest")
		return err
	})
	if err != nil {
		t.Fatal(err)
	}

	if image.Comment != "Imported from -" {
		t.Errorf("%s: unexpected image comment", version)
	}

	if image.Architecture != "amd64" {
		t.Errorf("%s: unexpected image architecture", version)
	}

	if version == "v2" && !image.PullByID {
		t.Errorf("%s: should be able to pull by ID %s", version, image.ID)
	}

	var other *dockerregistry.Image
	err = retryWhenUnreachable(t, func() error {
		other, err = conn.ImageByID("openshift", reponame, image.ID)
		return err
	})
	if err != nil {
		t.Fatal(err)
	}
	if !reflect.DeepEqual(other.ContainerConfig.Entrypoint, image.ContainerConfig.Entrypoint) {
		t.Errorf("%s: unexpected image: %#v", version, other)
	}
}

func TestRegistryClientAPIv2ManifestV2Schema2(t *testing.T) {
	t.Log("openshift/schema-v2-test-repo was pushed by Docker 1.11.1")
	doTestRegistryClientImage(t, dockerHubV2RegistryName, "schema-v2-test-repo", "v2")
}

func TestRegistryClientAPIv2ManifestV2Schema1(t *testing.T) {
	t.Log("openshift/schema-v1-test-repo was pushed by Docker 1.8.2")
	doTestRegistryClientImage(t, dockerHubV2RegistryName, "schema-v1-test-repo", "v2")
}

func TestRegistryClientAPIv1(t *testing.T) {
	t.Log("openshift/schema-v1-test-repo was pushed by Docker 1.8.2")
	doTestRegistryClientImage(t, dockerHubV1RegistryName, "schema-v1-test-repo", "v1")
}

func TestRegistryClientQuayIOImage(t *testing.T) {
	conn, err := dockerregistry.NewClient(10*time.Second, true).Connect("quay.io", false)
	if err != nil {
		t.Fatal(err)
	}
	err = retryWhenUnreachable(t, func() error {
		_, err := conn.ImageByTag("coreos", "etcd", "latest")
		return err
	}, imageNotFoundErrorPatterns...)
	if err != nil {
		t.Skip("SKIPPING: unexpected error from quay.io: %v", err)
	}
}