using System.Text.Json;
using Omika.App.Local.Models;
using Omika.App.Local.Services;
using Omika.Core.Contracts.Common;
using Omika.Core.Contracts.Planning;
namespace Omika.Tests.Mobile.Local;
///
/// Rendez-vous de groupe : l'unité affichée est l'AFFECTATION
/// (RDV_Collaborateur), jamais le noyau du rendez-vous. Une carte par
/// collaborateur et par jour, avec la date, les heures et le collaborateur de la
/// ligne réelle ; la fiche conserve l'affectation ouverte et expose, à titre
/// informatif, les jours et les collaborateurs du rendez-vous.
///
public class AgendaServiceAffectationTests : LocalTestBase
{
private static readonly DateTime Jour1 = new(2026, 8, 12);
private static readonly DateTime Jour2 = new(2026, 8, 13);
private static readonly Guid CollaborateurA = Guid.Parse("11111111-1111-1111-1111-111111111111");
private static readonly Guid CollaborateurB = Guid.Parse("22222222-2222-2222-2222-222222222222");
private readonly AgendaService _service;
public AgendaServiceAffectationTests()
{
_service = new AgendaService(
RendezVousRepository,
ClientRepository,
SiteRepository,
InterventionRepository,
CollaborateurRepository,
ApiClient,
SessionService);
}
private static AffectationCollaborateurDto Affectation(
Guid affectationId,
Guid collaborateurId,
string nom,
DateTime debut,
DateTime fin)
=> new()
{
Id = affectationId.ToString("D"),
CollaborateurId = collaborateurId.ToString("D"),
NomAffiche = nom,
PeriodeDebut = debut,
PeriodeFin = fin
};
///
/// Rendez-vous groupé sur DEUX jours avec DEUX collaborateurs : quatre
/// affectations, chacune avec son créneau.
///
private async Task<(Guid RendezVousId, Guid AffectationAJour2, Guid AffectationBJour2)> PreparerGroupeAsync()
{
Guid rendezVousId = Guid.NewGuid();
Guid aJour1 = Guid.NewGuid();
Guid aJour2 = Guid.NewGuid();
Guid bJour1 = Guid.NewGuid();
Guid bJour2 = Guid.NewGuid();
var dto = new RendezVousDetailsDto
{
Id = rendezVousId.ToString(),
Titre = "Chantier groupé",
Debut = Jour1.AddHours(8),
Fin = Jour2.AddHours(17),
Statut = new ReferenceValueDto { Label = "Planifié" },
EstGroupe = true,
Collaborateurs =
[
Affectation(aJour1, CollaborateurA, "Alice Martin", Jour1.AddHours(8), Jour1.AddHours(12)),
Affectation(aJour2, CollaborateurA, "Alice Martin", Jour2.AddHours(8), Jour2.AddHours(12)),
Affectation(bJour1, CollaborateurB, "Bruno Petit", Jour1.AddHours(13), Jour1.AddHours(17)),
Affectation(bJour2, CollaborateurB, "Bruno Petit", Jour2.AddHours(13), Jour2.AddHours(17))
]
};
await RendezVousRepository.UpsertAsync(new LocalRendezVous
{
Id = rendezVousId,
PayloadJson = JsonSerializer.Serialize(dto),
UpdatedAtUtc = DateTimeOffset.UtcNow
});
return (rendezVousId, aJour2, bJour2);
}
[Fact]
public async Task Agenda_UneCarteParAffectation_AvecLeBonCollaborateurEtLeBonCreneau()
{
// Arrange : aucune portée connue → toutes les affectations sont présentées.
(Guid rendezVousId, _, _) = await PreparerGroupeAsync();
// Act
IReadOnlyList result = await _service.GetRendezVousForRangeAsync(Jour1, Jour2.AddDays(1));
// Assert : une carte par affectation (2 collaborateurs × 2 jours).
Assert.Equal(4, result.Count);
// La carte d'Alice au 2e jour porte BIEN le créneau du 2e jour (et non
// celui du premier rendez-vous du groupe).
AgendaItem aliceJour2 = Assert.Single(result, i =>
i.CollaborateurNom == "Alice Martin" && i.Debut.Date == Jour2);
Assert.Equal(Jour2.AddHours(8), aliceJour2.Debut);
Assert.Equal(Jour2.AddHours(12), aliceJour2.Fin);
AgendaItem brunoJour2 = Assert.Single(result, i =>
i.CollaborateurNom == "Bruno Petit" && i.Debut.Date == Jour2);
Assert.Equal(Jour2.AddHours(13), brunoJour2.Debut);
Assert.Equal(Jour2.AddHours(17), brunoJour2.Fin);
// Toutes les cartes se rattachent au même rendez-vous (noyau).
Assert.All(result, item => Assert.Equal(rendezVousId, item.RendezVousGuid));
}
[Fact]
public async Task Agenda_PorteeCollaborateur_NePresenteQueSesPropresCreneaux()
{
// Arrange : agenda personnel d'Alice (bootstrap local).
await PreparerGroupeAsync();
SessionService.Bootstrap = new LocalBootstrapState
{
Collaborateur = new Omika.Core.Contracts.Organisation.CollaborateurDetailsDto
{
Id = CollaborateurA.ToString("D"),
Nom = "Martin",
Prenom = "Alice"
}
};
// Act
IReadOnlyList result = await _service.GetRendezVousForRangeAsync(Jour1, Jour2.AddDays(1));
// Assert : uniquement les deux créneaux d'Alice (un par jour).
Assert.Equal(2, result.Count);
Assert.All(result, item => Assert.Equal("Alice Martin", item.CollaborateurNom));
Assert.Equal([Jour1.AddHours(8), Jour2.AddHours(8)], result.Select(i => i.Debut).OrderBy(d => d).ToList());
}
[Fact]
public async Task Fiche_AffectationPrecise_GardeLeCreneauEtLeCollaborateurDeLaCarte()
{
// Arrange : la fiche est ouverte depuis la carte « Bruno – 2e jour ».
(Guid rendezVousId, _, Guid affectationBJour2) = await PreparerGroupeAsync();
// Act
AgendaItem? item = await _service.GetRendezVousByIdAsync(
rendezVousId,
default,
affectationBJour2.ToString("D"));
// Assert : créneau et collaborateur de l'affectation ouverte.
Assert.NotNull(item);
Assert.Equal(Jour2.AddHours(13), item!.Debut);
Assert.Equal(Jour2.AddHours(17), item.Fin);
Assert.Equal("Bruno Petit", item.CollaborateurNom);
Assert.Equal(affectationBJour2.ToString("D"), item.AffectationId);
}
[Fact]
public async Task Fiche_Groupe_ExposeLesJoursEtTousLesCollaborateurs()
{
// Arrange
(Guid rendezVousId, Guid affectationAJour2, _) = await PreparerGroupeAsync();
// Act
AgendaItem? item = await _service.GetRendezVousByIdAsync(
rendezVousId,
default,
affectationAJour2.ToString("D"));
// Assert : les deux jours du rendez-vous et les deux collaborateurs sont
// exposés pour le bloc informatif de la fiche, avec pour chaque journée le
// créneau réellement occupé (du premier démarrage à la dernière fin).
Assert.NotNull(item);
Assert.Equal([Jour1, Jour2], item!.JoursDuGroupe);
Assert.Equal(2, item.JoursAvecHoraires.Count);
Assert.Equal(Jour1.AddHours(8), item.JoursAvecHoraires[0].Debut);
Assert.Equal(Jour1.AddHours(17), item.JoursAvecHoraires[0].Fin);
Assert.Equal(Jour2.AddHours(8), item.JoursAvecHoraires[1].Debut);
Assert.Equal(Jour2.AddHours(17), item.JoursAvecHoraires[1].Fin);
Assert.Equal(2, item.Collaborateurs.Count);
Assert.Contains("Alice Martin", item.Collaborateurs);
Assert.Contains("Bruno Petit", item.Collaborateurs);
Assert.True(item.AfficheBlocPeriode);
// Le collaborateur de l'affectation OUVERTE est identifié : c'est lui qui
// sera exclu du bloc (sa propre fiche) et dont les journées sont présentées.
Assert.Equal(CollaborateurA, item.CollaborateurOccurrenceGuid);
// Journées PAR collaborateur : Alice n'a que son créneau du matin, Bruno
// celui de l'après-midi — aucun des deux n'hérite des heures de l'autre.
Assert.Equal(2, item.JoursParCollaborateur.Count);
JoursParCollaborateur joursAlice = Assert.Single(
item.JoursParCollaborateur, j => j.CollaborateurGuid == CollaborateurA);
Assert.Equal([Jour1, Jour2], joursAlice.Jours.Select(j => j.Jour).ToList());
Assert.All(joursAlice.Jours, j =>
{
Assert.Equal(j.Jour.AddHours(8), j.Debut);
Assert.Equal(j.Jour.AddHours(12), j.Fin);
});
JoursParCollaborateur joursBruno = Assert.Single(
item.JoursParCollaborateur, j => j.CollaborateurGuid == CollaborateurB);
Assert.Equal([Jour1, Jour2], joursBruno.Jours.Select(j => j.Jour).ToList());
Assert.All(joursBruno.Jours, j =>
{
Assert.Equal(j.Jour.AddHours(13), j.Debut);
Assert.Equal(j.Jour.AddHours(17), j.Fin);
});
}
[Fact]
public async Task Fiche_RendezVousSimple_NAffichePasLeBlocGroupe()
{
// Arrange : rendez-vous d'une journée, un seul collaborateur.
Guid rendezVousId = Guid.NewGuid();
Guid affectationId = Guid.NewGuid();
var dto = new RendezVousDetailsDto
{
Id = rendezVousId.ToString(),
Titre = "Visite",
Debut = Jour1.AddHours(9),
Fin = Jour1.AddHours(10),
Collaborateurs =
[
Affectation(affectationId, CollaborateurA, "Alice Martin", Jour1.AddHours(9), Jour1.AddHours(10))
]
};
await RendezVousRepository.UpsertAsync(new LocalRendezVous
{
Id = rendezVousId,
PayloadJson = JsonSerializer.Serialize(dto),
UpdatedAtUtc = DateTimeOffset.UtcNow
});
// Act
AgendaItem? item = await _service.GetRendezVousByIdAsync(rendezVousId, default, affectationId.ToString("D"));
// Assert : bloc informatif masqué (un jour, un collaborateur).
Assert.NotNull(item);
Assert.False(item!.AfficheBlocPeriode);
Assert.Single(item.JoursDuGroupe);
}
}