using Omika.App.Services;
using Omika.App.ViewModels;
using Omika.Core.Constants;
using SkiaSharp;
namespace Omika.Tests.Mobile.Local;
///
/// Tests de l'orientation EXIF et des règles de la politique de poids des photos.
///
/// L'orientation est vérifiée sur une image 2×1 dont les deux pixels sont
/// distincts, ce qui permet de valider les huit orientations sans dépendre d'un
/// fichier EXIF réel.
///
public class ImageOrientationTests
{
private static readonly SKColor Rouge = new(255, 0, 0);
private static readonly SKColor Bleu = new(0, 0, 255);
/// Source 2×1 : pixel gauche rouge, pixel droit bleu.
private static SKBitmap CreateSource()
{
var bitmap = new SKBitmap(new SKImageInfo(2, 1, SKColorType.Rgba8888, SKAlphaType.Premul));
bitmap.SetPixel(0, 0, Rouge);
bitmap.SetPixel(1, 0, Bleu);
return bitmap;
}
private static SKColor Pixel(SKBitmap bitmap, int x, int y) => bitmap.GetPixel(x, y);
[Fact]
public void TopLeft_AucuneTransformation()
{
// L'orientation normale ne doit produire aucune copie inutile.
using SKBitmap source = CreateSource();
Assert.Null(ImageOrientation.ApplyOrientation(source, SKEncodedOrigin.TopLeft));
}
[Theory]
// Miroir horizontal : [R B] → [B R]
[InlineData(SKEncodedOrigin.TopRight, 2, 1, 1, 0, true)]
// Rotation 180° : [R B] → [B R]
[InlineData(SKEncodedOrigin.BottomRight, 2, 1, 1, 0, true)]
// Miroir vertical sur une seule ligne : identité [R B]
[InlineData(SKEncodedOrigin.BottomLeft, 2, 1, 0, 0, true)]
// Transposition : colonne [R ; B]
[InlineData(SKEncodedOrigin.LeftTop, 1, 2, 0, 0, true)]
// Rotation 90° horaire : colonne [R ; B]
[InlineData(SKEncodedOrigin.RightTop, 1, 2, 0, 0, true)]
// Miroir + 90° horaire : colonne [B ; R]
[InlineData(SKEncodedOrigin.RightBottom, 1, 2, 0, 0, false)]
// Rotation 90° anti-horaire : colonne [B ; R]
[InlineData(SKEncodedOrigin.LeftBottom, 1, 2, 0, 0, false)]
public void Orientation_AxesEtCouleursCorrects(
SKEncodedOrigin origin,
int expectedWidth,
int expectedHeight,
int xDuPremierPixel,
int yDuPremierPixel,
bool premierPixelRouge)
{
// Act
using SKBitmap source = CreateSource();
using SKBitmap? result = ImageOrientation.ApplyOrientation(source, origin);
// Assert : dimensions permutées quand l'orientation le requiert…
Assert.NotNull(result);
Assert.Equal(expectedWidth, result.Width);
Assert.Equal(expectedHeight, result.Height);
// … et orientation réellement appliquée (aucun pixel perdu, aucun mélange).
SKColor premier = Pixel(result, xDuPremierPixel, yDuPremierPixel);
SKColor attendu = premierPixelRouge ? Rouge : Bleu;
Assert.Equal(attendu, premier);
// Les deux couleurs d'origine sont toujours présentes exactement une fois.
int rouges = 0;
int bleus = 0;
for (int x = 0; x < result.Width; x++)
{
for (int y = 0; y < result.Height; y++)
{
SKColor pixel = Pixel(result, x, y);
if (pixel == Rouge) rouges++;
if (pixel == Bleu) bleus++;
}
}
Assert.Equal(1, rouges);
Assert.Equal(1, bleus);
}
}
///
/// Tests des règles de la politique de poids des photos : détection d'image,
/// normalisation de la valeur configurée et formatage lisible.
///
public class PhotoOptimizationSettingsTests
{
[Theory]
[InlineData("photo.jpg", true)]
[InlineData("photo.JPEG", true)]
[InlineData("capture.png", true)]
[InlineData("anim.gif", true)]
[InlineData("image.webp", true)]
[InlineData("photo.heic", true)]
[InlineData("scan.tiff", true)]
[InlineData("rapport.pdf", false)]
[InlineData("lettre.docx", false)]
[InlineData("classeur.xlsx", false)]
[InlineData("archive.zip", false)]
[InlineData("notes.txt", false)]
[InlineData("sans_extension", false)]
[InlineData("", false)]
public void IsImageExtension_ClasseCorrectement(string fileName, bool expected)
=> Assert.Equal(expected, ImageFileTypes.IsImageExtension(fileName));
[Fact]
public void IsImage_MimeSuffitSansExtension()
=> Assert.True(ImageFileTypes.IsImage("fichier", "image/jpeg"));
[Fact]
public void IsImage_ExtensionSuffitSansMime()
=> Assert.True(ImageFileTypes.IsImage("photo.jpg", null));
[Fact]
public void IsImage_PdfJamaisImage()
=> Assert.False(ImageFileTypes.IsImage("rapport.pdf", "application/pdf"));
[Theory]
[InlineData(null, 500, PhotoLimitSource.DefaultFallback)]
[InlineData("", 500, PhotoLimitSource.DefaultFallback)]
[InlineData(" ", 500, PhotoLimitSource.DefaultFallback)]
[InlineData("abc", 500, PhotoLimitSource.DefaultFallback)]
[InlineData("-10", 500, PhotoLimitSource.DefaultFallback)]
// 0 est une valeur métier valide : « aucune limite », jamais remplacée par le repli.
[InlineData("0", 0, PhotoLimitSource.ApplicationParameter)]
[InlineData("500", 500, PhotoLimitSource.ApplicationParameter)]
[InlineData(" 2048 ", 2048, PhotoLimitSource.ApplicationParameter)]
[InlineData("10240", 10240, PhotoLimitSource.ApplicationParameter)]
public void Resolve_ValeurEtSource(string? valeur, int expectedLimit, PhotoLimitSource expectedSource)
{
PhotoLimitResolution resolution = PhotoOptimizationSettings.Resolve(valeur);
Assert.Equal(expectedLimit, resolution.LimitKb);
Assert.Equal(expectedSource, resolution.Source);
Assert.Equal(expectedSource == PhotoLimitSource.DefaultFallback, resolution.IsFallback);
Assert.Equal(expectedLimit <= 0, resolution.IsDisabled);
}
[Fact]
public void Resolve_Zero_NEStJamaisRemplaceParLeRepli()
{
// Règle métier critique : 0 ≠ absent.
PhotoLimitResolution zero = PhotoOptimizationSettings.Resolve("0");
PhotoLimitResolution absent = PhotoOptimizationSettings.Resolve(null);
Assert.Equal(0, zero.LimitKb);
Assert.True(zero.IsDisabled);
Assert.False(zero.IsFallback);
Assert.Equal(500, absent.LimitKb);
Assert.True(absent.IsFallback);
Assert.NotEqual(zero.LimitKb, absent.LimitKb);
}
[Theory]
[InlineData(0, "Aucune limite")]
[InlineData(250, "250 Ko")]
[InlineData(500, "500 Ko")]
[InlineData(750, "750 Ko")]
[InlineData(1024, "1 Mo")]
[InlineData(1536, "1,5 Mo")]
[InlineData(2048, "2 Mo")]
[InlineData(7680, "7,5 Mo")]
[InlineData(10240, "10 Mo")]
public void FormatPhotoSize_LibelleUtilisateur(int kiloOctets, string expected)
=> Assert.Equal(expected, ParametresViewModel.FormatPhotoSize(kiloOctets));
[Theory]
[InlineData(500, 2)]
[InlineData(2, 0)]
[InlineData(0, 0)]
[InlineData(10240, 11)]
[InlineData(9000, 11)]
[InlineData(1100, 4)]
public void IndexDuPalierLePlusProche_PositionneLaJauge(int kiloOctets, int expectedIndex)
=> Assert.Equal(expectedIndex, ParametresViewModel.IndexDuPalierLePlusProche(kiloOctets));
}