using System.Text.Json;
using Omika.App.Local.Models;
using Omika.App.Local.Infrastructure;
using Omika.App.Local.Services;
using Omika.Core.Contracts.Common;
using Omika.Core.Contracts.Interventions;
using Omika.Core.Contracts.Organisation;
using Omika.Core.Contracts.Planning;
namespace Omika.Tests.Mobile.Local;
///
/// Tests du service de lecture des collaborateurs depuis les données locales
/// SQLite. Relations réellement démontrables localement :
///
/// - Collaborateur → RendezVous : RendezVousDetailsDto.Collaborateurs
/// contient une affectation dont CollaborateurId correspond au GUID public.
/// - Collaborateur → Interventions : InterventionDetailsDto.CollaborateurIds
/// contient le GUID public du collaborateur.
///
/// Aucune relation n'est déduite par transitivité ni par commodité. Lecture
/// seule : aucune écriture ni Outbox n'est créée.
///
public class CollaborateurLocalServiceTests : LocalTestBase
{
private readonly CollaborateurLocalService _service;
public CollaborateurLocalServiceTests()
{
_service = new CollaborateurLocalService(CollaborateurRepository, RendezVousRepository, ContextInterventionCacheRepository, ApiClient, SessionService);
}
private static string Serialize(T value) => JsonSerializer.Serialize(value);
private static LocalCollaborateur CreateCollaborateur(
Guid id,
string? nom = null,
string? prenom = null,
string? nomAffiche = null,
string? code = null,
string? statut = null,
string? telephone = null,
string? mobile = null,
string? email = null,
bool actif = true,
bool estArchive = false)
{
var dto = new CollaborateurDetailsDto
{
Id = id.ToString(),
Nom = nom,
Prenom = prenom,
NomAffiche = nomAffiche,
Code = code,
Statut = statut is null ? null : new ReferenceValueDto { Label = statut },
Coordonnees = telephone is null && mobile is null && email is null
? null
: new ContactDetailsDto { Phone = telephone, Mobile = mobile, Email = email },
Actif = actif,
EstArchive = estArchive
};
return new LocalCollaborateur
{
Id = id,
PayloadJson = Serialize(dto),
UpdatedAtUtc = DateTimeOffset.UtcNow
};
}
private static LocalRendezVous CreateRendezVous(
Guid id,
Guid collaborateurId,
DateTime debut,
DateTime fin,
string titre = "Rendez-vous",
string? statut = "Planifié")
{
var dto = new RendezVousDetailsDto
{
Id = id.ToString(),
Titre = titre,
Debut = debut,
Fin = fin,
Statut = statut is null ? null : new ReferenceValueDto { Label = statut },
Collaborateurs =
[
new AffectationCollaborateurDto { CollaborateurId = collaborateurId.ToString() }
]
};
return new LocalRendezVous
{
Id = id,
PayloadJson = Serialize(dto),
UpdatedAtUtc = DateTimeOffset.UtcNow
};
}
private static LocalRendezVous CreateRendezVousSansCollaborateur(
Guid id,
DateTime debut,
DateTime fin,
string titre = "Rendez-vous")
{
var dto = new RendezVousDetailsDto
{
Id = id.ToString(),
Titre = titre,
Debut = debut,
Fin = fin,
Collaborateurs = []
};
return new LocalRendezVous
{
Id = id,
PayloadJson = Serialize(dto),
UpdatedAtUtc = DateTimeOffset.UtcNow
};
}
private static LocalIntervention CreateIntervention(
Guid id,
Guid collaborateurId,
string reference,
string titre,
DateTime? datePrevueDebut = null,
string? etat = null)
{
var dto = new InterventionDetailsDto
{
Id = id.ToString(),
Reference = reference,
Titre = titre,
DatePrevueDebut = datePrevueDebut,
Etat = etat is null ? null : new ReferenceValueDto { Label = etat },
CollaborateurIds = [collaborateurId.ToString()]
};
return new LocalIntervention
{
Id = id,
PayloadJson = Serialize(dto),
UpdatedAtUtc = DateTimeOffset.UtcNow
};
}
private static LocalIntervention CreateInterventionSansCollaborateur(
Guid id,
string reference,
string titre)
{
var dto = new InterventionDetailsDto
{
Id = id.ToString(),
Reference = reference,
Titre = titre,
CollaborateurIds = []
};
return new LocalIntervention
{
Id = id,
PayloadJson = Serialize(dto),
UpdatedAtUtc = DateTimeOffset.UtcNow
};
}
private async Task CacheCollaborateurInterventionsAsync(Guid collaborateurId, params LocalIntervention[] interventions)
{
var details = interventions
.Select(i => JsonSerializer.Deserialize(i.PayloadJson)!)
.ToList();
await ContextInterventionCacheRepository.ReplaceAsync(
InterventionContextTypes.Collaborateur,
collaborateurId,
details);
}
// ===================== LISTE =====================
[Fact]
public async Task AucunCollaborateur_RetourneListeVide()
{
// Act
var result = await _service.GetCollaborateursAsync();
// Assert
Assert.Empty(result);
}
[Fact]
public async Task UnCollaborateur_RetourneLeCollaborateur()
{
// Arrange
Guid collaborateurId = Guid.NewGuid();
await CollaborateurRepository.UpsertAsync(CreateCollaborateur(collaborateurId, "Dupont", "Jean", code: "C001"));
// Act
var result = await _service.GetCollaborateursAsync();
// Assert
var item = Assert.Single(result);
Assert.Equal(collaborateurId, item.CollaborateurGuid);
Assert.Equal("Jean Dupont", item.DisplayName);
Assert.Equal("C001", item.Code);
}
[Fact]
public async Task PlusieursCollaborateurs_RetourneTous()
{
// Arrange
await CollaborateurRepository.UpsertAsync(CreateCollaborateur(Guid.NewGuid(), "Dupont", "Jean"));
await CollaborateurRepository.UpsertAsync(CreateCollaborateur(Guid.NewGuid(), "Martin", "Paul"));
// Act
var result = await _service.GetCollaborateursAsync();
// Assert
Assert.Equal(2, result.Count);
}
[Fact]
public async Task Tri_DeterministeParNomAffichePuisCode()
{
// Arrange
await CollaborateurRepository.UpsertAsync(CreateCollaborateur(Guid.NewGuid(), "Beta", "B", code: "C002"));
await CollaborateurRepository.UpsertAsync(CreateCollaborateur(Guid.NewGuid(), "Alpha", "A", code: "C001"));
await CollaborateurRepository.UpsertAsync(CreateCollaborateur(Guid.NewGuid(), "Alpha", "A", code: "C003"));
// Act
var result = await _service.GetCollaborateursAsync();
// Assert : tri stable par nom affiché (insensible à la casse) puis code.
Assert.Equal("A Alpha", result[0].DisplayName);
Assert.Equal("C001", result[0].Code);
Assert.Equal("A Alpha", result[1].DisplayName);
Assert.Equal("C003", result[1].Code);
Assert.Equal("B Beta", result[2].DisplayName);
}
[Fact]
public async Task RechercheLocale_InsensibleALaCasse()
{
// Arrange
await CollaborateurRepository.UpsertAsync(CreateCollaborateur(Guid.NewGuid(), "Dupont", "Jean", code: "C001"));
await CollaborateurRepository.UpsertAsync(CreateCollaborateur(Guid.NewGuid(), "Martin", "Paul", code: "C002"));
// Act : le service retourne tout ; la recherche est gérée par le ViewModel.
var result = await _service.GetCollaborateursAsync();
// Assert
Assert.Equal(2, result.Count);
}
[Fact]
public async Task RechercheSansResultat_ListeVide()
{
// Arrange
await CollaborateurRepository.UpsertAsync(CreateCollaborateur(Guid.NewGuid(), "Dupont", "Jean"));
// Act : le service retourne tout ; la recherche est gérée par le ViewModel.
var result = await _service.GetCollaborateursAsync();
// Assert
Assert.Single(result);
}
[Fact]
public async Task PayloadInvalide_EntreeIgnoree()
{
// Arrange
await CollaborateurRepository.UpsertAsync(new LocalCollaborateur
{
Id = Guid.NewGuid(),
PayloadJson = "not-json",
UpdatedAtUtc = DateTimeOffset.UtcNow
});
await CollaborateurRepository.UpsertAsync(CreateCollaborateur(Guid.NewGuid(), "Dupont", "Jean"));
// Act
var result = await _service.GetCollaborateursAsync();
// Assert : l'entrée invalide est ignorée proprement.
var item = Assert.Single(result);
Assert.Equal("Jean Dupont", item.DisplayName);
}
[Fact]
public async Task IdentitePublique_Conservee()
{
// Arrange
Guid collaborateurId = Guid.NewGuid();
await CollaborateurRepository.UpsertAsync(CreateCollaborateur(collaborateurId, "Dupont", "Jean"));
// Act
var result = await _service.GetCollaborateursAsync();
// Assert : l'identité publique (GUID) est conservée.
var item = Assert.Single(result);
Assert.Equal(collaborateurId, item.CollaborateurGuid);
}
[Fact]
public async Task FonctionneOffline_SansAppelApi()
{
// Arrange : aucune dépendance réseau, uniquement SQLite local.
await CollaborateurRepository.UpsertAsync(CreateCollaborateur(Guid.NewGuid(), "Dupont", "Jean"));
// Act
var result = await _service.GetCollaborateursAsync();
// Assert
Assert.Single(result);
}
// ===================== DÉTAIL =====================
[Fact]
public async Task Detail_CollaborateurTrouve()
{
// Arrange
Guid collaborateurId = Guid.NewGuid();
await CollaborateurRepository.UpsertAsync(CreateCollaborateur(collaborateurId, "Dupont", "Jean", code: "C001"));
// Act
var result = await _service.GetCollaborateurByIdAsync(collaborateurId);
// Assert
Assert.NotNull(result);
Assert.Equal(collaborateurId, result.CollaborateurGuid);
Assert.Equal("Jean Dupont", result.NomAffiche);
Assert.Equal("C001", result.Code);
}
[Fact]
public async Task Detail_CollaborateurInconnu_RetourneNull()
{
// Act
var result = await _service.GetCollaborateurByIdAsync(Guid.NewGuid());
// Assert
Assert.Null(result);
}
[Fact]
public async Task Detail_DonneesReellesChargees()
{
// Arrange
Guid collaborateurId = Guid.NewGuid();
await CollaborateurRepository.UpsertAsync(CreateCollaborateur(
collaborateurId, "Dupont", "Jean", code: "C001", statut: "Actif",
telephone: "0102030405", mobile: "0607080910", email: "jean@omika.fr"));
// Act
var result = await _service.GetCollaborateurByIdAsync(collaborateurId);
// Assert
Assert.NotNull(result);
Assert.Equal("Dupont", result.Nom);
Assert.Equal("Jean", result.Prenom);
Assert.Equal("C001", result.Code);
Assert.Equal("Actif", result.Statut);
Assert.Equal("0102030405", result.Phone);
Assert.Equal("0607080910", result.Mobile);
Assert.Equal("jean@omika.fr", result.Email);
Assert.True(result.HasPhone);
Assert.True(result.HasEmail);
}
[Fact]
public async Task Detail_ChampsOptionnelsAbsents()
{
// Arrange : collaborateur minimal sans coordonnées ni statut.
Guid collaborateurId = Guid.NewGuid();
await CollaborateurRepository.UpsertAsync(CreateCollaborateur(collaborateurId, "Dupont", "Jean"));
// Act
var result = await _service.GetCollaborateurByIdAsync(collaborateurId);
// Assert : champs optionnels absents restent nuls sans erreur.
Assert.NotNull(result);
Assert.Null(result.Phone);
Assert.Null(result.Mobile);
Assert.Null(result.Email);
Assert.Null(result.Statut);
Assert.False(result.HasPhone);
Assert.False(result.HasEmail);
}
[Fact]
public async Task Detail_IdentitePublique_Conservee()
{
// Arrange
Guid collaborateurId = Guid.NewGuid();
await CollaborateurRepository.UpsertAsync(CreateCollaborateur(collaborateurId, "Dupont", "Jean"));
// Act
var result = await _service.GetCollaborateurByIdAsync(collaborateurId);
// Assert
Assert.NotNull(result);
Assert.Equal(collaborateurId, result.CollaborateurGuid);
}
[Fact]
public async Task Detail_AucuneRelation_NeProvoquePasDErreur()
{
// Arrange : collaborateur sans rendez-vous ni interventions.
Guid collaborateurId = Guid.NewGuid();
await CollaborateurRepository.UpsertAsync(CreateCollaborateur(collaborateurId, "Dupont", "Jean"));
// Act
var result = await _service.GetCollaborateurByIdAsync(collaborateurId);
// Assert : collections vides sans erreur.
Assert.NotNull(result);
Assert.Empty(result.RendezVous);
Assert.Empty(result.Interventions);
}
}