using System.IO.Compression; using System.IO.Packaging; using System.Text; using System.Text.Json; using DocumentFormat.OpenXml; using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Wordprocessing; using Microsoft.Extensions.Logging.Abstractions; using Omika.Core.Contracts.Documents; using Omika.Core.Interfaces; using Omika.Core.Services; using Omika.Data.Configuration; using Omika.Data.HfSql.Connection; using Omika.Data.Repositories.Omika; using WP = DocumentFormat.OpenXml.Drawing.Wordprocessing; namespace Omika.Tests.Connection; /// /// SONDE DIAGNOSTIC (lecture seule + génération en mémoire) : trace UNE génération /// de rapport réelle de bout en bout pour expliquer pourquoi une photo sélectionnée /// n'apparaît pas dans le PDF. Ne modifie JAMAIS la base. Écrit : /// — artifacts/diag-photo.txt : trace textuelle complète ; /// — artifacts/diag-modele.docx : modèle DOCX réel (copie) ; /// — artifacts/diag-rapport.docx : DOCX intermédiaire généré (avant conversion PDF). /// S'auto-ignore si la configuration locale est absente ou le serveur injoignable. /// public class RapportPhotoDiagnosticTests { private const string ConfigPath = @"D:\Avalonia\Omika\config\hfsql-test.local.json"; private const string ArtifactDir = @"D:\Avalonia\Omika\artifacts"; [Fact] public async Task Diagnostic_Trace_Photo_Reelle() { var sb = new StringBuilder(); sb.AppendLine("=== OMIKA PHOTO — DIAGNOSTIC (read-only) ==="); TestConfig? config = LoadConfig(); if (config is null) { sb.AppendLine("CONFIG: MISSING (skip)"); await WriteArtifact("diag-photo.txt", sb); return; } var options = new HfSqlConnectionOptions { Server = config.Server, Port = config.Port, Database = config.Omika.Database, User = config.Omika.User, Credential = config.Omika.Credential, FilePasswords = new HfSqlFilePasswordOptions { ProtectionStd = config.FilePasswords.ProtectionStd, ProtectionPlus = config.FilePasswords.ProtectionPlus } }; var factory = new HfSqlOleDbConnectionFactory(); await using var connection = new HfSqlOleDbConnection(factory, options); try { await connection.OpenAsync(); sb.AppendLine($"CONNECTION: OPEN (DB={config.Omika.Database})"); } catch (Exception ex) { sb.AppendLine($"CONNECTION FAILED (skip): {Redact(ex.Message)}"); await WriteArtifact("diag-photo.txt", sb); return; } try { // 1. Racine documentaire. object? rootObj = await connection.ExecuteScalarAsync( "SELECT PAP_Valeur FROM ParametreApplication WHERE PAP_Code = 'CHEMIN_REPERTOIRE_BASE_DOCUMENTS'"); string racine = rootObj?.ToString() ?? string.Empty; sb.AppendLine($"DOCUMENT ROOT = {racine}"); var parameters = new FakeApplicationParameters(racine); var repository = new OmikaRapportChampRepository( connection, NullLogger.Instance, parameters); // 2. Modèles contenant des rubriques photo. IReadOnlyList> modeles = await connection.QueryAsync( "SELECT MRP_PKID, MRP_Nom, MRP_Modele, MRP_Extra FROM ModeleRapport"); sb.AppendLine($"MODELES = {modeles.Count}"); long modelePhoto = 0; byte[]? modeleDocx = null; string? modeleTexte = null; foreach (var m in modeles) { byte[]? docx = m.GetValueOrDefault("MRP_Modele") as byte[]; if (docx is null || docx.Length == 0) { continue; } string texte = DocxTextExtractor.Extraire(docx); bool contientPhoto = texte.Contains("PHOTO", StringComparison.OrdinalIgnoreCase); sb.AppendLine($" MODELE pkid={m.GetValueOrDefault("MRP_PKID")} nom={Truncate(m.GetValueOrDefault("MRP_Nom")?.ToString())} photos={contientPhoto}"); if (contientPhoto && modelePhoto == 0) { modelePhoto = Convert.ToInt64(m.GetValueOrDefault("MRP_PKID")); modeleDocx = docx; modeleTexte = texte; } } if (modelePhoto == 0 || modeleDocx is null) { sb.AppendLine("NO PHOTO MODEL FOUND (skip)."); await WriteArtifact("diag-photo.txt", sb); return; } sb.AppendLine(); sb.AppendLine($"CHOSEN MODEL pkid={modelePhoto}"); sb.AppendLine("── MODEL TEXT (photo lines) ──"); foreach (string ligne in (modeleTexte ?? string.Empty).Split('\n')) { if (ligne.Contains("PHOTO", StringComparison.OrdinalIgnoreCase) || ligne.Contains("IMAGE", StringComparison.OrdinalIgnoreCase)) { sb.AppendLine($" {ligne.Trim()}"); } } await File.WriteAllBytesAsync(Path.Combine(ArtifactDir, "diag-modele.docx"), modeleDocx); // 3. RubriquesDocument liées aux photos. IReadOnlyList> rubriques = await connection.QueryAsync( "SELECT RD_PKID, RD_Nom, RD_Rubrique, RD_TypeMembre, RD_MasqueSaisie, RD_Groupe, RD_MembreMob, RD_IdRubParent, RD_Ordre " + "FROM RubriquesDocument WHERE RD_Groupe = 'PHOTO_INTER' ORDER BY RD_IdRubParent, RD_Ordre"); sb.AppendLine(); sb.AppendLine($"── RUBRIQUES PHOTO_INTER ({rubriques.Count}) ──"); foreach (var r in rubriques) { sb.AppendLine($" rub={r.GetValueOrDefault("RD_Rubrique")} type={r.GetValueOrDefault("RD_TypeMembre")} " + $"masque={r.GetValueOrDefault("RD_MasqueSaisie")} membre={r.GetValueOrDefault("RD_MembreMob")} parent={r.GetValueOrDefault("RD_IdRubParent")}"); } // 4. Intervention avec photos (celle qui possède réellement des .jpg). IReadOnlyList> interventionsAvecPhotos = await connection.QueryAsync( "SELECT DISTINCT DC_IDIntervention FROM Document WHERE DC_Nom LIKE '%.jpg' OR DC_Nom LIKE '%.png'"); long interventionId = 0; foreach (var i in interventionsAvecPhotos) { interventionId = Convert.ToInt64(i.GetValueOrDefault("DC_IDIntervention")); break; } if (interventionId == 0) { IReadOnlyList> toutes = await connection.QueryAsync( "SELECT IN_PKID FROM Intervention"); foreach (var i in toutes) { interventionId = Convert.ToInt64(i.GetValueOrDefault("IN_PKID")); break; } } IReadOnlyList> photos = await connection.QueryAsync( "SELECT Document.DC_PKID, Document.GUID, Document.DC_Nom, Document.DC_DateFichier, Document.DC_Chemin, " + "ParametreMetier.PA_Libelle AS CAT_Libelle " + "FROM Document LEFT OUTER JOIN ParametreMetier ON ParametreMetier.PA_CLE = Document.DC_CleCategorie " + "AND Document.DC_CleCategorie <> '' WHERE Document.DC_IDIntervention = ? ORDER BY Document.DC_PKID", [interventionId]); sb.AppendLine(); sb.AppendLine($"INTERVENTION pkid={interventionId} photosBrutes={photos.Count}"); foreach (var p in photos.Take(5)) { sb.AppendLine($" DC_PKID={p.GetValueOrDefault("DC_PKID")} nom={p.GetValueOrDefault("DC_Nom")} cat={p.GetValueOrDefault("CAT_Libelle")}"); } // 5. Champs complets via le repository (équivalent serveur). IReadOnlyList champs = await repository.GetChampsAsync( modelePhoto, interventionId, toutesRub: true); sb.AppendLine(); sb.AppendLine($"── CHAMPS ({champs.Count}) ──"); foreach (RapportChampDto c in champs) { bool photo = c.Rubrique?.Contains("PHOTO", StringComparison.OrdinalIgnoreCase) == true || string.Equals(c.Type, "ChpImage", StringComparison.OrdinalIgnoreCase); if (photo) { sb.AppendLine($" rub={c.Rubrique} type={c.Type} typeMembre={c.TypeMembre} membre={c.MembreMob} " + $"listeMembre={c.ListeMembre.Count} listeDonnesBD={(c.ListeDonnesBD ?? "").Length} chars"); foreach (RapportChampSousElementDto m in c.ListeMembre) { sb.AppendLine($" membre: rub={m.Rubrique} type={m.Type} typeMembre={m.TypeMembre} membreMob={m.MembreMob}"); } if (!string.IsNullOrWhiteSpace(c.ListeDonnesBD)) { sb.AppendLine($" listeDonnesBD JSON = {Truncate(c.ListeDonnesBD, 800)}"); } } } // 6. Photo sélectionnée (première option photo). RapportChampDto? photoChamp = champs.FirstOrDefault(c => string.Equals(c.Type, "ChpImage", StringComparison.OrdinalIgnoreCase) || c.Rubrique?.Contains("PHOTO", StringComparison.OrdinalIgnoreCase) == true); string? cheminComplet = null; if (photoChamp is not null) { if (photoChamp.ListeMembre is { Count: > 0 }) { // TABPHOTO : ListeDonnesBD = JSON array. using JsonDocument doc = JsonDocument.Parse(photoChamp.ListeDonnesBD ?? "[]"); if (doc.RootElement.ValueKind == JsonValueKind.Array) { foreach (JsonElement item in doc.RootElement.EnumerateArray()) { if (item.TryGetProperty("cheminComplet", out JsonElement cc) && cc.ValueKind == JsonValueKind.String) { cheminComplet = cc.GetString(); break; } } } } else { cheminComplet = photoChamp.ListeDonnesBD; } } sb.AppendLine(); sb.AppendLine($"PHOTO SELECTED = {(cheminComplet is null ? "NO" : "YES")}"); sb.AppendLine($"CHEMINCOMPLET = {cheminComplet}"); sb.AppendLine($"FILE EXISTS = {(cheminComplet is null ? "N/A" : File.Exists(cheminComplet))}"); if (cheminComplet is not null && File.Exists(cheminComplet)) { byte[] premiers = new byte[Math.Min(12, new FileInfo(cheminComplet).Length)]; using (FileStream fs = File.OpenRead(cheminComplet)) { _ = fs.Read(premiers, 0, premiers.Length); } sb.AppendLine($"MAGIC BYTES = {BitConverter.ToString(premiers)}"); } // 7. Génération du DOCX intermédiaire (vraie image fichier). sb.AppendLine(); sb.AppendLine("── RemplirRubriques ──"); byte[] docxRempli = RapportPdfService.RemplirRubriques(modeleDocx, champs); await File.WriteAllBytesAsync(Path.Combine(ArtifactDir, "diag-rapport.docx"), docxRempli); sb.AppendLine($"INTERMEDIATE DOCX = {docxRempli.Length} bytes"); InspecterDocx(sb, docxRempli, modeleTexte ?? string.Empty, champs); await WriteArtifact("diag-photo.txt", sb); } catch (Exception ex) { sb.AppendLine(); sb.AppendLine($"ERROR: {ex.GetType().Name}: {ex}"); await WriteArtifact("diag-photo.txt", sb); } } private static void InspecterDocx( StringBuilder sb, byte[] docx, string modeleTexte, IReadOnlyList champs) { using var stream = new MemoryStream(docx); using WordprocessingDocument document = WordprocessingDocument.Open(stream, false); MainDocumentPart? main = document.MainDocumentPart; if (main is null) { sb.AppendLine("INSPECT: MainDocumentPart missing"); return; } // Images dans le package (sous /word/media/). sb.AppendLine(); sb.AppendLine("── word/media/* ──"); using (var pkgStream = new MemoryStream(docx)) using (Package package = Package.Open(pkgStream, FileMode.Open, FileAccess.Read)) { var mediaParts = package.GetParts() .Where(p => p.Uri.OriginalString.StartsWith("/word/media/", StringComparison.OrdinalIgnoreCase)) .ToList(); foreach (var mediaPart in mediaParts) { long bytes = 0; try { using var s = mediaPart.GetStream(); bytes = s.Length; } catch { } sb.AppendLine($" media: uri={mediaPart.Uri} contentType={mediaPart.ContentType} bytes={bytes}"); } sb.AppendLine($"IMAGE PARTS = {mediaParts.Count}"); // Relations d'images (document.xml.rels). sb.AppendLine(); sb.AppendLine("── document.xml.rels (image relationships) ──"); var imageRelations = package .GetPart(new Uri("/word/document.xml", UriKind.Relative)) .GetRelationshipsByType("http://schemas.openxmlformats.org/officeDocument/2006/relationships/image") .ToList(); foreach (var rel in imageRelations) { sb.AppendLine($" relId={rel.Id} target={rel.TargetUri}"); } } sb.AppendLine(); sb.AppendLine("── Drawing OpenXML ──"); int drawings = main.Document.Body!.Descendants().Count(); var extents = main.Document.Body.Descendants() .Select(e => $"{e.Cx!.Value}x{e.Cy!.Value}").ToList(); sb.AppendLine($"DRAWINGS = {drawings}"); foreach (string extent in extents.Take(10)) { sb.AppendLine($" extent: {extent}"); } // Placeholder restants. sb.AppendLine(); sb.AppendLine("── PLACEHOLDER REMAINS ──"); string texte = string.Concat(main.Document.Body.Descendants().Select(t => t.Text)); foreach (RapportChampDto c in champs) { if (c.Rubrique?.Contains("PHOTO", StringComparison.OrdinalIgnoreCase) == true || string.Equals(c.Type, "ChpImage", StringComparison.OrdinalIgnoreCase)) { sb.AppendLine($" rub={c.Rubrique} remains={texte.Contains(c.Rubrique ?? "", StringComparison.Ordinal)}"); } } } private static async Task WriteArtifact(string name, StringBuilder sb) { Directory.CreateDirectory(ArtifactDir); await File.WriteAllTextAsync(Path.Combine(ArtifactDir, name), sb.ToString()); } private static string Truncate(string? s, int max = 60) => s is null ? "" : (s.Length <= max ? s : s[..max] + "…"); private static string Redact(string msg) => msg.Replace("Password", "[PWD]", StringComparison.OrdinalIgnoreCase); private sealed class FakeApplicationParameters : IApplicationParameterRepository { private readonly string _root; public FakeApplicationParameters(string root) => _root = root; public Task GetValueAsync(string code, CancellationToken ct = default) => Task.FromResult(code == "CHEMIN_REPERTOIRE_BASE_DOCUMENTS" ? _root : null); } private sealed class TestConfig { public int Port { get; set; } public string Server { get; set; } = ""; public FilePasswordsConfig FilePasswords { get; set; } = new(); public DatabaseConfig Omika { get; set; } = new(); } private sealed class FilePasswordsConfig { public string? ProtectionStd { get; set; } public string? ProtectionPlus { get; set; } } private sealed class DatabaseConfig { public string Credential { get; set; } = ""; public string Database { get; set; } = ""; public string User { get; set; } = ""; } private static TestConfig? LoadConfig() { if (!File.Exists(ConfigPath)) { return null; } try { return JsonSerializer.Deserialize( File.ReadAllText(ConfigPath), new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); } catch { return null; } } }