using GerbilManagerWebAPI.Import.Rpro3;
using GerbilManagerWebAPI.Models;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
namespace GerbilManager.Tests
{
///
/// Tests für den RennmausPro-III-Importer (Reader + Dedup + Execute).
/// Die meisten Tests bauen eine hermetische Mini-SQLite im RPRO3-Schema; ein optionaler
/// Test gegen die echte entpackte DB läuft nur, wenn sie lokal vorhanden ist.
///
public class Rpro3ImportTests
{
static Rpro3ImportTests()
{
// CP1252 für die Tests verfügbar machen (die Produktion registriert es im Reader-Ctor).
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
}
// ── Unit: Decode / JDN / Genotyp ──────────────────────────────
[Fact]
public void JdnToDate_converts_known_julian_day_numbers()
{
// Blacky _BIRTH = 2455036.0 → 2009-07-23 (siehe rpro3-vergleich.md).
Assert.Equal(new DateOnly(2009, 7, 23), Rpro3Reader.JdnToDate(2455036.0));
Assert.Null(Rpro3Reader.JdnToDate(0));
Assert.Null(Rpro3Reader.JdnToDate(null));
}
[Fact]
public void SmartDecode_prefers_utf8_then_cp1252()
{
var utf8 = System.Text.Encoding.UTF8.GetBytes("männlich");
Assert.Equal("männlich", Rpro3Reader.SmartDecode(utf8));
var cp1252 = System.Text.Encoding.GetEncoding(1252).GetBytes("männlich");
// CP1252-Bytes sind kein gültiges UTF-8 → Fallback dekodiert korrekt.
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
Assert.Equal("männlich", Rpro3Reader.SmartDecode(cp1252));
}
[Fact]
public void CleanGenotype_strips_brackets_keeps_dash()
{
Assert.Equal("aa Ccchm DD EE GG Pp spsp", Rpro3Reader_CleanGenotype("aa Cc[chm] DD EE GG Pp spsp"));
Assert.Equal("Kim AA C- Dd", Rpro3Reader_CleanGenotype("Kim AA C- Dd"));
}
private static string? Rpro3Reader_CleanGenotype(string s)
{
// CleanGenotype ist internal in Rpro3ImportService → über InternalsVisibleTo erreichbar.
var m = typeof(Rpro3ImportService).GetMethod("CleanGenotype",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
return (string?)m!.Invoke(null, new object?[] { s });
}
// ── Unit: Dedup ───────────────────────────────────────────────
[Fact]
public void Dedup_merges_compatible_same_name_records()
{
var animals = new List
{
Ext("u1", "Alice", new DateOnly(2010, 7, 19), "Schimmel", "Clan A"),
Ext("u2", "Alice", new DateOnly(2010, 7, 19), "Schimmel", ""), // kompatibel → merge
Ext("u3", "Alice", null, "Schimmel", "Clan A"), // kompatibel → merge
Ext("u4", "Alice", new DateOnly(2001, 1, 1), "Schwarz", "Clan B"), // Konflikt → eigener Cluster
};
var r = Rpro3Dedup.Run(animals);
// Mind. ein Merge-Cluster (u1/u2/u3), u4 separat → Name "Alice" mehrdeutig.
Assert.Single(r.MergeClusters);
Assert.Equal(3, r.MergeClusters.Values.First().Count);
Assert.Contains(r.Ambiguous, a => a.Name == "Alice");
}
[Fact]
public void Dedup_does_not_merge_placeholder_names()
{
var animals = new List
{
Ext("u1", "unbekannt", null, "", ""),
Ext("u2", "unbekannt", null, "", ""),
};
var r = Rpro3Dedup.Run(animals);
Assert.Empty(r.MergeClusters);
Assert.Equal(2, r.Placeholders.Count);
}
// ── Integration: Reader + Execute gegen eine Mini-RPRO3-DB ────
[Fact]
public async Task Execute_imports_animals_litters_contacts_and_is_idempotent()
{
var dbPath = BuildMiniRpro3();
try
{
var data = new Rpro3Reader().ReadFromDbFile(dbPath);
Assert.Equal(2, data.Animals.Count(a => a.Src == Rpro3Src.Stamm));
Assert.True(data.Animals.Count(a => a.Src == Rpro3Src.Fremd) >= 2);
using var efConn = new SqliteConnection("DataSource=:memory:");
efConn.Open();
var opts = new DbContextOptionsBuilder().UseSqlite(efConn).Options;
using var ctx = new ApplicationContext(opts);
ctx.Database.EnsureCreated();
var config = new ConfigurationBuilder().Build();
var service = new Rpro3ImportService(ctx, config, null);
var analyze = await service.AnalyzeAsync(data, photosProvided: false, availablePhotoFiles: null);
Assert.Equal(2, analyze.Counts.OwnAnimals);
Assert.True(analyze.NewVsCurrentNew >= 2);
var first = await service.ExecuteAsync(data, Path.GetTempPath(), photosProvided: false, photoSourcePaths: null);
Assert.True(first.GerbilsImported >= 4); // 2 stamm + ≥2 fremd
Assert.Equal(1, first.LittersImported >= 1 ? 1 : 0); // mind. 1 Wurf importiert
var gerbilCount1 = await ctx.Gerbils.CountAsync();
var litterCount1 = await ctx.Litters.CountAsync();
// Re-Run: idempotent — gleiche Zeilenzahl, keine Dubletten.
var second = await service.ExecuteAsync(data, Path.GetTempPath(), photosProvided: false, photoSourcePaths: null);
Assert.Equal(gerbilCount1, await ctx.Gerbils.CountAsync());
Assert.Equal(litterCount1, await ctx.Litters.CountAsync());
// Pedigree: Wurf hat Mutter + Vater verknüpft (resolved gegen importierte Tiere).
var litter = await ctx.Litters.FirstAsync(l => l.Name!.StartsWith("Wurf"));
Assert.NotNull(litter.MotherId);
Assert.NotNull(litter.FatherId);
// Genotyp wurde aus _FCODE übernommen + Klammern entfernt.
var blacky = await ctx.Gerbils.FirstAsync(g => g.Name == "Blacky");
Assert.Equal("aa Ccchm DD EE GG Pp spsp", blacky.Genotype);
Assert.True(blacky.IsResident);
// Encoding: "männlich" korrekt dekodiert (Latin-1-Quelle).
Assert.Equal(Gender.male, blacky.Gender);
}
finally { try { File.Delete(dbPath); } catch { } }
}
// ── Helpers ──
private static Rpro3Animal Ext(string rid, string name, DateOnly? dob, string farbe, string origin) =>
new() { Rid = rid, Src = Rpro3Src.Fremd, Name = name, Dob = dob, Farbe = farbe, Origin = origin };
/// Baut eine minimale SQLite im RPRO3-Schema (Latin-1-Geschlecht, JDN-Daten).
private static string BuildMiniRpro3()
{
var path = Path.Combine(Path.GetTempPath(), "mini-rpro3-" + Guid.NewGuid().ToString("N") + ".db");
using var conn = new SqliteConnection($"DataSource={path}");
conn.Open();
void Exec(string sql) { using var c = conn.CreateCommand(); c.CommandText = sql; c.ExecuteNonQuery(); }
Exec("CREATE TABLE herk_tb (id INTEGER, _BEZ, _ANREDE, _VNAME, _NNAME, _CLAN, _STR, _ORT, _PLZ, _TELP, _TELD, _MAIL, _HTTP, _MEMO, _BEM)");
Exec("CREATE TABLE abn_tb (id INTEGER, _BEZ, _ANREDE, _VNAME, _NNAME, _CLAN, _STR, _ORT, _PLZ, _TELP, _TELD, _MAIL, _HTTP, _MEMO, _BEM)");
Exec("CREATE TABLE baum_tb (id, _MID, _PID)");
Exec("CREATE TABLE stamm_tb (id INTEGER, _NAME, _SEX, _BIRTH, _HERKUNFT, _ZB, _STATUS, _FEHLER, _KASTRAT_DATE)");
Exec("CREATE TABLE fremd_tb (id INTEGER, _NAME, _SEX, _BIRTH, _ZB, _HERK, _MID, _PID, _TODAM, _KASTRAT_DATE)");
Exec("CREATE TABLE wurf_tb (id INTEGER, _MID, _PID, _AM, _BEZ, _BEM)");
Exec("CREATE TABLE wurftier_tb (id, _WID, _NAME, _SEX, _BIRTH, _SID, _TODAM, _WARUM, _ABAM, _ABN, _PRICE, _ZB, _FEHLER)");
Exec("CREATE TABLE color_tb (id, _FARBE, _FCODE)");
Exec("CREATE TABLE wurfcolor_tb (id, _FARBE, _FCODE)");
Exec("CREATE TABLE fremdcolor_tb (id, _FARBE, _FCODE)");
Exec("CREATE TABLE tod_tb (tid, _AM, _WARUM)");
Exec("CREATE TABLE waage_tb (_TID, _GRAMM, _DATE, _BEM)");
Exec("CREATE TABLE jungwaage_tb (_TID, _WID, _GRAMM, _DATE, _BEM)");
Exec("CREATE TABLE krank_tb (_TID, _DATE, _BEM, _MED)");
Exec("CREATE TABLE diary_tb (_TID, _BEZ, _DATE, _DESC)");
Exec("CREATE TABLE photo_tb (id, _P1, _P2, _P3)");
// Kontakte
Exec("INSERT INTO herk_tb (id,_BEZ,_CLAN,_ORT,_MAIL) VALUES (1,'eigene Zucht','','','')");
Exec("INSERT INTO herk_tb (id,_BEZ,_CLAN,_ORT,_MAIL) VALUES (61,'M.Knoss GG','Clan X','Gießen','a@b.de')");
// Geschlecht als Latin-1-Bytes einfügen (BLOB), um den smart_decode-Fallback zu testen.
var maennBytes = System.Text.Encoding.GetEncoding(1252).GetBytes("männlich");
var weibBytes = System.Text.Encoding.UTF8.GetBytes("weiblich");
void InsertStamm(int id, string name, byte[] sex, double birth, int herk, string zb)
{
using var c = conn.CreateCommand();
c.CommandText = "INSERT INTO stamm_tb (id,_NAME,_SEX,_BIRTH,_HERKUNFT,_ZB,_STATUS,_FEHLER) VALUES ($id,$n,$s,$b,$h,$z,3,'')";
c.Parameters.AddWithValue("$id", id);
c.Parameters.AddWithValue("$n", name);
c.Parameters.AddWithValue("$s", sex);
c.Parameters.AddWithValue("$b", birth);
c.Parameters.AddWithValue("$h", herk);
c.Parameters.AddWithValue("$z", zb);
c.ExecuteNonQuery();
}
InsertStamm(1, "Blacky", maennBytes, 2455036.0, 61, "ZdKC-B.09.2009");
InsertStamm(2, "Kuke", weibBytes, 2455093.0, 61, "ZdKC-B.10.2009");
// Genotyp (Bracket-Notation) für Blacky.
Exec("INSERT INTO color_tb (id,_FARBE,_FCODE) VALUES ('1','Schwarz','aa Cc[chm] DD EE GG Pp spsp')");
// Externe Ahnen (Eltern von Blacky & Kuke via baum_tb).
Exec("INSERT INTO fremd_tb (id,_NAME,_SEX,_BIRTH,_HERK,_MID,_PID) VALUES (1,'Oma','weiblich',0,1,'','')");
Exec("INSERT INTO fremd_tb (id,_NAME,_SEX,_BIRTH,_HERK,_MID,_PID) VALUES (2,'Opa','männlich',0,1,'','')");
Exec("INSERT INTO baum_tb (id,_MID,_PID) VALUES ('1','u1','u2')"); // Blacky Eltern = Oma×Opa
Exec("INSERT INTO baum_tb (id,_MID,_PID) VALUES ('2','u1','u2')"); // Kuke Eltern = Oma×Opa
// Wurf: Mutter Kuke (2) × Vater Blacky (1)
Exec("INSERT INTO wurf_tb (id,_MID,_PID,_AM,_BEZ,_BEM) VALUES (1,'2','1',2455246.0,'Wurf A','Erster Wurf')");
Exec("INSERT INTO wurftier_tb (id,_WID,_NAME,_SEX,_BIRTH,_SID,_TODAM,_ABAM,_ABN,_PRICE,_ZB) VALUES ('1j1','1','Alvin','männlich',2455246.0,'','','',NULL,'','21 A')");
// Gewicht + Krankheit für Blacky.
Exec("INSERT INTO waage_tb (_TID,_GRAMM,_DATE,_BEM) VALUES ('1',55.0,2455083.0,'')");
Exec("INSERT INTO krank_tb (_TID,_DATE,_BEM,_MED) VALUES ('1',2455077.0,'Prellung','')");
conn.Close();
SqliteConnection.ClearAllPools();
return path;
}
}
}