using Omika.Core.Compatibility; using Omika.Core.Contracts.Common; using Omika.Core.Contracts.Mobile.Sync; using Omika.Core.Contracts.Organisation; using Omika.Core.Contracts.Planning; using Omika.Core.Contracts.Sites; using Omika.Core.Domain; using Omika.Core.Domain.Commands; using Omika.Core.Domain.Entities.Clients; using Omika.Core.Interfaces; using Omika.Core.Interfaces.Mobile; using Omika.Core.Services; using Omika.Core.Services.Mobile; namespace Omika.Tests.Services; /// /// Tests du SiteService : les relations du site (contacts, clients, rendez-vous) /// doivent être résolues par LI_PKID numérique, jamais par le GUID public Omika. /// public class SiteServiceTests { private sealed class FakeSiteRepository : ISiteRepository { public long SiteNumericId { get; set; } = 42; public string? LastContactsId { get; private set; } public string? LastClientsId { get; private set; } public Task CreateAsync(SiteCreateCommand command, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task> SearchAsync(SiteSearchCriteria criteria, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task> SearchDetailsAsync(SiteSearchCriteria criteria, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task GetDetailsAsync(string id, CancellationToken cancellationToken = default) { var site = new Site(); site.SourceReference = new LegacyEntityReference(DatabaseSchemaVersion.Omika, NumericId: SiteNumericId); return Task.FromResult(site); } public Task> GetContactsAsync(string id, CancellationToken cancellationToken = default) { LastContactsId = id; return Task.FromResult>([]); } public Task> GetClientsAsync(string id, CancellationToken cancellationToken = default) { LastClientsId = id; return Task.FromResult>([]); } public Task UpdateAsync(string id, SiteUpdateCommand command, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task ArchiveAsync(string id, CancellationToken cancellationToken = default) => throw new NotSupportedException(); } private sealed class FakeRendezVousService : IRendezVousService { public IReadOnlyList DetailsResults { get; set; } = []; public Task> SearchAsync(SearchRendezVousRequest request, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task> SearchDetailsAsync(SearchRendezVousRequest request, CancellationToken cancellationToken = default) => Task.FromResult(new PagedResult( DetailsResults, 1, DetailsResults.Count, DetailsResults.Count, DetailsResults.Count == 0 ? 0 : 1)); public Task GetByIdAsync(string id, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task CreateAsync(CreateRendezVousRequest request, CancellationToken cancellationToken = default, AuditActor? auteur = null) => throw new NotSupportedException(); public Task UpdateAsync(string id, UpdateRendezVousRequest request, CancellationToken cancellationToken = default, AuditActor? auteur = null) => throw new NotSupportedException(); public Task CancelAsync(string id, AnnulationRendezVousDto annulation, CancellationToken cancellationToken = default, AuditActor? auteur = null) => throw new NotSupportedException(); public Task ArchiveAsync(string id, CancellationToken cancellationToken = default, AuditActor? auteur = null) => throw new NotSupportedException(); public Task SetTempsValideAsync(string rendezVousId, string collaborateurId, bool tempsValide, CancellationToken cancellationToken = default, AuditActor? auteur = null) => throw new NotSupportedException(); } private sealed class FakeCollaborateurService : ICollaborateurService { public Task> SearchAsync(SearchCollaborateurRequest request, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task> SearchDetailsAsync(SearchCollaborateurRequest request, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task GetByIdAsync(string id, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task CreateAsync(CreateCollaborateurRequest request, CancellationToken cancellationToken = default, AuditActor? auteur = null) => throw new NotSupportedException(); public Task UpdateAsync(string id, UpdateCollaborateurRequest request, CancellationToken cancellationToken = default, AuditActor? auteur = null) => throw new NotSupportedException(); public Task ArchiveAsync(string id, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task> GetSocieteIdsAsync(long collaborateurId, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task> GetAllowedCollaborateurIdsAsync(long collaborateurId, bool includeSocietePrincipale, bool includeSocietes, bool includeEquipe, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task> GetRendezVousAsync(string collaborateurId, DateTime debutMin, DateTime debutMax, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public Task> GetRendezVousForCollaborateursAsync(IReadOnlyList collaborateurIds, DateTime debutMin, DateTime debutMax, CancellationToken cancellationToken = default) => throw new NotSupportedException(); } private sealed class FakeIdentityResolver : IMobileEntityIdentityResolver { public Guid CreateCanonicalIdentity(string entityType, PhysicalEntityIdentity physical) => physical.Guid ?? Guid.NewGuid(); public PhysicalEntityIdentity? ResolvePhysicalIdentity(string entityType, Guid publicGuid) => new PhysicalEntityIdentity { Guid = publicGuid }; public bool IsCanonical(string entityType, Guid publicGuid) => publicGuid != Guid.Empty; } [Fact] public async Task GetContactsAsync_GuidPublic_ResoutLiPkidAvantLaRequete() { // Arrange : schéma Omika — le resolver retourne un GUID SANS NumericId. var repo = new FakeSiteRepository { SiteNumericId = 42 }; var service = new SiteService( repo, new FakeRendezVousService(), new FakeCollaborateurService(), new FakeIdentityResolver()); // Act : l'identité publique du site est un GUID (comme côté mobile Omika). await service.GetContactsAsync(Guid.NewGuid().ToString("D")); // Assert : le repository a reçu le PK numérique (42), jamais le GUID. Assert.Equal("42", repo.LastContactsId); } [Fact] public async Task GetClientsAsync_GuidPublic_ResoutLiPkidAvantLaRequete() { var repo = new FakeSiteRepository { SiteNumericId = 7 }; var service = new SiteService( repo, new FakeRendezVousService(), new FakeCollaborateurService(), new FakeIdentityResolver()); await service.GetClientsAsync(Guid.NewGuid().ToString("D")); Assert.Equal("7", repo.LastClientsId); } [Fact] public async Task GetRendezVousAsync_RdvDateSurAffectation_RestitueLeRdvRabattu() { // Omika : RV_Date (racine) vide → Debut = default. La fiche lieu doit // rabattre la date de l'affectation (RVC_DateDeb) AVANT l'éligibilité, // comme la fiche intervention et le snapshot. Sans cela, un rendez-vous // réellement daté est écarté et l'onglet « Rendez-vous » reste vide. DateTime debutAffectation = new DateTime(2026, 8, 19, 10, 0, 0); var rdvService = new FakeRendezVousService { DetailsResults = [ new RendezVousDetailsDto { Id = Guid.NewGuid().ToString(), Debut = default, Collaborateurs = [ new AffectationCollaborateurDto { PeriodeDebut = debutAffectation, PeriodeFin = debutAffectation.AddHours(1.5) } ] } ] }; var service = new SiteService( new FakeSiteRepository { SiteNumericId = 42 }, rdvService, new FakeCollaborateurService(), new FakeIdentityResolver()); IReadOnlyList result = await service.GetRendezVousAsync(Guid.NewGuid().ToString("D")); RendezVousDetailsDto item = Assert.Single(result); Assert.Equal(debutAffectation, item.Debut); } }