using Scry.Core.Imaging; using SkiaSharp; using Xunit; namespace Scry.Tests; public class PerceptualHashTests { [Fact] public void ComputeHash_ReturnsConsistentHash() { using var bitmap = CreateTestBitmap(32, 32, SKColors.Red); var hash1 = PerceptualHash.ComputeHash(bitmap); var hash2 = PerceptualHash.ComputeHash(bitmap); Assert.Equal(hash1, hash2); } [Fact] public void ComputeColorHash_Returns24Bytes() { using var bitmap = CreateTestBitmap(32, 32, SKColors.Blue); var hash = PerceptualHash.ComputeColorHash(bitmap); Assert.Equal(24, hash.Length); } [Fact] public void HammingDistance_IdenticalHashes_ReturnsZero() { var hash = new byte[] { 0xFF, 0x00, 0xAB, 0xCD }; var distance = PerceptualHash.HammingDistance(hash, hash); Assert.Equal(0, distance); } [Fact] public void HammingDistance_OppositeHashes_ReturnsMaxBits() { var hash1 = new byte[] { 0x00, 0x00 }; var hash2 = new byte[] { 0xFF, 0xFF }; var distance = PerceptualHash.HammingDistance(hash1, hash2); Assert.Equal(16, distance); } [Fact] public void HammingDistance_SingleBitDifference() { var hash1 = new byte[] { 0b00000000 }; var hash2 = new byte[] { 0b00000001 }; var distance = PerceptualHash.HammingDistance(hash1, hash2); Assert.Equal(1, distance); } [Fact] public void CalculateConfidence_ZeroDistance_ReturnsOne() { var confidence = PerceptualHash.CalculateConfidence(0, 192); Assert.Equal(1.0f, confidence); } [Fact] public void CalculateConfidence_HalfDistance_ReturnsHalf() { var confidence = PerceptualHash.CalculateConfidence(96, 192); Assert.Equal(0.5f, confidence); } [Theory] [InlineData("reference/brainstorm.png")] [InlineData("reference/force_of_will.png")] [InlineData("single_cards/llanowar_elves.jpg")] public void ComputeColorHash_RealImages_ProducesValidHash(string imagePath) { var fullPath = Path.Combine("TestImages", imagePath); if (!File.Exists(fullPath)) { return; } using var bitmap = SKBitmap.Decode(fullPath); Assert.NotNull(bitmap); var hash = PerceptualHash.ComputeColorHash(bitmap); Assert.Equal(24, hash.Length); Assert.True(hash.Any(b => b != 0), "Hash should not be all zeros"); } [Fact] public void SimilarImages_HaveLowHammingDistance() { using var bitmap1 = CreateGradientBitmap(32, 32, SKColors.Red, SKColors.Blue); using var bitmap2 = CreateGradientBitmap(32, 32, SKColors.Red, SKColors.Blue); var hash1 = PerceptualHash.ComputeColorHash(bitmap1); var hash2 = PerceptualHash.ComputeColorHash(bitmap2); var distance = PerceptualHash.HammingDistance(hash1, hash2); Assert.Equal(0, distance); } [Fact] public void DifferentImages_HaveHighHammingDistance() { using var bitmap1 = CreateTestBitmap(32, 32, SKColors.Red); using var bitmap2 = CreateTestBitmap(32, 32, SKColors.Blue); var hash1 = PerceptualHash.ComputeColorHash(bitmap1); var hash2 = PerceptualHash.ComputeColorHash(bitmap2); var distance = PerceptualHash.HammingDistance(hash1, hash2); Assert.True(distance > 10, $"Expected distance > 10, got {distance}"); } private static SKBitmap CreateTestBitmap(int width, int height, SKColor color) { var bitmap = new SKBitmap(width, height, SKColorType.Rgba8888, SKAlphaType.Premul); using var canvas = new SKCanvas(bitmap); canvas.Clear(color); return bitmap; } private static SKBitmap CreateGradientBitmap(int width, int height, SKColor start, SKColor end) { var bitmap = new SKBitmap(width, height, SKColorType.Rgba8888, SKAlphaType.Premul); using var canvas = new SKCanvas(bitmap); using var paint = new SKPaint(); paint.Shader = SKShader.CreateLinearGradient( new SKPoint(0, 0), new SKPoint(width, height), new[] { start, end }, SKShaderTileMode.Clamp); canvas.DrawRect(0, 0, width, height, paint); return bitmap; } }