// Copyright 2019 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 tarball

import (
	"archive/tar"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/google/go-containerregistry/pkg/name"
	v1 "github.com/google/go-containerregistry/pkg/v1"
	"github.com/google/go-containerregistry/pkg/v1/compare"
	"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/tarball"
	"github.com/google/go-containerregistry/pkg/v1/types"
	"github.com/google/go-containerregistry/pkg/v1/validate"
)

func TestWrite(t *testing.T) {
	// Make a tempfile for tarball writes.
	fp, err := os.CreateTemp("", "")
	if err != nil {
		t.Fatalf("Error creating temp file.")
	}
	t.Log(fp.Name())
	defer fp.Close()
	defer os.Remove(fp.Name())

	// Make a random image
	randImage, err := random.Image(256, 8)
	if err != nil {
		t.Fatalf("Error creating random image: %v", err)
	}
	tag, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation)
	if err != nil {
		t.Fatalf("Error creating test tag: %v", err)
	}
	o, err := os.Create(fp.Name())
	if err != nil {
		t.Fatalf("Error creating %q to write image tarball: %v", fp.Name(), err)
	}
	defer o.Close()
	if err := Write(tag, randImage, o); err != nil {
		t.Fatalf("Unexpected error writing tarball: %v", err)
	}

	// Make sure the image is valid and can be loaded.
	// Load it both by nil and by its name.
	for _, it := range []*name.Tag{nil, &tag} {
		tarImage, err := tarball.ImageFromPath(fp.Name(), it)
		if err != nil {
			t.Fatalf("Unexpected error reading tarball: %v", err)
		}
		if err := validate.Image(tarImage); err != nil {
			t.Errorf("validate.Image: %v", err)
		}
		if err := compare.Images(randImage, tarImage); err != nil {
			t.Errorf("compare.Images: %v", err)
		}
	}

	// Try loading a different tag, it should error.
	fakeTag, err := name.NewTag("gcr.io/notthistag:latest", name.StrictValidation)
	if err != nil {
		t.Fatalf("Error generating tag: %v", err)
	}
	if _, err := tarball.ImageFromPath(fp.Name(), &fakeTag); err == nil {
		t.Errorf("Expected error loading tag %v from image", fakeTag)
	}
}

func TestMultiWriteSameImage(t *testing.T) {
	// Make a tempfile for tarball writes.
	fp, err := os.CreateTemp("", "")
	if err != nil {
		t.Fatalf("Error creating temp file.")
	}
	t.Log(fp.Name())
	defer fp.Close()
	defer os.Remove(fp.Name())

	// Make a random image
	randImage, err := random.Image(256, 8)
	if err != nil {
		t.Fatalf("Error creating random image.")
	}

	// Make two tags that point to the random image above.
	tag1, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation)
	if err != nil {
		t.Fatalf("Error creating test tag1.")
	}
	tag2, err := name.NewTag("gcr.io/baz/bat:latest", name.StrictValidation)
	if err != nil {
		t.Fatalf("Error creating test tag2.")
	}
	dig3, err := name.NewDigest("gcr.io/baz/baz@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", name.StrictValidation)
	if err != nil {
		t.Fatalf("Error creating test dig3.")
	}
	refToImage := make(map[name.Reference]v1.Image)
	refToImage[tag1] = randImage
	refToImage[tag2] = randImage
	refToImage[dig3] = randImage

	o, err := os.Create(fp.Name())
	if err != nil {
		t.Fatalf("Error creating %q to write image tarball: %v", fp.Name(), err)
	}
	defer o.Close()

	// Write the images with both tags to the tarball
	if err := MultiWrite(refToImage, o); err != nil {
		t.Fatalf("Unexpected error writing tarball: %v", err)
	}
	for ref := range refToImage {
		tag, ok := ref.(name.Tag)
		if !ok {
			continue
		}

		tarImage, err := tarball.ImageFromPath(fp.Name(), &tag)
		if err != nil {
			t.Fatalf("Unexpected error reading tarball: %v", err)
		}
		if err := validate.Image(tarImage); err != nil {
			t.Errorf("validate.Image: %v", err)
		}
		if err := compare.Images(randImage, tarImage); err != nil {
			t.Errorf("compare.Images: %v", err)
		}
	}
}

func TestMultiWriteDifferentImages(t *testing.T) {
	// Make a tempfile for tarball writes.
	fp, err := os.CreateTemp("", "")
	if err != nil {
		t.Fatalf("Error creating temp file: %v", err)
	}
	t.Log(fp.Name())
	defer fp.Close()
	defer os.Remove(fp.Name())

	// Make a random image
	randImage1, err := random.Image(256, 8)
	if err != nil {
		t.Fatalf("Error creating random image 1: %v", err)
	}

	// Make another random image
	randImage2, err := random.Image(256, 8)
	if err != nil {
		t.Fatalf("Error creating random image 2: %v", err)
	}

	// Make another random image
	randImage3, err := random.Image(256, 8)
	if err != nil {
		t.Fatalf("Error creating random image 3: %v", err)
	}

	// Create two tags, one pointing to each image created.
	tag1, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation)
	if err != nil {
		t.Fatalf("Error creating test tag1: %v", err)
	}
	tag2, err := name.NewTag("gcr.io/baz/bat:latest", name.StrictValidation)
	if err != nil {
		t.Fatalf("Error creating test tag2: %v", err)
	}
	dig3, err := name.NewDigest("gcr.io/baz/baz@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", name.StrictValidation)
	if err != nil {
		t.Fatalf("Error creating test dig3: %v", err)
	}
	refToImage := make(map[name.Reference]v1.Image)
	refToImage[tag1] = randImage1
	refToImage[tag2] = randImage2
	refToImage[dig3] = randImage3

	o, err := os.Create(fp.Name())
	if err != nil {
		t.Fatalf("Error creating %q to write image tarball: %v", fp.Name(), err)
	}
	defer o.Close()

	// Write both images to the tarball.
	if err := MultiWrite(refToImage, o); err != nil {
		t.Fatalf("Unexpected error writing tarball: %v", err)
	}
	for ref, img := range refToImage {
		tag, ok := ref.(name.Tag)
		if !ok {
			continue
		}

		tarImage, err := tarball.ImageFromPath(fp.Name(), &tag)
		if err != nil {
			t.Fatalf("Unexpected error reading tarball: %v", err)
		}
		if err := validate.Image(tarImage); err != nil {
			t.Errorf("validate.Image: %v", err)
		}
		if err := compare.Images(img, tarImage); err != nil {
			t.Errorf("compare.Images: %v", err)
		}
	}
}

func TestWriteForeignLayers(t *testing.T) {
	// Make a tempfile for tarball writes.
	fp, err := os.CreateTemp("", "")
	if err != nil {
		t.Fatalf("Error creating temp file: %v", err)
	}
	t.Log(fp.Name())
	defer fp.Close()
	defer os.Remove(fp.Name())

	// Make a random image
	randImage, err := random.Image(256, 1)
	if err != nil {
		t.Fatalf("Error creating random image: %v", err)
	}
	tag, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation)
	if err != nil {
		t.Fatalf("Error creating test tag: %v", err)
	}
	randLayer, err := random.Layer(512, types.DockerForeignLayer)
	if err != nil {
		t.Fatalf("random.Layer: %v", err)
	}
	img, err := mutate.Append(randImage, mutate.Addendum{
		Layer: randLayer,
		URLs: []string{
			"example.com",
		},
	})
	if err != nil {
		t.Fatalf("Unable to mutate image to add foreign layer: %v", err)
	}
	o, err := os.Create(fp.Name())
	if err != nil {
		t.Fatalf("Error creating %q to write image tarball: %v", fp.Name(), err)
	}
	defer o.Close()
	if err := Write(tag, img, o); err != nil {
		t.Fatalf("Unexpected error writing tarball: %v", err)
	}

	tarImage, err := tarball.ImageFromPath(fp.Name(), &tag)
	if err != nil {
		t.Fatalf("Unexpected error reading tarball: %v", err)
	}

	if err := validate.Image(tarImage); err != nil {
		t.Fatalf("validate.Image(): %v", err)
	}

	m, err := tarImage.Manifest()
	if err != nil {
		t.Fatal(err)
	}

	if got, want := m.Layers[1].MediaType, types.DockerForeignLayer; got != want {
		t.Errorf("Wrong MediaType: %s != %s", got, want)
	}
	if got, want := m.Layers[1].URLs[0], "example.com"; got != want {
		t.Errorf("Wrong URLs: %s != %s", got, want)
	}
}

func TestMultiWriteNoHistory(t *testing.T) {
	// Make a random image.
	img, err := random.Image(256, 8)
	if err != nil {
		t.Fatalf("Error creating random image: %v", err)
	}
	cfg, err := img.ConfigFile()
	if err != nil {
		t.Fatalf("Error getting image config: %v", err)
	}
	// Blank out the layer history.
	cfg.History = nil
	tag, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation)
	if err != nil {
		t.Fatalf("Error creating test tag: %v", err)
	}
	// Make a tempfile for tarball writes.
	fp, err := os.CreateTemp("", "")
	if err != nil {
		t.Fatalf("Error creating temp file: %v", err)
	}
	t.Log(fp.Name())
	defer fp.Close()
	defer os.Remove(fp.Name())
	if err := Write(tag, img, fp); err != nil {
		t.Fatalf("Unexpected error writing tarball: %v", err)
	}
	tarImage, err := tarball.ImageFromPath(fp.Name(), &tag)
	if err != nil {
		t.Fatalf("Unexpected error reading tarball: %v", err)
	}
	if err := validate.Image(tarImage); err != nil {
		t.Fatalf("validate.Image(): %v", err)
	}
}

func TestMultiWriteHistoryEmptyLayers(t *testing.T) {
	// Build a history for 2 layers that is interspersed with empty layer
	// history.
	h := []v1.History{
		{EmptyLayer: true},
		{EmptyLayer: false},
		{EmptyLayer: true},
		{EmptyLayer: false},
		{EmptyLayer: true},
	}
	// Make a random image with the number of non-empty layers from the history
	// above.
	img, err := random.Image(256, int64(len(filterEmpty(h))))
	if err != nil {
		t.Fatalf("Error creating random image: %v", err)
	}
	cfg, err := img.ConfigFile()
	if err != nil {
		t.Fatalf("Error getting image config: %v", err)
	}
	// Override the config history with our custom built history that includes
	// history for empty layers.
	cfg.History = h
	tag, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation)
	if err != nil {
		t.Fatalf("Error creating test tag: %v", err)
	}
	// Make a tempfile for tarball writes.
	fp, err := os.CreateTemp("", "")
	if err != nil {
		t.Fatalf("Error creating temp file: %v", err)
	}
	t.Log(fp.Name())
	defer fp.Close()
	defer os.Remove(fp.Name())
	if err := Write(tag, img, fp); err != nil {
		t.Fatalf("Unexpected error writing tarball: %v", err)
	}
	tarImage, err := tarball.ImageFromPath(fp.Name(), &tag)
	if err != nil {
		t.Fatalf("Unexpected error reading tarball: %v", err)
	}
	if err := validate.Image(tarImage); err != nil {
		t.Fatalf("validate.Image(): %v", err)
	}
}

func TestMultiWriteMismatchedHistory(t *testing.T) {
	// Make a random image
	img, err := random.Image(256, 8)
	if err != nil {
		t.Fatalf("Error creating random image: %v", err)
	}
	cfg, err := img.ConfigFile()
	if err != nil {
		t.Fatalf("Error getting image config: %v", err)
	}

	// Set the history such that number of history entries != layers. This
	// should trigger an error during the image write.
	cfg.History = make([]v1.History, 1)
	img, err = mutate.ConfigFile(img, cfg)
	if err != nil {
		t.Fatalf("mutate.ConfigFile() = %v", err)
	}

	tag, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation)
	if err != nil {
		t.Fatalf("Error creating test tag: %v", err)
	}
	// Make a tempfile for tarball writes.
	fp, err := os.CreateTemp("", "")
	if err != nil {
		t.Fatalf("Error creating temp file: %v", err)
	}
	t.Log(fp.Name())
	defer fp.Close()
	defer os.Remove(fp.Name())
	err = Write(tag, img, fp)
	if err == nil {
		t.Fatal("Unexpected success writing tarball, got nil, want error.")
	}
	want := "image config had layer history which did not match the number of layers"
	if !strings.Contains(err.Error(), want) {
		t.Errorf("Got unexpected error when writing image with mismatched history & layer, got %v, want substring %q", err, want)
	}
}

type fastSizeLayer struct {
	v1.Layer
	size   int64
	called bool
}

func (l *fastSizeLayer) UncompressedSize() (int64, error) {
	l.called = true
	return l.size, nil
}

func TestUncompressedSize(t *testing.T) {
	// Make a random image
	img, err := random.Image(256, 8)
	if err != nil {
		t.Fatalf("Error creating random image: %v", err)
	}

	rand, err := random.Layer(1000, types.DockerLayer)
	if err != nil {
		t.Fatal(err)
	}

	size, err := partial.UncompressedSize(rand)
	if err != nil {
		t.Fatal(err)
	}

	l := &fastSizeLayer{Layer: rand, size: size}

	img, err = mutate.AppendLayers(img, l)
	if err != nil {
		t.Fatal(err)
	}
	tag, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation)
	if err != nil {
		t.Fatalf("Error creating test tag: %v", err)
	}
	// Make a tempfile for tarball writes.
	fp, err := os.CreateTemp("", "")
	if err != nil {
		t.Fatalf("Error creating temp file: %v", err)
	}
	t.Log(fp.Name())
	defer fp.Close()
	defer os.Remove(fp.Name())
	if err := Write(tag, img, fp); err != nil {
		t.Fatalf("Write(): %v", err)
	}
	if !l.called {
		t.Errorf("expected UncompressedSize to be called, but it wasn't")
	}
}

// TestWriteSharedLayers tests that writing a tarball of multiple images that
// share some layers only writes those shared layers once.
func TestWriteSharedLayers(t *testing.T) {
	// Make a tempfile for tarball writes.
	fp, err := os.CreateTemp("", "")
	if err != nil {
		t.Fatalf("Error creating temp file: %v", err)
	}
	t.Log(fp.Name())
	defer fp.Close()
	defer os.Remove(fp.Name())

	const baseImageLayerCount = 8

	// Make a random image
	baseImage, err := random.Image(256, baseImageLayerCount)
	if err != nil {
		t.Fatalf("Error creating base image: %v", err)
	}

	// Make another random image
	randLayer, err := random.Layer(256, types.DockerLayer)
	if err != nil {
		t.Fatalf("Error creating random layer %v", err)
	}
	extendedImage, err := mutate.Append(baseImage, mutate.Addendum{
		Layer: randLayer,
	})
	if err != nil {
		t.Fatalf("Error mutating base image %v", err)
	}

	// Create two tags, one pointing to each image created.
	tag1, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation)
	if err != nil {
		t.Fatalf("Error creating test tag1: %v", err)
	}
	tag2, err := name.NewTag("gcr.io/baz/bat:latest", name.StrictValidation)
	if err != nil {
		t.Fatalf("Error creating test tag2: %v", err)
	}
	refToImage := map[name.Reference]v1.Image{
		tag1: baseImage,
		tag2: extendedImage,
	}

	o, err := os.Create(fp.Name())
	if err != nil {
		t.Fatalf("Error creating %q to write image tarball: %v", fp.Name(), err)
	}
	defer o.Close()

	// Write both images to the tarball.
	if err := MultiWrite(refToImage, o); err != nil {
		t.Fatalf("Unexpected error writing tarball: %v", err)
	}
	for ref, img := range refToImage {
		tag, ok := ref.(name.Tag)
		if !ok {
			continue
		}

		tarImage, err := tarball.ImageFromPath(fp.Name(), &tag)
		if err != nil {
			t.Fatalf("Unexpected error reading tarball: %v", err)
		}
		if err := validate.Image(tarImage); err != nil {
			t.Errorf("validate.Image: %v", err)
		}
		if err := compare.Images(img, tarImage); err != nil {
			t.Errorf("compare.Images: %v", err)
		}
	}

	wantIDs := make(map[string]struct{})
	ids, err := v1LayerIDs(baseImage)
	if err != nil {
		t.Fatalf("Error getting base image IDs: %v", err)
	}
	for _, id := range ids {
		wantIDs[id] = struct{}{}
	}
	ids, err = v1LayerIDs(extendedImage)
	if err != nil {
		t.Fatalf("Error getting extended image IDs: %v", err)
	}
	for _, id := range ids {
		wantIDs[id] = struct{}{}
	}

	// base + extended layer + different top base layer
	if len(wantIDs) != baseImageLayerCount+2 {
		t.Errorf("Expected to have %d unique layer IDs but have %d", baseImageLayerCount+2, len(wantIDs))
	}

	const layerFileName = "layer.tar"
	r := tar.NewReader(fp)
	for {
		hdr, err := r.Next()
		if err != nil {
			if errors.Is(err, io.EOF) {
				break
			}
			t.Fatalf("Get tar header: %v", err)
		}
		if filepath.Base(hdr.Name) == layerFileName {
			id := filepath.Dir(hdr.Name)
			if _, ok := wantIDs[id]; ok {
				delete(wantIDs, id)
			} else {
				t.Errorf("Found unwanted layer with ID %q", id)
			}
		}
	}
	if len(wantIDs) != 0 {
		for id := range wantIDs {
			t.Errorf("Expected to find layer with ID %q but it didn't exist", id)
		}
	}
}

func v1LayerIDs(img v1.Image) ([]string, error) {
	layers, err := img.Layers()
	if err != nil {
		return nil, fmt.Errorf("get layers: %w", err)
	}
	ids := make([]string, len(layers))
	parentID := ""
	for i, layer := range layers {
		var rawCfg []byte
		if i == len(layers)-1 {
			rawCfg, err = img.RawConfigFile()
			if err != nil {
				return nil, fmt.Errorf("get raw config file: %w", err)
			}
		}
		id, err := v1LayerID(layer, parentID, rawCfg)
		if err != nil {
			return nil, fmt.Errorf("get v1 layer ID: %w", err)
		}

		ids[i] = id
		parentID = id
	}
	return ids, nil
}
