using Omika.Core.Domain;
using Omika.Core.Domain.Commands;
using Omika.Data.Configuration;
using Omika.Data.Connection;
using Omika.Data.HfSql.Metadata;
using Omika.Data.Repositories.Batik;
namespace Omika.Tests.Repositories.Batik;
///
/// Tests unitaires de l'annulation d'un rendez-vous Batik
/// () : aucun serveur HFSQL,
/// aucun ODBC réel, aucun réseau. Un fake enregistre
/// le SQL et les paramètres et retourne des dictionnaires selon la table interrogée.
///
/// Objectifs :
///
/// - un rendez-vous INDIVIDUEL (PL_IDGroupe = 0) n'annule que sa propre
/// ligne Planning_RDV (ciblée par PL_PKID) ;
/// - un rendez-vous GROUPÉ (PL_IDGroupe > 0) annule TOUTES les lignes
/// du groupe (ciblées par PL_IDGroupe) : tous les rendez-vous du groupe sont
/// impactés ;
/// - la description reçoit la mention d'annulation
/// Note (Nom) : message, le nom étant résolu côté serveur s'il n'est pas
/// fourni.
///
///
public class BatikRendezVousAnnulationTests
{
private const long RendezVousId = 42L;
private const string DescriptionInitiale = "Description initiale";
[Fact]
public async Task CancelAsync_RdvIndividuel_CibleUniquementSaPropreLigne()
{
var connection = new FakeHfsqlConnection
{
PlanningRows = [PlanningRow(RendezVousId, idGroupe: 0L, collaborateurId: 7L)],
ParametreMetierRows = ReferenceRows()
};
var repository = new BatikRendezVousRepository(connection);
bool cancelled = await repository.CancelAsync("42", new AnnulationRendezVousCommand
{
OrigineKey = "O1",
MotifKey = "M1",
Commentaire = "Client absent",
CollaborateurNom = "Jean DUPONT"
});
Assert.True(cancelled);
ExecutedCommand write = Assert.Single(connection.Writes);
Assert.Contains("UPDATE Planning_RDV SET", write.Sql);
// Aucun groupe : la ligne est ciblée par sa clé primaire.
Assert.Contains("WHERE PL_PKID = ?", write.Sql);
Assert.DoesNotContain("PL_IDGroupe", write.Sql);
// Statut « Annulé » (ParametreMetier PA_PKID=1000) + origine + motif.
Assert.Equal(1000L, write.Parameters[2]);
Assert.Equal(2000L, write.Parameters[0]);
Assert.Equal(3000L, write.Parameters[1]);
Assert.Equal(
DescriptionInitiale + Environment.NewLine + "Note (Jean DUPONT) : Client absent",
write.Parameters[3] as string);
// L'annulation modifie la ligne : elle porte l'estampille d'audit. Sans
// auteur résolvable, ModifDate seul est écrit (jamais ModifUser = 0).
Assert.Contains("ModifDate = ?", write.Sql);
Assert.DoesNotContain("ModifUser", write.Sql);
Assert.IsType(write.Parameters[4]);
Assert.Equal(RendezVousId, write.Parameters[5]);
AssertParametersMatchPlaceholders(write);
}
[Fact]
public async Task CancelAsync_RdvGroupe_AnnuleToutesLesLignesDuGroupe()
{
var connection = new FakeHfsqlConnection
{
// Le rendez-vous appartient au groupe 99 : plusieurs lignes Planning_RDV
// partagent ce PL_IDGroupe (une par collaborateur).
PlanningRows = [PlanningRow(RendezVousId, idGroupe: 99L, collaborateurId: 7L)],
ParametreMetierRows = ReferenceRows()
};
var repository = new BatikRendezVousRepository(connection);
bool cancelled = await repository.CancelAsync("42", new AnnulationRendezVousCommand
{
OrigineKey = "O1",
MotifKey = "M1",
Commentaire = "Client absent",
CollaborateurNom = "Jean DUPONT"
});
Assert.True(cancelled);
ExecutedCommand write = Assert.Single(connection.Writes);
// Le groupe entier est annulé : ciblage par PL_IDGroupe, jamais par PL_PKID.
Assert.Contains("WHERE PL_IDGroupe = ?", write.Sql);
Assert.Equal(
DescriptionInitiale + Environment.NewLine + "Note (Jean DUPONT) : Client absent",
write.Parameters[3] as string);
// Estampille d'audit (ModifDate) puis la clé du groupe.
Assert.Contains("ModifDate = ?", write.Sql);
Assert.Equal(99L, write.Parameters[5]);
AssertParametersMatchPlaceholders(write);
}
[Fact]
public async Task CancelAsync_AvecAuteur_EstampilleModifDateEtModifUser()
{
var connection = new FakeHfsqlConnection
{
PlanningRows = [PlanningRow(RendezVousId, idGroupe: 0L, collaborateurId: 7L)],
ParametreMetierRows = ReferenceRows()
};
var repository = new BatikRendezVousRepository(connection);
bool cancelled = await repository.CancelAsync(
"42",
new AnnulationRendezVousCommand
{
OrigineKey = "O1",
MotifKey = "M1",
Commentaire = "Client absent",
CollaborateurNom = "Jean DUPONT"
},
auteur: new AuditActor("77", null));
Assert.True(cancelled);
ExecutedCommand write = Assert.Single(connection.Writes);
Assert.Contains("ModifDate = ?", write.Sql);
Assert.Contains("ModifUser = ?", write.Sql);
Assert.IsType(write.Parameters[4]);
Assert.Equal(77L, write.Parameters[5]);
Assert.Equal(RendezVousId, write.Parameters[6]);
AssertParametersMatchPlaceholders(write);
}
[Fact]
public async Task CancelAsync_SansNomCollaborateur_ResoutLeNomDepuisLeCollaborateurAffecte()
{
var connection = new FakeHfsqlConnection
{
PlanningRows = [PlanningRow(RendezVousId, idGroupe: 0L, collaborateurId: 7L)],
ParametreMetierRows = ReferenceRows(),
CollaborateurRows =
[
new Dictionary
{
["CO_PKID"] = 7L,
["CO_Nom"] = "DUPONT",
["CO_Prenom"] = "Jean"
}
]
};
var repository = new BatikRendezVousRepository(connection);
bool cancelled = await repository.CancelAsync("42", new AnnulationRendezVousCommand
{
OrigineKey = "O1",
MotifKey = "M1",
Commentaire = "Client absent"
});
Assert.True(cancelled);
ExecutedCommand write = Assert.Single(connection.Writes);
Assert.Equal(
DescriptionInitiale + Environment.NewLine + "Note (Jean DUPONT) : Client absent",
write.Parameters[3] as string);
}
// =========================================================================
// Helpers
// =========================================================================
///
/// Vérifie que le nombre de paramètres positionnels correspond EXACTEMENT au
/// nombre de ? du SQL : une estampille d'audit oubliée (ou en trop)
/// décale les paramètres et corrompt silencieusement l'écriture.
///
private static void AssertParametersMatchPlaceholders(ExecutedCommand command)
{
int placeholders = command.Sql.Count(c => c == '?');
Assert.Equal(placeholders, command.Parameters.Count);
}
private static Dictionary PlanningRow(long pkid, long idGroupe, long collaborateurId)
=> new()
{
["PL_PKID"] = pkid,
["PL_Description"] = DescriptionInitiale,
["PL_IDGroupe"] = idGroupe,
["PL_IDCollaborateur"] = collaborateurId
};
/// Référentiel minimal : origine, motif et statut « Annulé ».
private static IReadOnlyList> ReferenceRows() =>
[
new Dictionary
{
["PA_PKID"] = 2000L,
["PA_Code"] = "O1",
["PA_Libelle"] = "Externe : Client"
},
new Dictionary
{
["PA_PKID"] = 3000L,
["PA_Code"] = "M1",
["PA_Libelle"] = "Absent"
},
new Dictionary
{
["PA_PKID"] = 1000L,
["PA_Code"] = "15",
["PA_Valeur"] = "S.RDV.ANNU",
["PA_Libelle"] = "Annulé"
}
];
private sealed record ExecutedCommand(string Sql, IReadOnlyList