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);
}
}