// Copyright 2018 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 gcrane

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"
	"path"
	"strings"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-containerregistry/internal/retry"
	"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/google"
	"github.com/google/go-containerregistry/pkg/v1/mutate"
	"github.com/google/go-containerregistry/pkg/v1/partial"
	"github.com/google/go-containerregistry/pkg/v1/random"
	"github.com/google/go-containerregistry/pkg/v1/remote"
	"github.com/google/go-containerregistry/pkg/v1/remote/transport"
	"github.com/google/go-containerregistry/pkg/v1/types"
)

type fakeXCR struct {
	h     http.Handler
	repos map[string]google.Tags
	t     *testing.T
}

func (xcr *fakeXCR) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	xcr.t.Logf("%s %s", r.Method, r.URL)
	if strings.HasPrefix(r.URL.Path, "/v2/") && strings.HasSuffix(r.URL.Path, "/tags/list") {
		repo := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/v2/"), "/tags/list")
		if tags, ok := xcr.repos[repo]; !ok {
			w.WriteHeader(http.StatusNotFound)
		} else {
			xcr.t.Logf("%+v", tags)
			if err := json.NewEncoder(w).Encode(tags); err != nil {
				xcr.t.Fatal(err)
			}
		}
	} else {
		xcr.h.ServeHTTP(w, r)
	}
}

func newFakeXCR(t *testing.T) *fakeXCR {
	h := registry.New()
	return &fakeXCR{h: h, t: t}
}

func (xcr *fakeXCR) setRefs(stuff map[name.Reference]partial.Describable) error {
	repos := make(map[string]google.Tags)

	for ref, thing := range stuff {
		repo := ref.Context().RepositoryStr()
		tags, ok := repos[repo]
		if !ok {
			tags = google.Tags{
				Name:     repo,
				Children: []string{},
			}
		}

		// Populate the "child" field.
		for parentPath := repo; parentPath != "."; parentPath = path.Dir(parentPath) {
			child, parent := path.Base(parentPath), path.Dir(parentPath)
			tags, ok := repos[parent]
			if !ok {
				tags = google.Tags{}
			}
			for _, c := range repos[parent].Children {
				if c == child {
					break
				}
			}
			tags.Children = append(tags.Children, child)
			repos[parent] = tags
		}

		// Populate the "manifests" and "tags" field.
		d, err := thing.Digest()
		if err != nil {
			return err
		}
		mt, err := thing.MediaType()
		if err != nil {
			return err
		}
		if tags.Manifests == nil {
			tags.Manifests = make(map[string]google.ManifestInfo)
		}
		mi, ok := tags.Manifests[d.String()]
		if !ok {
			mi = google.ManifestInfo{
				MediaType: string(mt),
				Tags:      []string{},
			}
		}
		if tag, ok := ref.(name.Tag); ok {
			tags.Tags = append(tags.Tags, tag.Identifier())
			mi.Tags = append(mi.Tags, tag.Identifier())
		}
		tags.Manifests[d.String()] = mi
		repos[repo] = tags
	}
	xcr.repos = repos
	return nil
}

func TestCopy(t *testing.T) {
	logs.Warn.SetOutput(os.Stderr)
	xcr := newFakeXCR(t)
	s := httptest.NewServer(xcr)
	u, err := url.Parse(s.URL)
	if err != nil {
		t.Fatal(err)
	}
	defer s.Close()
	src := path.Join(u.Host, "test/gcrane")
	dst := path.Join(u.Host, "test/gcrane/copy")

	oneTag, err := random.Image(1024, 5)
	if err != nil {
		t.Fatal(err)
	}
	twoTags, err := random.Image(1024, 5)
	if err != nil {
		t.Fatal(err)
	}
	noTags, err := random.Image(1024, 3)
	if err != nil {
		t.Fatal(err)
	}

	latestRef, err := name.ParseReference(src)
	if err != nil {
		t.Fatal(err)
	}
	oneTagRef := latestRef.Context().Tag("bar")

	d, err := noTags.Digest()
	if err != nil {
		t.Fatal(err)
	}
	noTagsRef := latestRef.Context().Digest(d.String())
	fooRef := latestRef.Context().Tag("foo")

	// Populate this after we create it so we know the hostname.
	if err := xcr.setRefs(map[name.Reference]partial.Describable{
		oneTagRef: oneTag,
		latestRef: twoTags,
		fooRef:    twoTags,
		noTagsRef: noTags,
	}); err != nil {
		t.Fatal(err)
	}

	if err := remote.Write(latestRef, twoTags); err != nil {
		t.Fatal(err)
	}
	if err := remote.Write(fooRef, twoTags); err != nil {
		t.Fatal(err)
	}
	if err := remote.Write(oneTagRef, oneTag); err != nil {
		t.Fatal(err)
	}
	if err := remote.Write(noTagsRef, noTags); err != nil {
		t.Fatal(err)
	}

	if err := Copy(src, dst); err != nil {
		t.Fatal(err)
	}

	if err := CopyRepository(context.Background(), src, dst); err != nil {
		t.Fatal(err)
	}
}

// TestCopyWithPlatform verifies that Copy with WithPlatform resolves the
// multi-platform source index to the single requested manifest at the dst.
// Regression test for #2059.
func TestCopyWithPlatform(t *testing.T) {
	s := httptest.NewServer(registry.New())
	defer s.Close()
	u, err := url.Parse(s.URL)
	if err != nil {
		t.Fatal(err)
	}
	src := path.Join(u.Host, "test/gcrane") + ":multi"
	dst := path.Join(u.Host, "test/gcrane/copy") + ":amd64"

	amdImg, err := random.Image(1024, 2)
	if err != nil {
		t.Fatal(err)
	}
	armImg, err := random.Image(1024, 2)
	if err != nil {
		t.Fatal(err)
	}

	idx := mutate.AppendManifests(empty.Index,
		mutate.IndexAddendum{
			Add: amdImg,
			Descriptor: v1.Descriptor{
				Platform: &v1.Platform{OS: "linux", Architecture: "amd64"},
			},
		},
		mutate.IndexAddendum{
			Add: armImg,
			Descriptor: v1.Descriptor{
				Platform: &v1.Platform{OS: "linux", Architecture: "arm64"},
			},
		},
	)

	srcRef, err := name.ParseReference(src)
	if err != nil {
		t.Fatal(err)
	}
	if err := remote.WriteIndex(srcRef, idx); err != nil {
		t.Fatal(err)
	}

	if err := Copy(src, dst, WithPlatform(&v1.Platform{OS: "linux", Architecture: "amd64"})); err != nil {
		t.Fatal(err)
	}

	dstRef, err := name.ParseReference(dst)
	if err != nil {
		t.Fatal(err)
	}
	desc, err := remote.Get(dstRef)
	if err != nil {
		t.Fatal(err)
	}
	if desc.MediaType.IsIndex() {
		t.Fatalf("dst is an index (%s); expected a single-platform manifest", desc.MediaType)
	}
	amdDigest, err := amdImg.Digest()
	if err != nil {
		t.Fatal(err)
	}
	if desc.Digest != amdDigest {
		t.Errorf("dst digest = %s, want %s (amd64 manifest)", desc.Digest, amdDigest)
	}
}

func TestRename(t *testing.T) {
	c := copier{
		srcRepo: name.MustParseReference("registry.example.com/foo").Context(),
		dstRepo: name.MustParseReference("registry.example.com/bar").Context(),
	}

	got, err := c.rename(name.MustParseReference("registry.example.com/foo/sub/repo").Context())
	if err != nil {
		t.Fatalf("unexpected err: %v", err)
	}
	want := name.MustParseReference("registry.example.com/bar/sub/repo").Context()

	if want.String() != got.String() {
		t.Errorf("%s != %s", want, got)
	}
}

func TestSubtractStringLists(t *testing.T) {
	cases := []struct {
		minuend    []string
		subtrahend []string
		result     []string
	}{{
		minuend:    []string{"a", "b", "c"},
		subtrahend: []string{"a"},
		result:     []string{"b", "c"},
	}, {
		minuend:    []string{"a", "a", "a"},
		subtrahend: []string{"a", "b"},
		result:     []string{},
	}, {
		minuend:    []string{},
		subtrahend: []string{"a", "b"},
		result:     []string{},
	}, {
		minuend:    []string{"a", "b"},
		subtrahend: []string{},
		result:     []string{"a", "b"},
	}}

	for _, tc := range cases {
		want, got := tc.result, subtractStringLists(tc.minuend, tc.subtrahend)
		if diff := cmp.Diff(want, got); diff != "" {
			t.Errorf("subtracting string lists: %v - %v: (-want +got)\n%s", tc.minuend, tc.subtrahend, diff)
		}
	}
}

func TestDiffImages(t *testing.T) {
	cases := []struct {
		want map[string]google.ManifestInfo
		have map[string]google.ManifestInfo
		need map[string]google.ManifestInfo
	}{{
		// Have everything we need.
		want: map[string]google.ManifestInfo{
			"a": {
				Tags: []string{"b", "c"},
			},
		},
		have: map[string]google.ManifestInfo{
			"a": {
				Tags: []string{"b", "c"},
			},
		},
		need: map[string]google.ManifestInfo{},
	}, {
		// Missing image a.
		want: map[string]google.ManifestInfo{
			"a": {
				Tags: []string{"b", "c", "d"},
			},
		},
		have: map[string]google.ManifestInfo{},
		need: map[string]google.ManifestInfo{
			"a": {
				Tags: []string{"b", "c", "d"},
			},
		},
	}, {
		// Missing tags "b" and "d"
		want: map[string]google.ManifestInfo{
			"a": {
				Tags: []string{"b", "c", "d"},
			},
		},
		have: map[string]google.ManifestInfo{
			"a": {
				Tags: []string{"c"},
			},
		},
		need: map[string]google.ManifestInfo{
			"a": {
				Tags: []string{"b", "d"},
			},
		},
	}, {
		// Make sure all properties get copied over.
		want: map[string]google.ManifestInfo{
			"a": {
				Size:      123,
				MediaType: string(types.DockerManifestSchema2),
				Created:   time.Date(1992, time.January, 7, 6, 40, 00, 5e8, time.UTC),
				Uploaded:  time.Date(2018, time.November, 29, 4, 13, 30, 5e8, time.UTC),
				Tags:      []string{"b", "c", "d"},
			},
		},
		have: map[string]google.ManifestInfo{},
		need: map[string]google.ManifestInfo{
			"a": {
				Size:      123,
				MediaType: string(types.DockerManifestSchema2),
				Created:   time.Date(1992, time.January, 7, 6, 40, 00, 5e8, time.UTC),
				Uploaded:  time.Date(2018, time.November, 29, 4, 13, 30, 5e8, time.UTC),
				Tags:      []string{"b", "c", "d"},
			},
		},
	}}

	for _, tc := range cases {
		want, got := tc.need, diffImages(tc.want, tc.have)
		if diff := cmp.Diff(want, got); diff != "" {
			t.Errorf("diffing images: %v - %v: (-want +got)\n%s", tc.want, tc.have, diff)
		}
	}
}

// Test that our backoff works the way we expect.
func TestBackoff(t *testing.T) {
	backoff := GCRBackoff()

	if d := backoff.Step(); d > 10*time.Second {
		t.Errorf("Duration too long: %v", d)
	}
	if d := backoff.Step(); d > 100*time.Second {
		t.Errorf("Duration too long: %v", d)
	}
	if d := backoff.Step(); d > 1000*time.Second {
		t.Errorf("Duration too long: %v", d)
	}
	if s := backoff.Steps; s != 0 {
		t.Errorf("backoff.Steps should be 0, got %d", s)
	}
}

func TestErrors(t *testing.T) {
	if hasStatusCode(nil, http.StatusOK) {
		t.Fatal("nil error should not have any status code")
	}
	if !hasStatusCode(&transport.Error{StatusCode: http.StatusOK}, http.StatusOK) {
		t.Fatal("200 should be 200")
	}
	if hasStatusCode(&transport.Error{StatusCode: http.StatusOK}, http.StatusNotFound) {
		t.Fatal("200 should not be 404")
	}

	if isServerError(nil) {
		t.Fatal("nil should not be server error")
	}
	if isServerError(fmt.Errorf("i am a string")) {
		t.Fatal("string should not be server error")
	}
	if !isServerError(&transport.Error{StatusCode: http.StatusServiceUnavailable}) {
		t.Fatal("503 should be server error")
	}
	if isServerError(&transport.Error{StatusCode: http.StatusTooManyRequests}) {
		t.Fatal("429 should not be server error")
	}
}

func TestRetryErrors(t *testing.T) {
	// We log a warning during retries, so we can tell if something retried by checking logs.Warn.
	var b bytes.Buffer
	logs.Warn.SetOutput(&b)

	err := backoffErrors(retry.Backoff{
		Duration: 1 * time.Millisecond,
		Steps:    3,
	}, func() error {
		return &transport.Error{StatusCode: http.StatusTooManyRequests}
	})

	if err == nil {
		t.Fatal("backoffErrors should return internal err, got nil")
	}
	var terr *transport.Error
	if !errors.As(err, &terr) {
		t.Fatalf("backoffErrors should return internal err, got different error: %v", err)
	} else if terr.StatusCode != http.StatusTooManyRequests {
		t.Fatalf("backoffErrors should return internal err, got different status code: %v", terr.StatusCode)
	}

	if b.Len() == 0 {
		t.Fatal("backoffErrors didn't log to logs.Warn")
	}
}

func TestBadInputs(t *testing.T) {
	t.Parallel()
	invalid := "@@@@@@"

	// Create a valid image reference that will fail with not found.
	s := httptest.NewServer(http.NotFoundHandler())
	u, err := url.Parse(s.URL)
	if err != nil {
		t.Fatal(err)
	}
	valid404 := fmt.Sprintf("%s/some/image", u.Host)

	ctx := context.Background()

	for _, tc := range []struct {
		desc string
		err  error
	}{
		{"Copy(invalid, invalid)", Copy(invalid, invalid)},
		{"Copy(404, invalid)", Copy(valid404, invalid)},
		{"Copy(404, 404)", Copy(valid404, valid404)},
		{"CopyRepository(invalid, invalid)", CopyRepository(ctx, invalid, invalid)},
		{"CopyRepository(404, invalid)", CopyRepository(ctx, valid404, invalid)},
		{"CopyRepository(404, 404)", CopyRepository(ctx, valid404, valid404, WithJobs(1))},
	} {
		if tc.err == nil {
			t.Errorf("%s: expected err, got nil", tc.desc)
		}
	}
}
