// Copyright 2020 Google LLC All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package remote

import (
	"context"
	"io"
	"log"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"
	"strings"
	"testing"

	"github.com/google/go-containerregistry/pkg/authn"
	"github.com/google/go-containerregistry/pkg/logs"
	"github.com/google/go-containerregistry/pkg/name"
	"github.com/google/go-containerregistry/pkg/registry"
	v1 "github.com/google/go-containerregistry/pkg/v1"
	"github.com/google/go-containerregistry/pkg/v1/empty"
	"github.com/google/go-containerregistry/pkg/v1/mutate"
	"github.com/google/go-containerregistry/pkg/v1/random"
	"github.com/google/go-containerregistry/pkg/v1/remote/transport"
	"github.com/google/go-containerregistry/pkg/v1/stream"
	"github.com/google/go-containerregistry/pkg/v1/types"
	"github.com/google/go-containerregistry/pkg/v1/validate"
)

func streamable(t *testing.T) v1.Layer {
	t.Helper()
	rl, err := random.Layer(1024, types.OCIUncompressedLayer)
	if err != nil {
		t.Fatal("random.Layer:", err)
	}
	rc, err := rl.Uncompressed()
	if err != nil {
		t.Fatalf("Uncompressed(): %v", err)
	}

	return stream.NewLayer(rc)
}

type rawManifest struct {
	b []byte
}

func (r *rawManifest) RawManifest() ([]byte, error) {
	return r.b, nil
}

func TestMultiWrite(t *testing.T) {
	c := make(chan v1.Update, 1000)

	logs.Progress.SetOutput(os.Stderr)
	logs.Warn.SetOutput(os.Stderr)

	// Create a random image.
	img1, err := random.Image(1024, 2)
	if err != nil {
		t.Fatal("random.Image:", err)
	}

	// Create another image that's based on the first.
	rl, err := random.Layer(1024, types.OCIUncompressedLayer)
	if err != nil {
		t.Fatal("random.Layer:", err)
	}
	img2, err := mutate.AppendLayers(img1, rl, streamable(t))
	if err != nil {
		t.Fatal("mutate.AppendLayers:", err)
	}

	// Also create a random index of images.
	subidx, err := random.Index(1024, 2, 3)
	if err != nil {
		t.Fatal("random.Index:", err)
	}

	// Add a sub-sub-index of random images.
	subsubidx, err := random.Index(1024, 3, 4)
	if err != nil {
		t.Fatal("random.Index:", err)
	}
	subidx = mutate.AppendManifests(subidx, mutate.IndexAddendum{Add: subsubidx})

	// Create an index containing both images and the index above.
	idx := mutate.AppendManifests(empty.Index,
		mutate.IndexAddendum{Add: img1},
		mutate.IndexAddendum{Add: img2},
		mutate.IndexAddendum{Add: subidx},
		mutate.IndexAddendum{Add: rl},
	)

	// Set up a fake registry.
	nopLog := log.New(io.Discard, "", 0)
	s := httptest.NewServer(registry.New(registry.Logger(nopLog)))
	defer s.Close()
	u, err := url.Parse(s.URL)
	if err != nil {
		t.Fatal(err)
	}

	// Write both images and the manifest list.
	tag1, tag2, tag3, tag4, tag5 := mustNewTag(t, u.Host+"/repo2:tag1"), mustNewTag(t, u.Host+"/repo:tag2"), mustNewTag(t, u.Host+"/repo:tag3"), mustNewTag(t, u.Host+"/repo:tag4"), mustNewTag(t, u.Host+"/repo1:tag4")

	if err := MultiWrite(map[name.Reference]Taggable{
		tag1: img1,
		tag2: img2,
		tag3: idx,
	}, WithProgress(c)); err != nil {
		t.Fatal("MultiWrite:", err)
	}

	// Check that tagged images are present.
	for _, tag := range []name.Tag{tag1, tag2} {
		got, err := Image(tag)
		if err != nil {
			t.Error(err)
			continue
		}
		if err := validate.Image(got); err != nil {
			t.Error("Validate() =", err)
		}
	}

	// Check that tagged manfest list is present and valid.
	got, err := Index(tag3)
	if err != nil {
		t.Fatal(err)
	}
	if err := validate.Index(got); err != nil {
		t.Error("Validate() =", err)
	}

	if err := checkUpdates(c); err != nil {
		t.Fatal(err)
	}

	desc1, err := Get(tag1)
	if err != nil {
		t.Fatal(err)
	}
	desc2, err := Get(tag3)
	if err != nil {
		t.Fatal(err)
	}

	rm := &rawManifest{[]byte("{}")}

	// Hit "already exists" coverage paths and move some tags.
	if err := MultiWrite(map[name.Reference]Taggable{
		tag1: img2,
		tag2: img1,
		tag3: desc2,
		tag4: desc1,
		tag5: rm,
	}); err != nil {
		t.Fatal("MultiWrite:", err)
	}
}

func TestMultiWriteWithNondistributableLayer(t *testing.T) {
	// Create a random image.
	img1, err := random.Image(1024, 2)
	if err != nil {
		t.Fatal("random.Image:", err)
	}

	// Create another image that's based on the first.
	rl, err := random.Layer(1024, types.OCIRestrictedLayer)
	if err != nil {
		t.Fatal("random.Layer:", err)
	}
	img, err := mutate.AppendLayers(img1, rl)
	if err != nil {
		t.Fatal("mutate.AppendLayers:", err)
	}

	// Set up a fake registry.
	s := httptest.NewServer(registry.New())
	defer s.Close()
	u, err := url.Parse(s.URL)
	if err != nil {
		t.Fatal(err)
	}

	// Write the image.
	tag1 := mustNewTag(t, u.Host+"/repo:tag1")
	if err := MultiWrite(map[name.Reference]Taggable{tag1: img}, WithNondistributable); err != nil {
		t.Error("Write:", err)
	}

	// Check that tagged image is present.
	got, err := Image(tag1)
	if err != nil {
		t.Error(err)
	}
	if err := validate.Image(got); err != nil {
		t.Error("Validate() =", err)
	}
}

func TestMultiWrite_Retry(t *testing.T) {
	// Create a random image.
	img1, err := random.Image(1024, 2)
	if err != nil {
		t.Fatal("random.Image:", err)
	}

	t.Run("retry http error 500", func(t *testing.T) {
		// Set up a fake registry.
		handler := registry.New()

		numOfInternalServerErrors := 0
		registryThatFailsOnFirstUpload := http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) {
			if strings.Contains(request.URL.Path, "/manifests/") && numOfInternalServerErrors < 1 {
				numOfInternalServerErrors++
				responseWriter.WriteHeader(500)
				return
			}
			handler.ServeHTTP(responseWriter, request)
		})

		s := httptest.NewServer(registryThatFailsOnFirstUpload)
		defer s.Close()
		u, err := url.Parse(s.URL)
		if err != nil {
			t.Fatal(err)
		}

		tag1 := mustNewTag(t, u.Host+"/repo:tag1")
		if err := MultiWrite(map[name.Reference]Taggable{
			tag1: img1,
		}, WithRetryBackoff(fastBackoff)); err != nil {
			t.Error("Write:", err)
		}
	})

	t.Run("do not retry transport errors if transport.Wrapper is used", func(t *testing.T) {
		// reference a http server that is not listening (used to pick a port that isn't listening)
		onlyHandlesPing := http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) {
			if strings.HasSuffix(request.URL.Path, "/v2/") {
				responseWriter.WriteHeader(200)
				return
			}
		})
		s := httptest.NewServer(onlyHandlesPing)
		defer s.Close()

		u, err := url.Parse(s.URL)
		if err != nil {
			t.Fatal(err)
		}

		tag1 := mustNewTag(t, u.Host+"/repo:tag1")

		// using a transport.Wrapper, meaning retry logic should not be wrapped
		doesNotRetryTransport := &countTransport{inner: http.DefaultTransport}
		transportWrapper, err := transport.NewWithContext(context.Background(), tag1.Repository.Registry, authn.Anonymous, doesNotRetryTransport, nil)
		if err != nil {
			t.Fatal(err)
		}

		noRetry := func(error) bool { return false }

		if err := MultiWrite(map[name.Reference]Taggable{
			tag1: img1,
		}, WithTransport(transportWrapper), WithJobs(1), WithRetryPredicate(noRetry)); err == nil {
			t.Errorf("Expected an error, got nil")
		}

		// expect count == 1 since jobs is set to 1 and we should not retry on transport eof error
		if doesNotRetryTransport.count != 1 {
			t.Errorf("Incorrect count, got %d, want %d", doesNotRetryTransport.count, 1)
		}
	})

	t.Run("do not add UserAgent if transport.Wrapper is used", func(t *testing.T) {
		expectedNotUsedUserAgent := "TEST_USER_AGENT"

		handler := registry.New()

		registryThatAssertsUserAgentIsCorrect := http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) {
			if strings.Contains(request.Header.Get("User-Agent"), expectedNotUsedUserAgent) {
				t.Fatalf("Should not contain User-Agent: %s, Got: %s", expectedNotUsedUserAgent, request.Header.Get("User-Agent"))
			}

			handler.ServeHTTP(responseWriter, request)
		})

		s := httptest.NewServer(registryThatAssertsUserAgentIsCorrect)

		defer s.Close()
		u, err := url.Parse(s.URL)
		if err != nil {
			t.Fatal(err)
		}

		tag1 := mustNewTag(t, u.Host+"/repo:tag1")
		// using a transport.Wrapper, meaning retry logic should not be wrapped
		transportWrapper, err := transport.NewWithContext(context.Background(), tag1.Repository.Registry, authn.Anonymous, http.DefaultTransport, nil)
		if err != nil {
			t.Fatal(err)
		}

		if err := MultiWrite(map[name.Reference]Taggable{
			tag1: img1,
		}, WithTransport(transportWrapper), WithUserAgent(expectedNotUsedUserAgent)); err != nil {
			t.Fatal(err)
		}
	})
}

// TestMultiWrite_Deep tests that a deeply nested tree of manifest lists gets
// pushed in the correct order (i.e., each level in sequence).
func TestMultiWrite_Deep(t *testing.T) {
	idx, err := random.Index(1024, 2, 2)
	if err != nil {
		t.Fatal("random.Image:", err)
	}
	for i := 0; i < 4; i++ {
		idx = mutate.AppendManifests(idx, mutate.IndexAddendum{Add: idx})
	}

	// Set up a fake registry (with NOP logger to avoid spamming test logs).
	nopLog := log.New(io.Discard, "", 0)
	s := httptest.NewServer(registry.New(registry.Logger(nopLog)))
	defer s.Close()
	u, err := url.Parse(s.URL)
	if err != nil {
		t.Fatal(err)
	}

	// Write both images and the manifest list.
	tag := mustNewTag(t, u.Host+"/repo:tag")
	if err := MultiWrite(map[name.Reference]Taggable{
		tag: idx,
	}); err != nil {
		t.Error("Write:", err)
	}

	// Check that tagged manfest list is present and valid.
	got, err := Index(tag)
	if err != nil {
		t.Fatal(err)
	}
	if err := validate.Index(got); err != nil {
		t.Error("Validate() =", err)
	}
}

type countTransport struct {
	count int
	inner http.RoundTripper
}

func (t *countTransport) RoundTrip(req *http.Request) (*http.Response, error) {
	if strings.HasSuffix(req.URL.Path, "/v2/") {
		return t.inner.RoundTrip(req)
	}

	t.count++
	return nil, io.ErrUnexpectedEOF
}
