// Copyright 2023 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_test

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

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

func TestReferrers(t *testing.T) {
	// Run all tests against:
	//
	//   (1) A OCI 1.0 registry (without referrers API)
	//   (2) An OCI 1.1+ registry (with referrers API)
	//
	for _, leg := range []struct {
		server      *httptest.Server
		tryFallback bool
	}{
		{
			server:      httptest.NewServer(registry.New(registry.WithReferrersSupport(false))),
			tryFallback: true,
		},
		{
			server:      httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
			tryFallback: false,
		},
	} {
		s := leg.server
		defer s.Close()
		u, err := url.Parse(s.URL)
		if err != nil {
			t.Fatal(err)
		}

		descriptor := func(img v1.Image) v1.Descriptor {
			d, err := img.Digest()
			if err != nil {
				t.Fatal(err)
			}
			sz, err := img.Size()
			if err != nil {
				t.Fatal(err)
			}
			mt, err := img.MediaType()
			if err != nil {
				t.Fatal(err)
			}
			return v1.Descriptor{
				Digest:       d,
				Size:         sz,
				MediaType:    mt,
				ArtifactType: "application/testing123",
			}
		}

		// Push an image we'll attach things to.
		// We'll copy from src to dst.
		rootRef, err := name.ParseReference(fmt.Sprintf("%s/repo:root", u.Host))
		if err != nil {
			t.Fatal(err)
		}
		rootImg, err := random.Image(10, 10)
		if err != nil {
			t.Fatal(err)
		}
		rootImg = mutate.ConfigMediaType(rootImg, types.MediaType("application/testing123"))
		if err := remote.Write(rootRef, rootImg); err != nil {
			t.Fatal(err)
		}
		rootDesc := descriptor(rootImg)
		t.Logf("root image is %s", rootDesc.Digest)

		// Before pushing referrers, try to get the referrers of the root image.
		rootRefDigest := rootRef.Context().Digest(rootDesc.Digest.String())
		index, err := remote.Referrers(rootRefDigest)
		if err != nil {
			t.Fatal(err)
		}
		m, err := index.IndexManifest()
		if err != nil {
			t.Fatal(err)
		}
		if numManifests := len(m.Manifests); numManifests != 0 {
			t.Fatalf("expected index to contain 0 manifests, but had %d", numManifests)
		}

		// Push an image that refers to the root image as its subject.
		leafRef, err := name.ParseReference(fmt.Sprintf("%s/repo:leaf", u.Host))
		if err != nil {
			t.Fatal(err)
		}
		leafImg, err := random.Image(20, 20)
		if err != nil {
			t.Fatal(err)
		}
		leafImg = mutate.ConfigMediaType(leafImg, types.MediaType("application/testing123"))
		leafImg = mutate.Subject(leafImg, rootDesc).(v1.Image)
		if err := remote.Write(leafRef, leafImg); err != nil {
			t.Fatal(err)
		}
		leafDesc := descriptor(leafImg)
		t.Logf("leaf image is %s", leafDesc.Digest)

		// Get the referrers of the root image, by digest.
		index, err = remote.Referrers(rootRefDigest)
		if err != nil {
			t.Fatal(err)
		}
		m2, err := index.IndexManifest()
		if err != nil {
			t.Fatal(err)
		}
		if d := cmp.Diff([]v1.Descriptor{leafDesc}, m2.Manifests); d != "" {
			t.Logf("m2.Manifests: %v", m2.Manifests)
			t.Fatalf("referrers diff (-want,+got): %s", d)
		}

		if leg.tryFallback {
			// Get the referrers by querying the root image's fallback tag directly.
			tag, err := name.ParseReference(fmt.Sprintf("%s/repo:sha256-%s", u.Host, rootDesc.Digest.Hex))
			if err != nil {
				t.Fatal(err)
			}
			idx, err := remote.Index(tag)
			if err != nil {
				t.Fatal(err)
			}
			mf, err := idx.IndexManifest()
			if err != nil {
				t.Fatal(err)
			}
			m2, err := index.IndexManifest()
			if err != nil {
				t.Fatal(err)
			}
			if d := cmp.Diff(m2.Manifests, mf.Manifests); d != "" {
				t.Fatalf("fallback tag diff (-want,+got): %s", d)
			}
		}

		// Push the leaf image again, this time with a different tag.
		// This shouldn't add another item to the root image's referrers,
		// because it's the same digest.
		// Push an image that refers to the root image as its subject.
		leaf2Ref, err := name.ParseReference(fmt.Sprintf("%s/repo:leaf2", u.Host))
		if err != nil {
			t.Fatal(err)
		}
		if err := remote.Write(leaf2Ref, leafImg); err != nil {
			t.Fatal(err)
		}
		// Get the referrers of the root image again, which should only have one entry.
		rootRefDigest = rootRef.Context().Digest(rootDesc.Digest.String())
		index, err = remote.Referrers(rootRefDigest)
		if err != nil {
			t.Fatal(err)
		}
		m3, err := index.IndexManifest()
		if err != nil {
			t.Fatal(err)
		}
		if d := cmp.Diff([]v1.Descriptor{leafDesc}, m3.Manifests); d != "" {
			t.Fatalf("referrers diff after second push (-want,+got): %s", d)
		}

		// Try applying filters and verify number of manifests and and annotations
		index, err = remote.Referrers(rootRefDigest,
			remote.WithFilter("artifactType", "application/testing123"))
		if err != nil {
			t.Fatal(err)
		}
		m4, err := index.IndexManifest()
		if err != nil {
			t.Fatal(err)
		}
		if numManifests := len(m4.Manifests); numManifests == 0 {
			t.Fatal("index contained 0 manifests")
		}

		index, err = remote.Referrers(rootRefDigest,
			remote.WithFilter("artifactType", "application/testing123BADDDD"))
		if err != nil {
			t.Fatal(err)
		}
		m5, err := index.IndexManifest()
		if err != nil {
			t.Fatal(err)
		}
		if numManifests := len(m5.Manifests); numManifests != 0 {
			t.Fatalf("expected index to contain 0 manifests, but had %d", numManifests)
		}
	}
}
