package image

import (
	"context"
	"path/filepath"
	"strings"
	"testing"
	"time"

	containerd "github.com/containerd/containerd/v2/client"
	localcontent "github.com/containerd/containerd/v2/plugins/content/local"
	"github.com/opencontainers/go-digest"
	ispec "github.com/opencontainers/image-spec/specs-go"
	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)

func TestSerializeUnpack(t *testing.T) {
	firstEntered := make(chan struct{})
	releaseFirst := make(chan struct{})
	firstDone := make(chan error, 1)
	released := false
	release := func() {
		if !released {
			close(releaseFirst)
			released = true
		}
	}
	defer release()

	go func() {
		firstDone <- serializeUnpack(context.Background(), "erofs", "sha256:chain", func() error {
			close(firstEntered)
			<-releaseFirst
			return nil
		})
	}()
	<-firstEntered

	secondCtx, cancelSecond := context.WithCancel(context.Background())
	secondDone := make(chan error, 1)
	secondCalled := false
	go func() {
		secondDone <- serializeUnpack(secondCtx, "erofs", "sha256:chain", func() error {
			secondCalled = true
			return nil
		})
	}()
	waitForUnpackLockRefs(t, "sn://erofs/sha256:chain", 2)
	cancelSecond()
	select {
	case err := <-secondDone:
		if err != context.Canceled {
			t.Fatalf("second serializeUnpack() error = %v, want context.Canceled", err)
		}
	case <-time.After(time.Second):
		release()
		<-firstDone
		t.Fatal("canceled duplicate unpack remained blocked")
	}
	if secondCalled {
		t.Fatal("duplicate unpack entered before the first unpack completed")
	}

	for _, tc := range []struct {
		name        string
		snapshotter string
		chainID     string
	}{
		{name: "chain ID", snapshotter: "erofs", chainID: "sha256:other"},
		{name: "snapshotter", snapshotter: "overlayfs", chainID: "sha256:chain"},
	} {
		ctx, cancel := context.WithTimeout(context.Background(), time.Second)
		err := serializeUnpack(ctx, tc.snapshotter, tc.chainID, func() error { return nil })
		cancel()
		if err != nil {
			t.Fatalf("unpack with independent %s: %v", tc.name, err)
		}
	}
	release()
	if err := <-firstDone; err != nil {
		t.Fatalf("first serializeUnpack() error = %v", err)
	}
}

func waitForUnpackLockRefs(t *testing.T, key string, want int) {
	t.Helper()
	deadline := time.Now().Add(time.Second)
	for time.Now().Before(deadline) {
		componentUnpackLocks.mu.Lock()
		entry := componentUnpackLocks.locks[key]
		got := 0
		if entry != nil {
			got = entry.refs
		}
		componentUnpackLocks.mu.Unlock()
		if got == want {
			return
		}
		time.Sleep(time.Millisecond)
	}
	t.Fatalf("unpack lock %q did not reach %d references", key, want)
}

func TestGetKindDefaultsToUnknown(t *testing.T) {
	got := getKind(ocispec.Descriptor{})
	if got != KindUnknown {
		t.Fatalf("kind: got %q want %q", got, KindUnknown)
	}
}

func TestValidateNativeComponentManifestRejectsEmptyLayers(t *testing.T) {
	ctx := context.Background()
	store, err := localcontent.NewStore(filepath.Join(t.TempDir(), "content"))
	if err != nil {
		t.Fatalf("NewStore: %v", err)
	}
	client, err := containerd.New("", containerd.WithServices(containerd.WithContentStore(store)))
	if err != nil {
		t.Fatalf("new containerd client: %v", err)
	}

	manifestDesc, err := writeBlobJSONToContent(ctx, store, ocispec.Manifest{
		Versioned: ispec.Versioned{SchemaVersion: 2},
		MediaType: ocispec.MediaTypeImageManifest,
		Config: ocispec.Descriptor{
			MediaType: ocispec.MediaTypeImageConfig,
			Digest:    digest.FromString("empty-component-config"),
			Size:      1,
		},
	}, ocispec.MediaTypeImageManifest)
	if err != nil {
		t.Fatalf("write manifest: %v", err)
	}

	for _, kind := range []string{KindRootfs, KindSandbox, KindMemSnapshot} {
		t.Run(kind, func(t *testing.T) {
			err := validateNativeComponentManifest(ctx, client, kind, manifestDesc)
			if err == nil || !strings.Contains(err.Error(), "no layers") {
				t.Fatalf("validateNativeComponentManifest() error = %v, want no layers", err)
			}
		})
	}
}

func TestValidateBootIndexManifestKindsRejectsDuplicateAndUnknownKinds(t *testing.T) {
	descriptor := func(kind, payload string) ocispec.Descriptor {
		return ocispec.Descriptor{
			MediaType:   ocispec.MediaTypeImageManifest,
			Digest:      digest.FromString(payload),
			Size:        1,
			Annotations: map[string]string{"io.conch.kind": kind},
		}
	}

	_, err := validateBootIndexManifestKinds([]ocispec.Descriptor{
		descriptor(KindRootfs, "rootfs-a"),
		descriptor(KindRootfs, "rootfs-b"),
		descriptor(KindSandbox, "sandbox"),
	})
	if err == nil || !strings.Contains(err.Error(), "duplicate") {
		t.Fatalf("duplicate kind error = %v", err)
	}

	_, err = validateBootIndexManifestKinds([]ocispec.Descriptor{
		descriptor(KindRootfs, "rootfs"),
		descriptor(KindUnknown, "unknown"),
		descriptor(KindSandbox, "sandbox"),
	})
	if err == nil || !strings.Contains(err.Error(), "unsupported") {
		t.Fatalf("unknown kind error = %v", err)
	}
}