package directory

import (
	"bytes"
	"context"
	"errors"
	"io"
	"os"
	"path/filepath"
	"testing"

	"github.com/opencontainers/go-digest"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.podman.io/image/v5/internal/private"
	"go.podman.io/image/v5/manifest"
	"go.podman.io/image/v5/pkg/blobinfocache/memory"
	"go.podman.io/image/v5/types"
)

var (
	_ private.ImageSource      = (*dirImageSource)(nil)
	_ private.ImageDestination = (*dirImageDestination)(nil)
)

func TestDestinationReference(t *testing.T) {
	ref, tmpDir := refToTempDir(t)

	dest, err := ref.NewImageDestination(context.Background(), nil)
	require.NoError(t, err)
	defer dest.Close()
	ref2 := dest.Reference()
	assert.Equal(t, tmpDir, ref2.StringWithinTransport())
}

func TestGetPutManifest(t *testing.T) {
	ref, _ := refToTempDir(t)

	man := []byte("test-manifest")
	list := []byte("test-manifest-list")
	md, err := manifest.Digest(man)
	require.NoError(t, err)
	dest, err := ref.NewImageDestination(context.Background(), nil)
	require.NoError(t, err)
	defer dest.Close()
	err = dest.PutManifest(context.Background(), man, &md)
	assert.NoError(t, err)
	err = dest.PutManifest(context.Background(), list, nil)
	assert.NoError(t, err)
	err = dest.Commit(context.Background(), nil) // nil unparsedToplevel is invalid, we don’t currently use the value
	assert.NoError(t, err)

	src, err := ref.NewImageSource(context.Background(), nil)
	require.NoError(t, err)
	defer src.Close()
	m, mt, err := src.GetManifest(context.Background(), nil)
	assert.NoError(t, err)
	assert.Equal(t, list, m)
	assert.Equal(t, "", mt)

	m, mt, err = src.GetManifest(context.Background(), &md)
	assert.NoError(t, err)
	assert.Equal(t, man, m)
	assert.Equal(t, "", mt)
}

func TestGetPutBlob(t *testing.T) {
	computedBlob := []byte("test-blob")
	providedBlob := []byte("provided-blob")
	providedDigest := digest.Digest("sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")

	ref, _ := refToTempDir(t)
	cache := memory.New()

	dest, err := ref.NewImageDestination(context.Background(), nil)
	require.NoError(t, err)
	defer dest.Close()
	assert.Equal(t, types.PreserveOriginal, dest.DesiredLayerCompression())
	// PutBlob with caller-provided data
	providedInfo, err := dest.PutBlob(context.Background(), bytes.NewReader(providedBlob), types.BlobInfo{Digest: providedDigest, Size: int64(len(providedBlob))}, cache, false)
	assert.NoError(t, err)
	assert.Equal(t, int64(len(providedBlob)), providedInfo.Size)
	assert.Equal(t, providedDigest, providedInfo.Digest)
	// PutBlob with unknown data
	computedInfo, err := dest.PutBlob(context.Background(), bytes.NewReader(computedBlob), types.BlobInfo{Digest: "", Size: int64(-1)}, cache, false)
	assert.NoError(t, err)
	assert.Equal(t, int64(len(computedBlob)), computedInfo.Size)
	assert.Equal(t, digest.FromBytes(computedBlob), computedInfo.Digest)
	err = dest.Commit(context.Background(), nil) // nil unparsedToplevel is invalid, we don’t currently use the value
	assert.NoError(t, err)

	src, err := ref.NewImageSource(context.Background(), nil)
	require.NoError(t, err)
	defer src.Close()
	for digest, expectedBlob := range map[digest.Digest][]byte{
		providedInfo.Digest: providedBlob,
		computedInfo.Digest: computedBlob,
	} {
		rc, size, err := src.GetBlob(context.Background(), types.BlobInfo{Digest: digest, Size: int64(len(expectedBlob))}, cache)
		assert.NoError(t, err)
		defer rc.Close()
		b, err := io.ReadAll(rc)
		assert.NoError(t, err)
		assert.Equal(t, expectedBlob, b)
		assert.Equal(t, int64(len(expectedBlob)), size)
	}
}

// readerFromFunc allows implementing Reader by any function, e.g. a closure.
type readerFromFunc func([]byte) (int, error)

func (fn readerFromFunc) Read(p []byte) (int, error) {
	return fn(p)
}

// TestPutBlobDigestFailure simulates behavior on digest verification failure.
func TestPutBlobDigestFailure(t *testing.T) {
	const digestErrorString = "Simulated digest error"
	const blobDigest = digest.Digest("sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")

	ref, _ := refToTempDir(t)
	dirRef, ok := ref.(dirReference)
	require.True(t, ok)
	blobPath, err := dirRef.layerPath(blobDigest)
	require.NoError(t, err)
	cache := memory.New()

	firstRead := true
	reader := readerFromFunc(func(p []byte) (int, error) {
		_, err := os.Lstat(blobPath)
		require.Error(t, err)
		require.True(t, os.IsNotExist(err))
		if firstRead {
			if len(p) > 0 {
				firstRead = false
			}
			for i := range p {
				p[i] = 0xAA
			}
			return len(p), nil
		}
		return 0, errors.New(digestErrorString)
	})

	dest, err := ref.NewImageDestination(context.Background(), nil)
	require.NoError(t, err)
	defer dest.Close()
	_, err = dest.PutBlob(context.Background(), reader, types.BlobInfo{Digest: blobDigest, Size: -1}, cache, false)
	assert.ErrorContains(t, err, digestErrorString)
	err = dest.Commit(context.Background(), nil) // nil unparsedToplevel is invalid, we don’t currently use the value
	assert.NoError(t, err)

	_, err = os.Lstat(blobPath)
	require.Error(t, err)
	require.True(t, os.IsNotExist(err))
}

func TestGetPutSignatures(t *testing.T) {
	ref, _ := refToTempDir(t)

	man := []byte("test-manifest")
	list := []byte("test-manifest-list")
	md, err := manifest.Digest(man)
	require.NoError(t, err)
	// These signatures are completely invalid; start with 0xA3 just to be minimally plausible to signature.FromBlob.
	signatures := [][]byte{
		[]byte("\xA3sig1"),
		[]byte("\xA3sig2"),
	}
	listSignatures := [][]byte{
		[]byte("\xA3sig3"),
		[]byte("\xA3sig4"),
	}

	dest, err := ref.NewImageDestination(context.Background(), nil)
	require.NoError(t, err)
	defer dest.Close()
	err = dest.SupportsSignatures(context.Background())
	assert.NoError(t, err)

	err = dest.PutManifest(context.Background(), man, &md)
	require.NoError(t, err)
	err = dest.PutManifest(context.Background(), list, nil)
	require.NoError(t, err)

	err = dest.PutSignatures(context.Background(), signatures, &md)
	assert.NoError(t, err)
	err = dest.PutSignatures(context.Background(), listSignatures, nil)
	assert.NoError(t, err)
	err = dest.Commit(context.Background(), nil) // nil unparsedToplevel is invalid, we don’t currently use the value
	assert.NoError(t, err)

	src, err := ref.NewImageSource(context.Background(), nil)
	require.NoError(t, err)
	defer src.Close()
	sigs, err := src.GetSignatures(context.Background(), nil)
	assert.NoError(t, err)
	assert.Equal(t, listSignatures, sigs)

	sigs, err = src.GetSignatures(context.Background(), &md)
	assert.NoError(t, err)
	assert.Equal(t, signatures, sigs)
}

func TestSourceReference(t *testing.T) {
	ref, tmpDir := refToTempDir(t)
	err := os.WriteFile(filepath.Join(tmpDir, "version"), []byte("Directory Transport Version: 1.1\n"), 0o644)
	require.NoError(t, err)

	src, err := ref.NewImageSource(context.Background(), nil)
	require.NoError(t, err)
	defer src.Close()
	ref2 := src.Reference()
	assert.Equal(t, tmpDir, ref2.StringWithinTransport())
}
