using Omika.App.Local.Abstractions; using Omika.App.Local.Models; using Omika.App.Navigation; using Omika.App.Services; using Omika.App.ViewModels; using Omika.Core.Contracts.Documents; namespace Omika.Tests.Mobile.Local; /// /// Tests du ViewModel Documents / Photos : chargement local, états vide/erreur, /// téléchargement à la demande, ouverture, ajout photo/document et navigation. /// public class InterventionDocumentsViewModelTests { private sealed class FakeDocumentService : IDocumentLocalService { public IReadOnlyList ItemsToReturn { get; set; } = []; public bool ThrowOnLoad { get; set; } public bool DownloadResult { get; set; } = true; public int DownloadCalls { get; private set; } public bool OpenResult { get; set; } = true; public int OpenCalls { get; private set; } public InterventionDocumentItem? PhotoToReturn { get; set; } public InterventionDocumentItem? DocumentToReturn { get; set; } public AddPhotosResult PhotosBatchToReturn { get; set; } = new(); public InterventionDocumentItem? CameraPhotoToReturn { get; set; } public Exception? CameraExceptionToThrow { get; set; } public int AddPhotosCalls { get; private set; } public Task> GetDocumentsForInterventionAsync( Guid interventionGuid, CancellationToken ct = default) { if (ThrowOnLoad) { throw new InvalidOperationException("Erreur de chargement"); } return Task.FromResult(ItemsToReturn); } public Task EnsureDownloadedAsync(Guid documentGuid, CancellationToken ct = default) { DownloadCalls++; return Task.FromResult(DownloadResult); } public Task OpenAsync(Guid documentGuid, CancellationToken ct = default) { OpenCalls++; return Task.FromResult(OpenResult); } public Task GetCachedFilePathAsync(Guid documentGuid, CancellationToken ct = default) => Task.FromResult(null); public Task> GetNotesForDocumentAsync(Guid documentGuid, CancellationToken ct = default) => Task.FromResult>([]); public Task AddNoteAsync(Guid documentGuid, string contenu, CancellationToken ct = default) => Task.FromResult(new DocumentNoteItem()); public Task AddPhotoAsync(Guid interventionGuid, CancellationToken ct = default) { if (PhotoToReturn is not null) { var items = ItemsToReturn.ToList(); items.Add(PhotoToReturn); ItemsToReturn = items; } return Task.FromResult(PhotoToReturn); } public Task AddDocumentAsync(Guid interventionGuid, CancellationToken ct = default) { if (DocumentToReturn is not null) { var items = ItemsToReturn.ToList(); items.Add(DocumentToReturn); ItemsToReturn = items; } return Task.FromResult(DocumentToReturn); } public Task AddPhotosAsync(Guid interventionGuid, CancellationToken ct = default) { AddPhotosCalls++; foreach (InterventionDocumentItem item in PhotosBatchToReturn.Added) { var items = ItemsToReturn.ToList(); items.Add(item); ItemsToReturn = items; } return Task.FromResult(PhotosBatchToReturn); } public Task AddPhotoFromCameraAsync(Guid interventionGuid, CancellationToken ct = default) { if (CameraExceptionToThrow is not null) { return Task.FromException(CameraExceptionToThrow); } if (CameraPhotoToReturn is not null) { var items = ItemsToReturn.ToList(); items.Add(CameraPhotoToReturn); ItemsToReturn = items; } return Task.FromResult(CameraPhotoToReturn); } public Task AddAnnotatedPhotoAsync(Guid interventionGuid, Guid sourceDocumentGuid, byte[] pngBytes, string fileName, CancellationToken ct = default) => Task.FromResult(null); } private static InterventionDocumentItem CreateItem(Guid id, string nom, bool isPhoto = false, bool isCached = false) => new() { DocumentGuid = id, Nom = nom, Titre = nom, IsPhoto = isPhoto, IsCached = isCached }; private sealed class PermissivePermissionService : IPermissionService { public IReadOnlyDictionary Rights => new Dictionary(); public void SetRights(IReadOnlyDictionary rights) { } public bool HasRight(string key) => true; public bool MessagerieExterne => false; public void SetMessagerieExterne(bool externe) { } public bool PeutRechercherInterventions => true; public bool VoirRdvSansRestriction => true; public bool PeutVoirAgendaEquipe => true; public bool PeutEnvoyerEmail => true; public bool PeutComposerEmail => true; public bool PeutCreerRapport => true; public bool PeutGererLicence => true; public bool PeutSaisirTempsAutrui => true; public bool PeutVoirNotesAdmin => true; public bool PeutVoirClients => true; public bool PeutVoirSites => true; public bool PeutVoirDossiers => true; } private sealed class FakeSyncEngine : ISyncEngine { public Task SyncAsync(CancellationToken ct = default) => Task.FromResult(new SyncCycleResult { Executed = true }); public Task PushAsync(CancellationToken cancellationToken = default) => Task.FromResult(new SyncCycleResult { Executed = true }); } private readonly FakeDocumentService _service = new(); private readonly NavigationService _navigation = new(); private readonly InterventionDocumentsViewModel _viewModel; public InterventionDocumentsViewModelTests() { _viewModel = new InterventionDocumentsViewModel( _service, _navigation, new PermissivePermissionService(), new FakeSyncEngine()); } [Fact] public async Task Load_ChargeLesDocuments() { // Arrange Guid interventionId = Guid.NewGuid(); Guid docId = Guid.NewGuid(); _service.ItemsToReturn = [CreateItem(docId, "doc.pdf")]; // Act await _viewModel.LoadCommand.ExecuteAsync(interventionId); // Assert Assert.False(_viewModel.IsLoading); Assert.Null(_viewModel.ErrorMessage); Assert.Single(_viewModel.Documents); Assert.Equal(docId, _viewModel.Documents[0].DocumentGuid); Assert.True(_viewModel.HasDocuments); Assert.False(_viewModel.IsEmpty); } [Fact] public async Task Load_AucunDocument_IsEmpty() { // Arrange _service.ItemsToReturn = []; // Act await _viewModel.LoadCommand.ExecuteAsync(Guid.NewGuid()); // Assert Assert.Empty(_viewModel.Documents); Assert.False(_viewModel.HasDocuments); Assert.True(_viewModel.IsEmpty); } [Fact] public async Task Load_Erreur_MessageAffiché() { // Arrange _service.ThrowOnLoad = true; // Act await _viewModel.LoadCommand.ExecuteAsync(Guid.NewGuid()); // Assert Assert.False(_viewModel.IsLoading); Assert.NotNull(_viewModel.ErrorMessage); Assert.Empty(_viewModel.Documents); } [Fact] public async Task Download_AppelleLeService() { // Arrange Guid interventionId = Guid.NewGuid(); Guid docId = Guid.NewGuid(); var item = CreateItem(docId, "doc.pdf", isCached: false); _service.ItemsToReturn = [item]; await _viewModel.LoadCommand.ExecuteAsync(interventionId); // Act await _viewModel.DownloadCommand.ExecuteAsync(item); // Assert Assert.Equal(1, _service.DownloadCalls); } [Fact] public async Task Download_DejaEnCache_NeReappellePas() { // Arrange Guid interventionId = Guid.NewGuid(); Guid docId = Guid.NewGuid(); var item = CreateItem(docId, "doc.pdf", isCached: true); _service.ItemsToReturn = [item]; await _viewModel.LoadCommand.ExecuteAsync(interventionId); // Act await _viewModel.DownloadCommand.ExecuteAsync(item); // Assert : déjà en cache, aucun appel de téléchargement. Assert.Equal(0, _service.DownloadCalls); } [Fact] public async Task Open_AppelleLeService() { // Arrange Guid interventionId = Guid.NewGuid(); Guid docId = Guid.NewGuid(); var item = CreateItem(docId, "doc.pdf", isCached: true); _service.ItemsToReturn = [item]; await _viewModel.LoadCommand.ExecuteAsync(interventionId); // Act await _viewModel.OpenCommand.ExecuteAsync(item); // Assert Assert.Equal(1, _service.OpenCalls); Assert.Null(_viewModel.ErrorMessage); } [Fact] public async Task Open_NonDisponible_MessageErreur() { // Arrange Guid interventionId = Guid.NewGuid(); Guid docId = Guid.NewGuid(); var item = CreateItem(docId, "doc.pdf", isCached: false); _service.ItemsToReturn = [item]; _service.OpenResult = false; await _viewModel.LoadCommand.ExecuteAsync(interventionId); // Act await _viewModel.OpenCommand.ExecuteAsync(item); // Assert Assert.Equal(1, _service.OpenCalls); Assert.NotNull(_viewModel.ErrorMessage); } [Fact] public async Task AddPhoto_AjouteALaListe() { // Arrange Guid interventionId = Guid.NewGuid(); var photo = CreateItem(Guid.NewGuid(), "photo.jpg", isPhoto: true, isCached: true); _service.PhotosBatchToReturn = new AddPhotosResult { Added = [photo] }; await _viewModel.LoadCommand.ExecuteAsync(interventionId); // Act await _viewModel.AddPhotoCommand.ExecuteAsync(null); // Assert Assert.Contains(_viewModel.Documents, d => d.DocumentGuid == photo.DocumentGuid); Assert.True(_viewModel.HasDocuments); } [Fact] public async Task AddDocument_AjouteALaListe() { // Arrange Guid interventionId = Guid.NewGuid(); var doc = CreateItem(Guid.NewGuid(), "doc.pdf", isCached: true); _service.DocumentToReturn = doc; await _viewModel.LoadCommand.ExecuteAsync(interventionId); // Act await _viewModel.AddDocumentCommand.ExecuteAsync(null); // Assert Assert.Contains(_viewModel.Documents, d => d.DocumentGuid == doc.DocumentGuid); Assert.True(_viewModel.HasDocuments); } [Fact] public async Task AddPhoto_Annule_NeRienAjoute() { // Arrange : sélection multiple annulée → lot vide (Added vide, 0 échec). Guid interventionId = Guid.NewGuid(); _service.PhotosBatchToReturn = new AddPhotosResult(); await _viewModel.LoadCommand.ExecuteAsync(interventionId); // Act await _viewModel.AddPhotoCommand.ExecuteAsync(null); // Assert Assert.Empty(_viewModel.Documents); Assert.Null(_viewModel.ErrorMessage); } [Fact] public async Task AddPhoto_Multiple_ToutesAjoutees() { // Arrange : 3 photos sélectionnées. Guid interventionId = Guid.NewGuid(); var p1 = CreateItem(Guid.NewGuid(), "p1.jpg", isPhoto: true, isCached: true); var p2 = CreateItem(Guid.NewGuid(), "p2.jpg", isPhoto: true, isCached: true); var p3 = CreateItem(Guid.NewGuid(), "p3.jpg", isPhoto: true, isCached: true); _service.PhotosBatchToReturn = new AddPhotosResult { Added = [p1, p2, p3] }; await _viewModel.LoadCommand.ExecuteAsync(interventionId); // Act await _viewModel.AddPhotoCommand.ExecuteAsync(null); // Assert Assert.Equal(3, _viewModel.Documents.Count); Assert.Null(_viewModel.ErrorMessage); } [Fact] public async Task AddPhoto_Partiel_MessageErreurEtSuccesConserves() { // Arrange : 2 succès + 1 échec. Guid interventionId = Guid.NewGuid(); var p1 = CreateItem(Guid.NewGuid(), "p1.jpg", isPhoto: true, isCached: true); var p2 = CreateItem(Guid.NewGuid(), "p2.jpg", isPhoto: true, isCached: true); _service.PhotosBatchToReturn = new AddPhotosResult { Added = [p1, p2], FailedCount = 1 }; await _viewModel.LoadCommand.ExecuteAsync(interventionId); // Act await _viewModel.AddPhotoCommand.ExecuteAsync(null); // Assert : les 2 succès sont conservés + message utilisateur cohérent. Assert.Equal(2, _viewModel.Documents.Count); Assert.NotNull(_viewModel.ErrorMessage); Assert.Contains("2 images ajoutées", _viewModel.ErrorMessage); Assert.Contains("1 image n'a pas pu être ajoutée", _viewModel.ErrorMessage); } [Fact] public async Task AddPhotoFromCamera_Annule_AucunAjout() { // Arrange Guid interventionId = Guid.NewGuid(); _service.CameraPhotoToReturn = null; await _viewModel.LoadCommand.ExecuteAsync(interventionId); // Act await _viewModel.AddPhotoFromCameraCommand.ExecuteAsync(null); // Assert : aucun document, aucune erreur. Assert.Empty(_viewModel.Documents); Assert.Null(_viewModel.ErrorMessage); } [Fact] public async Task AddPhotoFromCamera_Erreur_MessageAffiché() { // Arrange : échec du launcher caméra → exception contrôlée. Guid interventionId = Guid.NewGuid(); _service.CameraExceptionToThrow = new PhotoCaptureException("Caméra indisponible."); await _viewModel.LoadCommand.ExecuteAsync(interventionId); // Act await _viewModel.AddPhotoFromCameraCommand.ExecuteAsync(null); // Assert : l'application ne plante pas, message utilisateur affiché. Assert.Empty(_viewModel.Documents); Assert.Equal("Caméra indisponible.", _viewModel.ErrorMessage); } [Fact] public void Back_RetourneALaFicheIntervention() { // Arrange Guid interventionId = Guid.NewGuid(); _viewModel.InterventionGuid = interventionId; // Act _viewModel.BackCommand.Execute(null); // Assert Assert.Equal(AppPage.InterventionDetail, _navigation.CurrentPage); Assert.Equal(interventionId, _navigation.CurrentParameter); } }