// 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 remote

import (
	"bytes"
	"context"
	"fmt"
	"net/http"
	"net/http/httptest"
	"net/url"
	"testing"

	"github.com/google/go-cmp/cmp"
	v1 "github.com/google/go-containerregistry/pkg/v1"
	"github.com/google/go-containerregistry/pkg/v1/random"
	"github.com/google/go-containerregistry/pkg/v1/types"
)

func randomIndex(t *testing.T) v1.ImageIndex {
	rnd, err := random.Index(1024, 1, 3)
	if err != nil {
		t.Fatalf("random.Index() = %v", err)
	}
	return rnd
}

func mustIndexManifest(t *testing.T, idx v1.ImageIndex) *v1.IndexManifest {
	m, err := idx.IndexManifest()
	if err != nil {
		t.Fatalf("IndexManifest() = %v", err)
	}
	return m
}

func mustChild(t *testing.T, idx v1.ImageIndex, h v1.Hash) v1.Image {
	img, err := idx.Image(h)
	if err != nil {
		t.Fatalf("Image(%s) = %v", h, err)
	}
	return img
}

func mustMediaType(t *testing.T, tag withMediaType) types.MediaType {
	mt, err := tag.MediaType()
	if err != nil {
		t.Fatalf("MediaType() = %v", err)
	}
	return mt
}

func mustHash(t *testing.T, s string) v1.Hash {
	h, err := v1.NewHash(s)
	if err != nil {
		t.Fatalf("NewHash() = %v", err)
	}
	return h
}

func TestIndexRawManifestDigests(t *testing.T) {
	idx := randomIndex(t)
	expectedRepo := "foo/bar"

	cases := []struct {
		name          string
		ref           string
		responseBody  []byte
		contentDigest string
		wantErr       bool
	}{{
		name:          "normal pull, by tag",
		ref:           "latest",
		responseBody:  mustRawManifest(t, idx),
		contentDigest: mustDigest(t, idx).String(),
		wantErr:       false,
	}, {
		name:          "normal pull, by digest",
		ref:           mustDigest(t, idx).String(),
		responseBody:  mustRawManifest(t, idx),
		contentDigest: mustDigest(t, idx).String(),
		wantErr:       false,
	}, {
		name:          "right content-digest, wrong body, by digest",
		ref:           mustDigest(t, idx).String(),
		responseBody:  []byte("not even json"),
		contentDigest: mustDigest(t, idx).String(),
		wantErr:       true,
	}, {
		name:          "right body, wrong content-digest, by tag",
		ref:           "latest",
		responseBody:  mustRawManifest(t, idx),
		contentDigest: bogusDigest,
		wantErr:       false,
	}, {
		// NB: This succeeds! We don't care what the registry thinks.
		name:          "right body, wrong content-digest, by digest",
		ref:           mustDigest(t, idx).String(),
		responseBody:  mustRawManifest(t, idx),
		contentDigest: bogusDigest,
		wantErr:       false,
	}}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			manifestPath := fmt.Sprintf("/v2/%s/manifests/%s", expectedRepo, tc.ref)
			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				switch r.URL.Path {
				case manifestPath:
					if r.Method != http.MethodGet {
						t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet)
					}

					w.Header().Set("Docker-Content-Digest", tc.contentDigest)
					w.Write(tc.responseBody)
				default:
					t.Fatalf("Unexpected path: %v", r.URL.Path)
				}
			}))
			defer server.Close()
			u, err := url.Parse(server.URL)
			if err != nil {
				t.Fatalf("url.Parse(%v) = %v", server.URL, err)
			}

			ref, err := newReference(u.Host, expectedRepo, tc.ref)
			if err != nil {
				t.Fatalf("url.Parse(%v, %v, %v) = %v", u.Host, expectedRepo, tc.ref, err)
			}

			rmt := remoteIndex{
				ref: ref,
				ctx: context.Background(),
				fetcher: fetcher{
					target: ref.Context(),
					client: http.DefaultClient,
				},
			}

			if _, err := rmt.RawManifest(); (err != nil) != tc.wantErr {
				t.Errorf("RawManifest() wrong error: %v, want %v: %v\n", (err != nil), tc.wantErr, err)
			}
		})
	}
}

func TestIndex(t *testing.T) {
	idx := randomIndex(t)
	expectedRepo := "foo/bar"
	manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo)
	childDigest := mustIndexManifest(t, idx).Manifests[0].Digest
	child := mustChild(t, idx, childDigest)
	childPath := fmt.Sprintf("/v2/%s/manifests/%s", expectedRepo, childDigest)
	configPath := fmt.Sprintf("/v2/%s/blobs/%s", expectedRepo, mustConfigName(t, child))
	manifestReqCount := 0
	childReqCount := 0

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/v2/":
			w.WriteHeader(http.StatusOK)
		case manifestPath:
			manifestReqCount++
			if r.Method != http.MethodGet {
				t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet)
			}
			w.Header().Set("Content-Type", string(mustMediaType(t, idx)))
			w.Write(mustRawManifest(t, idx))
		case childPath:
			childReqCount++
			if r.Method != http.MethodGet {
				t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet)
			}
			w.Write(mustRawManifest(t, child))
		case configPath:
			if r.Method != http.MethodGet {
				t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet)
			}
			w.Write(mustRawConfigFile(t, child))
		default:
			t.Fatalf("Unexpected path: %v", r.URL.Path)
		}
	}))
	defer server.Close()
	u, err := url.Parse(server.URL)
	if err != nil {
		t.Fatalf("url.Parse(%v) = %v", server.URL, err)
	}

	tag := mustNewTag(t, fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo))
	rmt, err := Index(tag, WithTransport(http.DefaultTransport))
	if err != nil {
		t.Errorf("Index() = %v", err)
	}
	rmtChild, err := rmt.Image(childDigest)
	if err != nil {
		t.Errorf("remoteIndex.Image(%s) = %v", childDigest, err)
	}

	// Test that index works as expected.
	if got, want := mustRawManifest(t, rmt), mustRawManifest(t, idx); !bytes.Equal(got, want) {
		t.Errorf("RawManifest() = %v, want %v", got, want)
	}
	if diff := cmp.Diff(mustIndexManifest(t, idx), mustIndexManifest(t, rmt)); diff != "" {
		t.Errorf("IndexManifest() (-want +got) = %v", diff)
	}
	if got, want := mustMediaType(t, rmt), mustMediaType(t, idx); got != want {
		t.Errorf("MediaType() = %v, want %v", got, want)
	}
	if got, want := mustDigest(t, rmt), mustDigest(t, idx); got != want {
		t.Errorf("Digest() = %v, want %v", got, want)
	}
	// Make sure caching the manifest works for index.
	if manifestReqCount != 1 {
		t.Errorf("RawManifest made %v requests, expected 1", manifestReqCount)
	}

	// Test that child works as expected.
	if got, want := mustRawManifest(t, rmtChild), mustRawManifest(t, child); !bytes.Equal(got, want) {
		t.Errorf("RawManifest() = %v, want %v", got, want)
	}
	if got, want := mustRawConfigFile(t, rmtChild), mustRawConfigFile(t, child); !bytes.Equal(got, want) {
		t.Errorf("RawConfigFile() = %v, want %v", got, want)
	}
	// Make sure caching the manifest works for child.
	if childReqCount != 1 {
		t.Errorf("RawManifest made %v requests, expected 1", childReqCount)
	}

	// Try to fetch bogus children.
	bogusHash := mustHash(t, bogusDigest)

	if _, err := rmt.Image(bogusHash); err == nil {
		t.Errorf("remoteIndex.Image(bogusDigest) err = %v, wanted err", err)
	}
	if _, err := rmt.ImageIndex(bogusHash); err == nil {
		t.Errorf("remoteIndex.ImageIndex(bogusDigest) err = %v, wanted err", err)
	}
}

// TestMatchesPlatform runs test cases on the matchesPlatform function which verifies
// whether the given platform can run on the required platform by checking the
// compatibility of architecture, OS, OS version, OS features, variant and features.
func TestMatchesPlatform(t *testing.T) {
	t.Parallel()
	tests := []struct {
		// want is the expected return value from matchesPlatform
		// when the given platform is 'given' and the required platform is 'required'.
		given    v1.Platform
		required v1.Platform
		want     bool
	}{{ // The given & required platforms are identical. matchesPlatform expected to return true.
		given: v1.Platform{
			Architecture: "amd64",
			OS:           "linux",
			OSVersion:    "10.0.10586",
			OSFeatures:   []string{"win32k"},
			Variant:      "armv6l",
			Features:     []string{"sse4"},
		},
		required: v1.Platform{
			Architecture: "amd64",
			OS:           "linux",
			OSVersion:    "10.0.10586",
			OSFeatures:   []string{"win32k"},
			Variant:      "armv6l",
			Features:     []string{"sse4"},
		},
		want: true,
	},
		{ // OS and Architecture must exactly match. matchesPlatform expected to return false.
			given: v1.Platform{
				Architecture: "arm",
				OS:           "linux",
				OSVersion:    "10.0.10586",
				OSFeatures:   []string{"win64k"},
				Variant:      "armv6l",
				Features:     []string{"sse4"},
			},
			required: v1.Platform{
				Architecture: "amd64",
				OS:           "linux",
				OSVersion:    "10.0.10586",
				OSFeatures:   []string{"win32k"},
				Variant:      "armv6l",
				Features:     []string{"sse4"},
			},
			want: false,
		},
		{ // OS version must exactly match
			given: v1.Platform{
				Architecture: "amd64",
				OS:           "linux",
				OSVersion:    "10.0.10586",
				OSFeatures:   []string{"win64k"},
				Variant:      "armv6l",
				Features:     []string{"sse4"},
			},
			required: v1.Platform{
				Architecture: "amd64",
				OS:           "linux",
				OSVersion:    "10.0.10587",
				OSFeatures:   []string{"win64k"},
				Variant:      "armv6l",
				Features:     []string{"sse4"},
			},
			want: false,
		},
		{ // OS Features must exactly match. matchesPlatform expected to return false.
			given: v1.Platform{
				Architecture: "arm",
				OS:           "linux",
				OSVersion:    "10.0.10586",
				OSFeatures:   []string{"win64k"},
				Variant:      "armv6l",
				Features:     []string{"sse4"},
			},
			required: v1.Platform{
				Architecture: "arm",
				OS:           "linux",
				OSVersion:    "10.0.10586",
				OSFeatures:   []string{"win32k"},
				Variant:      "armv6l",
				Features:     []string{"sse4"},
			},
			want: false,
		},
		{ // Variant must exactly match. matchesPlatform expected to return false.
			given: v1.Platform{
				Architecture: "amd64",
				OS:           "linux",
				OSVersion:    "10.0.10586",
				OSFeatures:   []string{"win64k"},
				Variant:      "armv6l",
				Features:     []string{"sse4"},
			},
			required: v1.Platform{
				Architecture: "amd64",
				OS:           "linux",
				OSVersion:    "10.0.10586",
				OSFeatures:   []string{"win64k"},
				Variant:      "armv7l",
				Features:     []string{"sse4"},
			},
			want: false,
		},
		{ // OS must exactly match, and is case sensative. matchesPlatform expected to return false.
			given: v1.Platform{
				Architecture: "arm",
				OS:           "linux",
				OSVersion:    "10.0.10586",
				OSFeatures:   []string{"win64k"},
				Variant:      "armv6l",
				Features:     []string{"sse4"},
			},
			required: v1.Platform{
				Architecture: "arm",
				OS:           "LinuX",
				OSVersion:    "10.0.10586",
				OSFeatures:   []string{"win64k"},
				Variant:      "armv6l",
				Features:     []string{"sse4"},
			},
			want: false,
		},
		{ // OSVersion and Variant are specified in given but not in required.
			// matchesPlatform expected to return true.
			given: v1.Platform{
				Architecture: "arm",
				OS:           "linux",
				OSVersion:    "10.0.10586",
				OSFeatures:   []string{"win64k"},
				Variant:      "armv6l",
				Features:     []string{"sse4"},
			},
			required: v1.Platform{
				Architecture: "arm",
				OS:           "linux",
				OSVersion:    "",
				OSFeatures:   []string{"win64k"},
				Variant:      "",
				Features:     []string{"sse4"},
			},
			want: true,
		},
		{ // Ensure the optional field OSVersion & Variant match exactly if specified as required.
			given: v1.Platform{
				Architecture: "amd64",
				OS:           "linux",
				OSVersion:    "",
				OSFeatures:   []string{},
				Variant:      "",
				Features:     []string{},
			},
			required: v1.Platform{
				Architecture: "amd64",
				OS:           "linux",
				OSVersion:    "10.0.10586",
				OSFeatures:   []string{"win32k"},
				Variant:      "armv6l",
				Features:     []string{"sse4"},
			},
			want: false,
		},
		{ // Checking subset validity when required less features than given features.
			// matchesPlatform expected to return true.
			given: v1.Platform{
				Architecture: "",
				OS:           "linux",
				OSVersion:    "10.0.10586",
				OSFeatures:   []string{"win32k"},
				Variant:      "armv6l",
				Features:     []string{"sse4"},
			},
			required: v1.Platform{
				Architecture: "",
				OS:           "linux",
				OSVersion:    "",
				OSFeatures:   []string{},
				Variant:      "",
				Features:     []string{},
			},
			want: true,
		},
		{ // Checking subset validity when required features are subset of given features.
			// matchesPlatform expected to return true.
			given: v1.Platform{
				Architecture: "arm",
				OS:           "linux",
				OSVersion:    "10.0.10586",
				OSFeatures:   []string{"win64k", "f1", "f2"},
				Variant:      "",
				Features:     []string{"sse4", "f1"},
			},
			required: v1.Platform{
				Architecture: "arm",
				OS:           "linux",
				OSVersion:    "10.0.10586",
				OSFeatures:   []string{"win64k"},
				Variant:      "",
				Features:     []string{"sse4"},
			},
			want: true,
		},
		{ // Checking subset validity when some required features is not subset of given features.
			// matchesPlatform expected to return false.
			given: v1.Platform{
				Architecture: "arm",
				OS:           "linux",
				OSVersion:    "10.0.10586",
				OSFeatures:   []string{"win64k", "f1", "f2"},
				Variant:      "",
				Features:     []string{"sse4", "f1"},
			},
			required: v1.Platform{
				Architecture: "arm",
				OS:           "linux",
				OSVersion:    "10.0.10586",
				OSFeatures:   []string{"win64k"},
				Variant:      "",
				Features:     []string{"sse4", "f2"},
			},
			want: false,
		},
		{ // Checking subset validity when OS features not required,
			// and required features is indeed a subset of given features.
			// matchesPlatform expected to return true.
			given: v1.Platform{
				Architecture: "arm",
				OS:           "linux",
				OSVersion:    "10.0.10586",
				OSFeatures:   []string{"win64k", "f1", "f2"},
				Variant:      "armv6l",
				Features:     []string{"sse4"},
			},
			required: v1.Platform{
				Architecture: "arm",
				OS:           "linux",
				OSVersion:    "10.0.10586",
				OSFeatures:   []string{},
				Variant:      "armv6l",
				Features:     []string{"sse4"},
			},
			want: true,
		},
	}

	for _, test := range tests {
		got := matchesPlatform(test.given, test.required)
		if got != test.want {
			t.Errorf("matchesPlatform(%v, %v); got %v, want %v", test.given, test.required, got, test.want)
		}
	}
}
