package ai

import (
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/anchore/syft/syft/file"
	"github.com/anchore/syft/syft/pkg"
)

// stPkg builds a model package carrying the given metadata, with each path
// recorded as a primary-evidence location.
func stPkg(md pkg.SafeTensorsModelInfo, paths ...string) pkg.Package {
	locs := make([]file.Location, 0, len(paths))
	for _, p := range paths {
		locs = append(locs, file.NewLocation(p).WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation))
	}
	return pkg.Package{Type: pkg.ModelPkg, Metadata: md, Locations: file.NewLocationSet(locs...)}
}

// shardMeta is a content-derived shard entry: it carries a MetadataHash, which is
// what marks a group member as a shard (vs. a hash-less aggregate config blob).
func shardMeta(hash string, tensorCount uint64) pkg.SafeTensorsModelInfo {
	return pkg.SafeTensorsModelInfo{
		Format:       "safetensors",
		TensorCount:  tensorCount,
		Quantization: "BF16",
		Parameters:   1000,
		MetadataHash: hash,
	}
}

// TestMergeSafeTensorsGroup exercises the rollup contract directly (the cataloger
// tests cover it only as a side effect of the merge processor). It locks how a
// group's per-member metadata folds into one package: tensor-count summing,
// aggregate-over-shard field precedence, UserMetadata dedup + sorting, Parts
// rollup, ShardCount derivation, and the content MetadataHash rollup.
func TestMergeSafeTensorsGroup(t *testing.T) {
	t.Run("single shard: hash passes through, ShardCount 1, no Parts", func(t *testing.T) {
		out := mergeSafeTensorsGroup([]pkg.Package{stPkg(shardMeta("aaaa", 5), "/m/a.safetensors")})

		md := out.Metadata.(pkg.SafeTensorsModelInfo)
		assert.Equal(t, pkg.ModelPkg, out.Type)
		assert.Equal(t, 1, md.ShardCount)
		assert.Equal(t, uint64(5), md.TensorCount)
		assert.Equal(t, "aaaa", md.MetadataHash, "a single shard's hash passes through unchanged")
		assert.Nil(t, md.Parts, "single-shard models do not populate Parts")
	})

	t.Run("multi-shard: tensors summed, Parts sorted by hash, rollup is order-independent", func(t *testing.T) {
		in := []pkg.Package{
			stPkg(shardMeta("cccc", 3), "/m/c.safetensors"),
			stPkg(shardMeta("aaaa", 3), "/m/a.safetensors"),
			stPkg(shardMeta("bbbb", 3), "/m/b.safetensors"),
		}
		out := mergeSafeTensorsGroup(in)

		md := out.Metadata.(pkg.SafeTensorsModelInfo)
		assert.Equal(t, 3, md.ShardCount)
		assert.Equal(t, uint64(9), md.TensorCount, "tensor counts are summed across shards")
		assert.Equal(t, uint64(3000), md.Parameters, "parameter counts are summed across shards")
		require.Len(t, md.Parts, 3)
		assert.Equal(t,
			[]string{"aaaa", "bbbb", "cccc"},
			[]string{md.Parts[0].MetadataHash, md.Parts[1].MetadataHash, md.Parts[2].MetadataHash},
			"Parts are sorted by metadata hash",
		)
		assert.Equal(t, rollupHash([]string{"aaaa", "bbbb", "cccc"}), md.MetadataHash)

		// the rollup hash must not depend on the order members arrive in
		shuffled := []pkg.Package{
			stPkg(shardMeta("bbbb", 3), "/m/b.safetensors"),
			stPkg(shardMeta("cccc", 3), "/m/c.safetensors"),
			stPkg(shardMeta("aaaa", 3), "/m/a.safetensors"),
		}
		out2 := mergeSafeTensorsGroup(shuffled)
		assert.Equal(t, md.MetadataHash, out2.Metadata.(pkg.SafeTensorsModelInfo).MetadataHash)
	})

	t.Run("aggregate fields win over shard-derived fields", func(t *testing.T) {
		// an aggregate (OCI config blob) carries no MetadataHash but declares the
		// authoritative totals.
		aggregate := pkg.SafeTensorsModelInfo{
			Format:       "safetensors",
			TensorCount:  999,
			TotalSize:    "5.00GB",
			Quantization: "Q4_K_M",
		}
		in := []pkg.Package{
			stPkg(aggregate, "/"),
			stPkg(shardMeta("aaaa", 3), "/"),
			stPkg(shardMeta("bbbb", 3), "/"),
		}
		out := mergeSafeTensorsGroup(in)

		md := out.Metadata.(pkg.SafeTensorsModelInfo)
		assert.Equal(t, uint64(999), md.TensorCount, "aggregate TensorCount is authoritative; shard counts are not summed in")
		assert.Equal(t, "5.00GB", md.TotalSize)
		assert.Equal(t, uint64(2000), md.Parameters, "parameters are always measured from the shards (summed), not taken from the aggregate")
		assert.Equal(t, "Q4_K_M", md.Quantization, "aggregate quantization wins over the shard dtype")
		assert.Equal(t, 2, md.ShardCount, "ShardCount comes from the number of shards, not the aggregate")
		assert.Equal(t, rollupHash([]string{"aaaa", "bbbb"}), md.MetadataHash, "the content hash still rolls up the shard hashes")
	})

	t.Run("aggregate-only group: ShardCount 1, empty hash, no Parts", func(t *testing.T) {
		aggregate := pkg.SafeTensorsModelInfo{Format: "safetensors", TensorCount: 42, TotalSize: "1.00GB"}
		out := mergeSafeTensorsGroup([]pkg.Package{stPkg(aggregate, "/")})

		md := out.Metadata.(pkg.SafeTensorsModelInfo)
		assert.Equal(t, uint64(42), md.TensorCount)
		assert.Equal(t, 1, md.ShardCount, "a group with no shards still reports a single shard")
		assert.Equal(t, "", md.MetadataHash, "there are no shard hashes to roll up")
		assert.Nil(t, md.Parts)
	})

	t.Run("UserMetadata: keys merged and sorted, first value wins on conflict", func(t *testing.T) {
		// keys are intentionally unsorted within each shard so the assertion proves
		// the merge re-sorts globally; "format" appears in both shards so dedup
		// precedence (first wins) is exercised too.
		s1 := shardMeta("aaaa", 1)
		s1.UserMetadata = pkg.KeyValues{{Key: "format", Value: "pt"}, {Key: "author", Value: "alice"}}
		s2 := shardMeta("bbbb", 1)
		s2.UserMetadata = pkg.KeyValues{{Key: "format", Value: "gguf"}, {Key: "license", Value: "mit"}}

		out := mergeSafeTensorsGroup([]pkg.Package{stPkg(s1, "/m/a.safetensors"), stPkg(s2, "/m/b.safetensors")})

		md := out.Metadata.(pkg.SafeTensorsModelInfo)
		assert.Equal(t, pkg.KeyValues{
			{Key: "author", Value: "alice"},
			{Key: "format", Value: "pt"}, // first shard's value wins over s2's "gguf"
			{Key: "license", Value: "mit"},
		}, md.UserMetadata)
	})

	t.Run("members without safetensors metadata are ignored in the rollup", func(t *testing.T) {
		notST := pkg.Package{
			Type:      pkg.ModelPkg,
			Metadata:  pkg.GGUFFileHeader{},
			Locations: file.NewLocationSet(file.NewLocation("/m/x.gguf")),
		}
		out := mergeSafeTensorsGroup([]pkg.Package{stPkg(shardMeta("aaaa", 2), "/m/a.safetensors"), notST})

		md := out.Metadata.(pkg.SafeTensorsModelInfo)
		assert.Equal(t, uint64(2), md.TensorCount, "only the safetensors shard contributes")
		assert.Equal(t, 1, md.ShardCount)
		assert.Equal(t, "aaaa", md.MetadataHash)
	})
}

// TestRollupHash locks the cross-source content-fingerprint rollup: empty input
// yields no hash, a lone shard's hash passes through unchanged (so a single-shard
// model fingerprints identically across directory and OCI sources), and multiple
// shards fold into one order-independent digest.
func TestRollupHash(t *testing.T) {
	assert.Equal(t, "", rollupHash(nil), "no hashes → empty")
	assert.Equal(t, "solo", rollupHash([]string{"solo"}), "a single hash passes through unchanged")

	ab := rollupHash([]string{"a", "b"})
	ba := rollupHash([]string{"b", "a"})
	assert.Equal(t, ab, ba, "the rollup is independent of input order")
	assert.Len(t, ab, 16, "a multi-hash rollup is a 16-char xxhash")
	assert.NotEqual(t, "a", ab)
	assert.NotEqual(t, "b", ab)
}
