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;
namespace Omika.Tests.Mobile.Local;
///
/// Tests du service de lecture des clients depuis les données locales SQLite.
/// Relations réellement démontrables localement :
///
/// - Client → Sites : liste Sites embarquée dans le DTO Client.
/// - Client → Contacts : liste Contacts embarquée dans le DTO Client.
/// - Client → Dossiers : DossierDetailsDto.ClientId == ClientGuid.
/// - Client → Interventions : InterventionDetailsDto.ClientId == ClientGuid.
///
/// Aucune relation n'est déduite par commodité. Lecture seule : aucune écriture
/// ni Outbox n'est créée.
///
public class ClientLocalServiceTests : LocalTestBase
{
private readonly ClientLocalService _service;
public ClientLocalServiceTests()
{
_service = new ClientLocalService(ClientRepository, DossierRepository, ContextInterventionCacheRepository, ClientStateRepository, ApiClient);
}
private static string Serialize(T value) => JsonSerializer.Serialize(value);
private static LocalClient CreateClient(
Guid id,
string nom,
string? code = null,
string? ville = null,
string? statut = null,
string? telephone = null,
string? email = null,
IReadOnlyList? sites = null,
IReadOnlyList? contacts = null)
{
var dto = new ClientDetailsDto
{
Id = id.ToString(),
Nom = nom,
Code = code,
Statut = statut is null ? null : new ReferenceValueDto { Label = statut },
Coordonnees = telephone is null && email is null
? null
: new ContactDetailsDto { Phone = telephone, Email = email },
AdressePrincipale = ville is null
? null
: new AdresseDto { AdressePostale = new AddressDto { City = ville } },
Sites = sites ?? [],
Contacts = contacts ?? []
};
return new LocalClient
{
Id = id,
PayloadJson = Serialize(dto),
UpdatedAtUtc = DateTimeOffset.UtcNow
};
}
private static LocalDossier CreateDossier(
Guid id,
Guid clientId,
string reference,
string titre,
DateTime? dateOuverture = null,
string? statut = null)
{
var dto = new DossierDetailsDto
{
Id = id.ToString(),
RefInterne = reference,
Titre = titre,
ClientId = clientId.ToString(),
DateOuverture = dateOuverture,
Statut = statut is null ? null : new ReferenceValueDto { Label = statut }
};
return new LocalDossier
{
Id = id,
ClientId = clientId,
PayloadJson = Serialize(dto),
UpdatedAtUtc = DateTimeOffset.UtcNow
};
}
private static LocalIntervention CreateIntervention(
Guid id,
Guid clientId,
string reference,
string titre,
DateTime? datePrevueDebut = null,
string? etat = null)
{
var dto = new InterventionDetailsDto
{
Id = id.ToString(),
Reference = reference,
Titre = titre,
ClientId = clientId.ToString(),
DatePrevueDebut = datePrevueDebut,
Etat = etat is null ? null : new ReferenceValueDto { Label = etat }
};
return new LocalIntervention
{
Id = id,
ClientId = clientId,
PayloadJson = Serialize(dto),
UpdatedAtUtc = DateTimeOffset.UtcNow
};
}
private async Task CacheClientInterventionsAsync(Guid clientId, params LocalIntervention[] interventions)
{
var details = interventions
.Select(i => JsonSerializer.Deserialize(i.PayloadJson)!)
.ToList();
await ContextInterventionCacheRepository.ReplaceAsync(InterventionContextTypes.Client, clientId, details);
}
// ===================== LISTE =====================
[Fact]
public async Task AucunClient_RetourneListeVide()
{
// Act
var result = await _service.GetClientsAsync();
// Assert
Assert.Empty(result);
}
[Fact]
public async Task UnClient_RetourneLeClient()
{
// Arrange
Guid clientId = Guid.NewGuid();
await ClientRepository.UpsertAsync(CreateClient(clientId, "Client Alpha", "C001"));
// Act
var result = await _service.GetClientsAsync();
// Assert
var item = Assert.Single(result);
Assert.Equal(clientId, item.ClientGuid);
Assert.Equal("Client Alpha", item.Nom);
Assert.Equal("C001", item.Code);
}
[Fact]
public async Task PlusieursClients_RetourneTous()
{
// Arrange
await ClientRepository.UpsertAsync(CreateClient(Guid.NewGuid(), "Alpha"));
await ClientRepository.UpsertAsync(CreateClient(Guid.NewGuid(), "Beta"));
// Act
var result = await _service.GetClientsAsync();
// Assert
Assert.Equal(2, result.Count);
}
[Fact]
public async Task Tri_DeterministeParNomPuisCode()
{
// Arrange
await ClientRepository.UpsertAsync(CreateClient(Guid.NewGuid(), "Beta", "C002"));
await ClientRepository.UpsertAsync(CreateClient(Guid.NewGuid(), "Alpha", "C001"));
await ClientRepository.UpsertAsync(CreateClient(Guid.NewGuid(), "Alpha", "C003"));
// Act
var result = await _service.GetClientsAsync();
// Assert : tri stable par nom (insensible à la casse) puis code.
Assert.Equal("Alpha", result[0].Nom);
Assert.Equal("C001", result[0].Code);
Assert.Equal("Alpha", result[1].Nom);
Assert.Equal("C003", result[1].Code);
Assert.Equal("Beta", result[2].Nom);
}
[Fact]
public async Task RechercheLocale_InsensibleALaCasse()
{
// Arrange
await ClientRepository.UpsertAsync(CreateClient(Guid.NewGuid(), "Alpha", "C001", "Paris"));
await ClientRepository.UpsertAsync(CreateClient(Guid.NewGuid(), "Beta", "C002", "Lyon"));
// Act : le service retourne tout ; la recherche est gérée par le ViewModel.
var result = await _service.GetClientsAsync();
// Assert
Assert.Equal(2, result.Count);
}
[Fact]
public async Task RechercheSansResultat_ListeVide()
{
// Arrange
await ClientRepository.UpsertAsync(CreateClient(Guid.NewGuid(), "Alpha"));
// Act : le service retourne tout ; la recherche est gérée par le ViewModel.
var result = await _service.GetClientsAsync();
// Assert
Assert.Single(result);
}
[Fact]
public async Task PayloadInvalide_EntreeIgnoree()
{
// Arrange
await ClientRepository.UpsertAsync(new LocalClient
{
Id = Guid.NewGuid(),
PayloadJson = "not-json",
UpdatedAtUtc = DateTimeOffset.UtcNow
});
await ClientRepository.UpsertAsync(CreateClient(Guid.NewGuid(), "Valide"));
// Act
var result = await _service.GetClientsAsync();
// 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 clientId = Guid.NewGuid();
await ClientRepository.UpsertAsync(CreateClient(clientId, "Alpha"));
// Act
var result = await _service.GetClientsAsync();
// Assert : l'identité publique (GUID) est conservée.
var item = Assert.Single(result);
Assert.Equal(clientId, item.ClientGuid);
}
[Fact]
public async Task FonctionneOffline_SansAppelApi()
{
// Arrange : aucune dépendance réseau, uniquement SQLite local.
await ClientRepository.UpsertAsync(CreateClient(Guid.NewGuid(), "Alpha"));
// Act
var result = await _service.GetClientsAsync();
// Assert
Assert.Single(result);
}
// ===================== DÉTAIL =====================
[Fact]
public async Task Detail_ClientTrouve()
{
// Arrange
Guid clientId = Guid.NewGuid();
await ClientRepository.UpsertAsync(CreateClient(clientId, "Client Alpha", "C001"));
// Act
var result = await _service.GetClientByIdAsync(clientId);
// Assert
Assert.NotNull(result);
Assert.Equal(clientId, result.ClientGuid);
Assert.Equal("Client Alpha", result.Nom);
Assert.Equal("C001", result.Code);
}
[Fact]
public async Task Detail_ClientInconnu_RetourneNull()
{
// Act
var result = await _service.GetClientByIdAsync(Guid.NewGuid());
// Assert
Assert.Null(result);
}
[Fact]
public async Task Detail_DonneesReellesChargees()
{
// Arrange
Guid clientId = Guid.NewGuid();
await ClientRepository.UpsertAsync(CreateClient(clientId, "Client Alpha", "C001", "Paris", "Actif", "0102030405", "a@b.fr"));
// Act
var result = await _service.GetClientByIdAsync(clientId);
// Assert
Assert.NotNull(result);
Assert.Equal("Client Alpha", result.Nom);
Assert.Equal("C001", result.Code);
Assert.Equal("Actif", result.Statut);
Assert.Equal("0102030405", result.Phone);
Assert.Equal("a@b.fr", result.Email);
Assert.True(result.HasPhone);
Assert.True(result.HasEmail);
Assert.True(result.HasAdresse);
}
[Fact]
public async Task Detail_ChampsOptionnelsAbsents()
{
// Arrange : client minimal sans coordonnées ni adresse.
Guid clientId = Guid.NewGuid();
await ClientRepository.UpsertAsync(CreateClient(clientId, "Client Alpha"));
// Act
var result = await _service.GetClientByIdAsync(clientId);
// Assert : champs optionnels absents restent nuls sans erreur.
Assert.NotNull(result);
Assert.Null(result.Phone);
Assert.Null(result.Email);
Assert.Null(result.AdressePrincipale);
Assert.False(result.HasPhone);
Assert.False(result.HasEmail);
Assert.False(result.HasAdresse);
}
[Fact]
public async Task Detail_AucuneRelation_NeProvoquePasDErreur()
{
// Arrange : client sans sites, contacts, dossiers ni interventions.
Guid clientId = Guid.NewGuid();
await ClientRepository.UpsertAsync(CreateClient(clientId, "Client Alpha"));
// Act
var result = await _service.GetClientByIdAsync(clientId);
// Assert : collections vides sans erreur.
Assert.NotNull(result);
Assert.Empty(result.Sites);
Assert.Empty(result.Contacts);
Assert.Empty(result.Dossiers);
Assert.Empty(result.Interventions);
}
[Fact]
public async Task Detail_IdentitePublique_Conservee()
{
// Arrange
Guid clientId = Guid.NewGuid();
await ClientRepository.UpsertAsync(CreateClient(clientId, "Alpha"));
// Act
var result = await _service.GetClientByIdAsync(clientId);
// Assert
Assert.NotNull(result);
Assert.Equal(clientId, result.ClientGuid);
}
// ===================== RELATION CLIENT → CONTACTS =====================
[Fact]
public async Task ClientA_RecoitUniquementSesContacts()
{
// Arrange : Client A embarque 2 contacts, Client B en embarque 1.
Guid clientA = Guid.NewGuid();
Guid clientB = Guid.NewGuid();
Guid contactA1 = Guid.NewGuid();
Guid contactA2 = Guid.NewGuid();
Guid contactB = Guid.NewGuid();
await ClientRepository.UpsertAsync(CreateClient(clientA, "Client A", contacts:
[
new ContactDto { Id = contactA1.ToString(), NomAffiche = "Contact A1" },
new ContactDto { Id = contactA2.ToString(), NomAffiche = "Contact A2" }
]));
await ClientRepository.UpsertAsync(CreateClient(clientB, "Client B", contacts:
[
new ContactDto { Id = contactB.ToString(), NomAffiche = "Contact B" }
]));
// Act
var resultA = await _service.GetClientByIdAsync(clientA);
// 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 ContactDuClientB_Exclu()
{
// Arrange
Guid clientA = Guid.NewGuid();
Guid clientB = Guid.NewGuid();
Guid contactB = Guid.NewGuid();
await ClientRepository.UpsertAsync(CreateClient(clientA, "Client A"));
await ClientRepository.UpsertAsync(CreateClient(clientB, "Client B", contacts:
[
new ContactDto { Id = contactB.ToString(), NomAffiche = "Contact B" }
]));
// Act
var resultA = await _service.GetClientByIdAsync(clientA);
// Assert : A n'a aucun contact de B.
Assert.NotNull(resultA);
Assert.Empty(resultA.Contacts);
}
[Fact]
public async Task ContactSansClient_Exclu()
{
// Arrange : un contact sans ClientId n'est pas rattaché à un client.
Guid clientA = Guid.NewGuid();
await ClientRepository.UpsertAsync(CreateClient(clientA, "Client A"));
// Act
var resultA = await _service.GetClientByIdAsync(clientA);
// Assert : aucun contact sans rattachement n'est ajouté.
Assert.NotNull(resultA);
Assert.Empty(resultA.Contacts);
}
// ===================== RELATION CLIENT → DOSSIERS =====================
[Fact]
public async Task ClientA_RecoitUniquementSesDossiers()
{
// Arrange
Guid clientA = Guid.NewGuid();
Guid clientB = Guid.NewGuid();
Guid dossierA = Guid.NewGuid();
Guid dossierB = Guid.NewGuid();
await ClientRepository.UpsertAsync(CreateClient(clientA, "Client A"));
await ClientRepository.UpsertAsync(CreateClient(clientB, "Client B"));
await DossierRepository.UpsertAsync(CreateDossier(dossierA, clientA, "DOS-A", "Dossier A"));
await DossierRepository.UpsertAsync(CreateDossier(dossierB, clientB, "DOS-B", "Dossier B"));
// Act
var resultA = await _service.GetClientByIdAsync(clientA);
// Assert : A reçoit uniquement ses dossiers.
Assert.NotNull(resultA);
var dossier = Assert.Single(resultA.Dossiers);
Assert.Equal(dossierA, dossier.DossierGuid);
Assert.Equal("DOS-A", dossier.RefInterne);
}
[Fact]
public async Task DossierDuClientB_Exclu()
{
// Arrange
Guid clientA = Guid.NewGuid();
Guid clientB = Guid.NewGuid();
await ClientRepository.UpsertAsync(CreateClient(clientA, "Client A"));
await ClientRepository.UpsertAsync(CreateClient(clientB, "Client B"));
await DossierRepository.UpsertAsync(CreateDossier(Guid.NewGuid(), clientB, "DOS-B", "Dossier B"));
// Act
var resultA = await _service.GetClientByIdAsync(clientA);
// Assert : A n'a aucun dossier de B.
Assert.NotNull(resultA);
Assert.Empty(resultA.Dossiers);
}
// ===================== RELATION CLIENT → INTERVENTIONS =====================
[Fact]
public async Task ClientA_RecoitUniquementSesInterventions()
{
// Arrange
Guid clientA = Guid.NewGuid();
Guid clientB = Guid.NewGuid();
Guid interventionA = Guid.NewGuid();
Guid interventionB = Guid.NewGuid();
await ClientRepository.UpsertAsync(CreateClient(clientA, "Client A"));
await ClientRepository.UpsertAsync(CreateClient(clientB, "Client B"));
await CacheClientInterventionsAsync(
clientA,
CreateIntervention(interventionA, clientA, "INT-A", "Intervention A"),
CreateIntervention(interventionB, clientB, "INT-B", "Intervention B"));
// Act
var resultA = await _service.GetClientByIdAsync(clientA);
// Assert : A reçoit uniquement ses interventions.
Assert.NotNull(resultA);
var intervention = Assert.Single(resultA.Interventions);
Assert.Equal(interventionA, intervention.InterventionGuid);
Assert.Equal("INT-A", intervention.Reference);
}
[Fact]
public async Task InterventionDuClientB_Exclue()
{
// Arrange
Guid clientA = Guid.NewGuid();
Guid clientB = Guid.NewGuid();
await ClientRepository.UpsertAsync(CreateClient(clientA, "Client A"));
await ClientRepository.UpsertAsync(CreateClient(clientB, "Client B"));
await CacheClientInterventionsAsync(
clientA,
CreateIntervention(Guid.NewGuid(), clientB, "INT-B", "Intervention B"));
// Act
var resultA = await _service.GetClientByIdAsync(clientA);
// Assert : A n'a aucune intervention de B.
Assert.NotNull(resultA);
Assert.Empty(resultA.Interventions);
}
// ===================== RELATION CLIENT → SITES =====================
[Fact]
public async Task ClientA_RecoitSesSitesEmbarques()
{
// Arrange : les sites sont embarqués dans le DTO Client.
Guid clientA = Guid.NewGuid();
Guid siteA = Guid.NewGuid();
await ClientRepository.UpsertAsync(CreateClient(clientA, "Client A", sites:
[
new SiteDto { Id = siteA.ToString(), Nom = "Site A", Code = "SITE-A" }
]));
// Act
var resultA = await _service.GetClientByIdAsync(clientA);
// Assert
Assert.NotNull(resultA);
var site = Assert.Single(resultA.Sites);
Assert.Equal(siteA, site.SiteGuid);
Assert.Equal("Site A", site.Nom);
Assert.Equal("SITE-A", site.Code);
}
[Fact]
public async Task ClientSansSites_ListeVide()
{
// Arrange
Guid clientA = Guid.NewGuid();
await ClientRepository.UpsertAsync(CreateClient(clientA, "Client A"));
// Act
var resultA = await _service.GetClientByIdAsync(clientA);
// Assert
Assert.NotNull(resultA);
Assert.Empty(resultA.Sites);
}
}