using System.Text.Json; using Omika.App.Local.Models; using Omika.App.Local.Infrastructure; using Omika.App.Local.Services; using Omika.Core.Contracts.Clients; using Omika.Core.Contracts.Common; using Omika.Core.Contracts.Interventions; using Omika.Core.Contracts.Sites; namespace Omika.Tests.Mobile.Local; /// /// Tests du service de lecture des sites / lieux depuis les données locales /// SQLite. Relations réellement démontrables localement : /// /// Site → Client : relation explicite SiteDetailsDto.ClientId. /// Site → Contacts : liste explicite SiteDetailsDto.ContactIds, résolue vers les Personnes/Contacts locales. /// Site → Interventions : relation directe InterventionDetailsDto.SiteId. /// /// Aucune relation n'est déduite par commodité (nom, adresse, transitivité). /// Lecture seule : aucune écriture ni Outbox n'est créée. /// public class SiteLocalServiceTests : LocalTestBase { private readonly SiteLocalService _service; public SiteLocalServiceTests() { _service = new SiteLocalService( SiteRepository, ClientRepository, PersonneRepository, RendezVousRepository, InterventionRepository, ContextInterventionCacheRepository, SiteStateRepository, ApiClient); } private static string Serialize(T value) => JsonSerializer.Serialize(value); private static LocalSite CreateSite( Guid id, string nom, string? code = null, string? ville = null, string? type = null, Guid? clientId = null, IReadOnlyList? contactIds = null) { var dto = new SiteDetailsDto { Id = id.ToString(), Nom = nom, Code = code, Type = type is null ? null : new ReferenceValueDto { Label = type }, ClientId = clientId?.ToString(), Adresse = ville is null ? null : new AddressDto { City = ville, OneLine = ville }, ContactIds = contactIds ?? [] }; return new LocalSite { Id = id, ClientId = clientId, PayloadJson = Serialize(dto), UpdatedAtUtc = DateTimeOffset.UtcNow }; } private static LocalClient CreateClient(Guid id, string nom, string? code = null) { var dto = new ClientDetailsDto { Id = id.ToString(), Nom = nom, Code = code }; return new LocalClient { Id = id, PayloadJson = Serialize(dto), UpdatedAtUtc = DateTimeOffset.UtcNow }; } private static LocalPersonne CreatePersonne(Guid id, string nomAffiche, string? email = null) { var dto = new ContactDto { Id = id.ToString(), NomAffiche = nomAffiche, Coordonnees = email is null ? null : new ContactDetailsDto { Email = email } }; return new LocalPersonne { Id = id, PayloadJson = Serialize(dto), UpdatedAtUtc = DateTimeOffset.UtcNow }; } private static LocalIntervention CreateIntervention( Guid id, Guid siteId, string reference, string titre, DateTime? datePrevueDebut = null, string? etat = null) { var dto = new InterventionDetailsDto { Id = id.ToString(), Reference = reference, Titre = titre, SiteId = siteId.ToString(), DatePrevueDebut = datePrevueDebut, Etat = etat is null ? null : new ReferenceValueDto { Label = etat } }; return new LocalIntervention { Id = id, SiteId = siteId, PayloadJson = Serialize(dto), UpdatedAtUtc = DateTimeOffset.UtcNow }; } private async Task CacheSiteInterventionsAsync(Guid siteId, params LocalIntervention[] interventions) { var details = interventions .Select(i => JsonSerializer.Deserialize(i.PayloadJson)!) .ToList(); await ContextInterventionCacheRepository.ReplaceAsync(InterventionContextTypes.Site, siteId, details); } // ===================== LISTE ===================== [Fact] public async Task AucunSite_RetourneListeVide() { // Act var result = await _service.GetSitesAsync(); // Assert Assert.Empty(result); } [Fact] public async Task UnSite_RetourneLeSite() { // Arrange Guid siteId = Guid.NewGuid(); await SiteRepository.UpsertAsync(CreateSite(siteId, "Site Alpha", "S001")); // Act var result = await _service.GetSitesAsync(); // Assert var item = Assert.Single(result); Assert.Equal(siteId, item.SiteGuid); Assert.Equal("Site Alpha", item.Nom); Assert.Equal("S001", item.Code); } [Fact] public async Task PlusieursSites_RetourneTous() { // Arrange await SiteRepository.UpsertAsync(CreateSite(Guid.NewGuid(), "Alpha")); await SiteRepository.UpsertAsync(CreateSite(Guid.NewGuid(), "Beta")); // Act var result = await _service.GetSitesAsync(); // Assert Assert.Equal(2, result.Count); } [Fact] public async Task Tri_DeterministeParNomPuisCode() { // Arrange await SiteRepository.UpsertAsync(CreateSite(Guid.NewGuid(), "Beta", "S002")); await SiteRepository.UpsertAsync(CreateSite(Guid.NewGuid(), "Alpha", "S001")); await SiteRepository.UpsertAsync(CreateSite(Guid.NewGuid(), "Alpha", "S003")); // Act var result = await _service.GetSitesAsync(); // Assert : tri stable par nom (insensible à la casse) puis code. Assert.Equal("Alpha", result[0].Nom); Assert.Equal("S001", result[0].Code); Assert.Equal("Alpha", result[1].Nom); Assert.Equal("S003", result[1].Code); Assert.Equal("Beta", result[2].Nom); } [Fact] public async Task RechercheLocale_InsensibleALaCasse() { // Arrange await SiteRepository.UpsertAsync(CreateSite(Guid.NewGuid(), "Alpha", "S001", "Paris")); await SiteRepository.UpsertAsync(CreateSite(Guid.NewGuid(), "Beta", "S002", "Lyon")); // Act : le service retourne tout ; la recherche est gérée par le ViewModel. var result = await _service.GetSitesAsync(); // Assert Assert.Equal(2, result.Count); } [Fact] public async Task RechercheSansResultat_ListeVide() { // Arrange await SiteRepository.UpsertAsync(CreateSite(Guid.NewGuid(), "Alpha")); // Act : le service retourne tout ; la recherche est gérée par le ViewModel. var result = await _service.GetSitesAsync(); // Assert Assert.Single(result); } [Fact] public async Task PayloadInvalide_EntreeIgnoree() { // Arrange await SiteRepository.UpsertAsync(new LocalSite { Id = Guid.NewGuid(), PayloadJson = "not-json", UpdatedAtUtc = DateTimeOffset.UtcNow }); await SiteRepository.UpsertAsync(CreateSite(Guid.NewGuid(), "Valide")); // Act var result = await _service.GetSitesAsync(); // Assert : l'entrée invalide est ignorée proprement. var item = Assert.Single(result); Assert.Equal("Valide", item.Nom); } [Fact] public async Task IdentitePublique_Conservee() { // Arrange Guid siteId = Guid.NewGuid(); await SiteRepository.UpsertAsync(CreateSite(siteId, "Alpha")); // Act var result = await _service.GetSitesAsync(); // Assert : l'identité publique (GUID) est conservée. var item = Assert.Single(result); Assert.Equal(siteId, item.SiteGuid); } [Fact] public async Task FonctionneOffline_SansAppelApi() { // Arrange : aucune dépendance réseau, uniquement SQLite local. await SiteRepository.UpsertAsync(CreateSite(Guid.NewGuid(), "Alpha")); // Act var result = await _service.GetSitesAsync(); // Assert Assert.Single(result); } // ===================== DÉTAIL ===================== [Fact] public async Task Detail_SiteTrouve() { // Arrange Guid siteId = Guid.NewGuid(); await SiteRepository.UpsertAsync(CreateSite(siteId, "Site Alpha", "S001")); // Act var result = await _service.GetSiteByIdAsync(siteId); // Assert Assert.NotNull(result); Assert.Equal(siteId, result.SiteGuid); Assert.Equal("Site Alpha", result.Nom); Assert.Equal("S001", result.Code); } [Fact] public async Task Detail_SiteInconnu_RetourneNull() { // Act var result = await _service.GetSiteByIdAsync(Guid.NewGuid()); // Assert Assert.Null(result); } [Fact] public async Task Detail_DonneesReellesChargees() { // Arrange Guid siteId = Guid.NewGuid(); await SiteRepository.UpsertAsync(CreateSite(siteId, "Site Alpha", "S001", "Paris", "Entrepôt")); // Act var result = await _service.GetSiteByIdAsync(siteId); // Assert Assert.NotNull(result); Assert.Equal("Site Alpha", result.Nom); Assert.Equal("S001", result.Code); Assert.Equal("Entrepôt", result.Type); Assert.True(result.HasAdresse); Assert.Equal("Paris", result.Adresse!.City); } [Fact] public async Task Detail_ChampsOptionnelsAbsents() { // Arrange : site minimal sans adresse ni type. Guid siteId = Guid.NewGuid(); await SiteRepository.UpsertAsync(CreateSite(siteId, "Site Alpha")); // Act var result = await _service.GetSiteByIdAsync(siteId); // Assert : champs optionnels absents restent nuls sans erreur. Assert.NotNull(result); Assert.Null(result.Type); Assert.Null(result.Adresse); Assert.False(result.HasAdresse); } [Fact] public async Task Detail_AucuneRelation_NeProvoquePasDErreur() { // Arrange : site sans client, contacts ni interventions. Guid siteId = Guid.NewGuid(); await SiteRepository.UpsertAsync(CreateSite(siteId, "Site Alpha")); // Act var result = await _service.GetSiteByIdAsync(siteId); // Assert : collections vides sans erreur. Assert.NotNull(result); Assert.Empty(result.Clients); Assert.Empty(result.Contacts); Assert.Empty(result.Interventions); } [Fact] public async Task Detail_IdentitePublique_Conservee() { // Arrange Guid siteId = Guid.NewGuid(); await SiteRepository.UpsertAsync(CreateSite(siteId, "Alpha")); // Act var result = await _service.GetSiteByIdAsync(siteId); // Assert Assert.NotNull(result); Assert.Equal(siteId, result.SiteGuid); } // ===================== RELATION SITE → CONTACTS ===================== [Fact] public async Task SiteA_RecoitUniquementSesContacts() { // Arrange : Site A embarque 2 contacts, Site B en embarque 1. Guid siteA = Guid.NewGuid(); Guid siteB = Guid.NewGuid(); Guid contactA1 = Guid.NewGuid(); Guid contactA2 = Guid.NewGuid(); Guid contactB = Guid.NewGuid(); await SiteRepository.UpsertAsync(CreateSite(siteA, "Site A", contactIds: [contactA1.ToString(), contactA2.ToString()])); await SiteRepository.UpsertAsync(CreateSite(siteB, "Site B", contactIds: [contactB.ToString()])); await PersonneRepository.UpsertAsync(CreatePersonne(contactA1, "Contact A1")); await PersonneRepository.UpsertAsync(CreatePersonne(contactA2, "Contact A2")); await PersonneRepository.UpsertAsync(CreatePersonne(contactB, "Contact B")); // Act var resultA = await _service.GetSiteByIdAsync(siteA); // Assert : A reçoit uniquement ses contacts. Assert.NotNull(resultA); Assert.Equal(2, resultA.Contacts.Count); Assert.Contains(resultA.Contacts, c => c.ContactGuid == contactA1); Assert.Contains(resultA.Contacts, c => c.ContactGuid == contactA2); Assert.DoesNotContain(resultA.Contacts, c => c.ContactGuid == contactB); } [Fact] public async Task ContactDuMemeClientMaisAutreSite_Exclu() { // Arrange : Contact lié au Site B uniquement, même si le Client est partagé. Guid siteA = Guid.NewGuid(); Guid siteB = Guid.NewGuid(); Guid contactB = Guid.NewGuid(); await SiteRepository.UpsertAsync(CreateSite(siteA, "Site A")); await SiteRepository.UpsertAsync(CreateSite(siteB, "Site B", contactIds: [contactB.ToString()])); await PersonneRepository.UpsertAsync(CreatePersonne(contactB, "Contact B")); // Act var resultA = await _service.GetSiteByIdAsync(siteA); // Assert : A n'a aucun contact de B. Assert.NotNull(resultA); Assert.Empty(resultA.Contacts); } [Fact] public async Task ContactNonRelie_Exclu() { // Arrange : un contact sans ContactIds sur le site. Guid siteA = Guid.NewGuid(); Guid contact = Guid.NewGuid(); await SiteRepository.UpsertAsync(CreateSite(siteA, "Site A")); await PersonneRepository.UpsertAsync(CreatePersonne(contact, "Contact Libre")); // Act var resultA = await _service.GetSiteByIdAsync(siteA); // Assert : aucun contact non relié n'est ajouté. Assert.NotNull(resultA); Assert.Empty(resultA.Contacts); } [Fact] public async Task ContactGuid_PublicConserve() { // Arrange Guid siteA = Guid.NewGuid(); Guid contactA = Guid.NewGuid(); await SiteRepository.UpsertAsync(CreateSite(siteA, "Site A", contactIds: [contactA.ToString()])); await PersonneRepository.UpsertAsync(CreatePersonne(contactA, "Contact A")); // Act var resultA = await _service.GetSiteByIdAsync(siteA); // Assert : le GUID public du contact est conservé. Assert.NotNull(resultA); var contact = Assert.Single(resultA.Contacts); Assert.Equal(contactA, contact.ContactGuid); } // ===================== RELATION SITE → INTERVENTIONS ===================== [Fact] public async Task InterventionSiteA_Incluse() { // Arrange Guid siteA = Guid.NewGuid(); Guid interventionA = Guid.NewGuid(); await SiteRepository.UpsertAsync(CreateSite(siteA, "Site A")); await CacheSiteInterventionsAsync(siteA, CreateIntervention(interventionA, siteA, "INT-A", "Intervention A")); // Act var resultA = await _service.GetSiteByIdAsync(siteA); // Assert Assert.NotNull(resultA); var intervention = Assert.Single(resultA.Interventions); Assert.Equal(interventionA, intervention.InterventionGuid); Assert.Equal("INT-A", intervention.Reference); } [Fact] public async Task InterventionSiteB_Exclue() { // Arrange Guid siteA = Guid.NewGuid(); Guid siteB = Guid.NewGuid(); await SiteRepository.UpsertAsync(CreateSite(siteA, "Site A")); await SiteRepository.UpsertAsync(CreateSite(siteB, "Site B")); await CacheSiteInterventionsAsync(siteA, CreateIntervention(Guid.NewGuid(), siteB, "INT-B", "Intervention B")); // Act var resultA = await _service.GetSiteByIdAsync(siteA); // Assert : A n'a aucune intervention de B. Assert.NotNull(resultA); Assert.Empty(resultA.Interventions); } [Fact] public async Task InterventionSansSite_Exclue() { // Arrange : intervention sans SiteId. Guid siteA = Guid.NewGuid(); await SiteRepository.UpsertAsync(CreateSite(siteA, "Site A")); await CacheSiteInterventionsAsync(siteA, new LocalIntervention { Id = Guid.NewGuid(), PayloadJson = Serialize(new InterventionDetailsDto { Id = Guid.NewGuid().ToString(), Reference = "INT-X", Titre = "Intervention X" }), UpdatedAtUtc = DateTimeOffset.UtcNow }); // Act var resultA = await _service.GetSiteByIdAsync(siteA); // Assert : aucune intervention sans site n'est ajoutée. Assert.NotNull(resultA); Assert.Empty(resultA.Interventions); } [Fact] public async Task AucuneDeductionViaClient() { // Arrange : intervention liée au Client A mais pas au Site A. Guid clientA = Guid.NewGuid(); Guid siteA = Guid.NewGuid(); await ClientRepository.UpsertAsync(CreateClient(clientA, "Client A")); await SiteRepository.UpsertAsync(CreateSite(siteA, "Site A", clientId: clientA)); await CacheSiteInterventionsAsync(siteA, new LocalIntervention { Id = Guid.NewGuid(), ClientId = clientA, PayloadJson = Serialize(new InterventionDetailsDto { Id = Guid.NewGuid().ToString(), Reference = "INT-A", Titre = "Intervention A", ClientId = clientA.ToString() }), UpdatedAtUtc = DateTimeOffset.UtcNow }); // Act var resultA = await _service.GetSiteByIdAsync(siteA); // Assert : l'intervention n'est pas déduite via le Client. Assert.NotNull(resultA); Assert.Empty(resultA.Interventions); } [Fact] public async Task InterventionGuid_PublicConserve() { // Arrange Guid siteA = Guid.NewGuid(); Guid interventionA = Guid.NewGuid(); await SiteRepository.UpsertAsync(CreateSite(siteA, "Site A")); await CacheSiteInterventionsAsync(siteA, CreateIntervention(interventionA, siteA, "INT-A", "Intervention A")); // Act var resultA = await _service.GetSiteByIdAsync(siteA); // Assert : le GUID public de l'intervention est conservé. Assert.NotNull(resultA); var intervention = Assert.Single(resultA.Interventions); Assert.Equal(interventionA, intervention.InterventionGuid); } }