using GerbilManagerWebAPI.Import;
using GerbilManagerWebAPI.Models;
using Microsoft.EntityFrameworkCore;

namespace GerbilManager.Tests
{
    /// <summary>
    /// FEAT-8c loader tests against a small JSON fixture + an in-memory DB (never Julian's
    /// instance). Covers categorisation, the quarantine policy, genotype composition,
    /// Farbschlag matching, high-confidence linking, and idempotency.
    /// </summary>
    public class ImportServiceTests : IDisposable
    {
        private readonly string _dir;

        public ImportServiceTests()
        {
            _dir = Path.Combine(Path.GetTempPath(), "feat8c-" + Guid.NewGuid().ToString("N"));
            Directory.CreateDirectory(_dir);
            File.WriteAllText(Path.Combine(_dir, "litters.json"), LittersJson);
            File.WriteAllText(Path.Combine(_dir, "animals.json"), AnimalsJson);
        }

        public void Dispose()
        {
            try { Directory.Delete(_dir, recursive: true); } catch { }
        }

        private ApplicationContext NewDb()
        {
            var opts = new DbContextOptionsBuilder<ApplicationContext>()
                .UseInMemoryDatabase("feat8c-" + Guid.NewGuid().ToString("N"))
                .Options;
            var db = new ApplicationContext(opts);
            db.Database.EnsureCreated();   // applies the 73-variety HasData seed
            return db;
        }

        [Fact]
        public async Task DryRun_categorises_without_writing()
        {
            using var db = NewDb();
            var report = await new ImportService(db, _dir, _dir).RunAsync(execute: false);

            Assert.False(report.Executed);
            Assert.Equal(2, report.Litters.InSource);
            Assert.Equal(2, report.Litters.Created);
            // 4 animals: a1 (hoch) + a4 (date-only) loadable; a2 conflict, a3 stub quarantined
            Assert.Equal(4, report.Animals.InSource);
            Assert.Equal(2, report.Animals.Created);
            Assert.Equal(1, report.Animals.LinkedToLitter);          // only a1 (hoch)
            Assert.Equal(1, report.Animals.FarbschlagMatched);       // a1 -> Agouti
            Assert.Equal(1, report.Animals.Quarantined.Conflicts);
            Assert.Equal(1, report.Animals.Quarantined.Stubs);
            Assert.Equal(1, report.Animals.Quarantined.DateOnlyLinks);

            // nothing written in dry-run
            Assert.Equal(0, await db.Gerbils.CountAsync());
            Assert.Equal(0, await db.Litters.CountAsync());
        }

        [Fact]
        public async Task Execute_loads_conflict_free_and_links_high_confidence()
        {
            using var db = NewDb();
            var report = await new ImportService(db, _dir, _dir).RunAsync(execute: true);

            Assert.True(report.Executed);
            Assert.Equal(2, await db.Litters.CountAsync());
            Assert.Equal(2, await db.Gerbils.CountAsync());          // a1, a4 only

            // conflict + stub never loaded
            Assert.False(await db.Gerbils.AnyAsync(g => g.ExternalRef == "a2"));
            Assert.False(await db.Gerbils.AnyAsync(g => g.ExternalRef == "a3"));

            var a1 = await db.Gerbils.SingleAsync(g => g.ExternalRef == "a1");
            Assert.NotNull(a1.LitterId);                              // high-confidence link
            Assert.Equal("FEAT-8 Stammbaum/Wurfchronik", a1.ImportSource);
            Assert.NotNull(a1.ColorVarietyId);                       // Agouti matched
            Assert.Contains("aa CC", a1.Genotype);                   // composed from mapped8locus
            Assert.Contains("??", a1.Genotype!);                     // missing loci -> ??
            Assert.Contains("eef", a1.Genotype!);                    // e^f caret stripped
            Assert.Contains("RawGenotype", a1.RawImportData!);       // raw verbatim preserved

            var a4 = await db.Gerbils.SingleAsync(g => g.ExternalRef == "a4");
            Assert.Null(a4.LitterId);                                // date-only link quarantined

            // PairingCode carried through
            Assert.True(await db.Litters.AnyAsync(l => l.PairingCode == "G01/ZdkC"));
        }

        [Fact]
        public async Task Execute_is_idempotent()
        {
            using var db = NewDb();
            await new ImportService(db, _dir, _dir).RunAsync(execute: true);
            var second = await new ImportService(db, _dir, _dir).RunAsync(execute: true);

            Assert.Equal(0, second.Animals.Created);
            Assert.Equal(0, second.Litters.Created);
            Assert.Equal(2, await db.Gerbils.CountAsync());
            Assert.Equal(2, await db.Litters.CountAsync());
        }

        [Fact]
        public void ComposeGenotype_strips_carets_and_fills_missing_loci()
        {
            var g = new SourceGenotype
            {
                Mapped8locus = new()
                {
                    ["A"] = new() { "a", "a" },
                    ["C"] = new() { "C", "c^chm" },
                    ["E"] = new() { "e", "e^f" },
                },
            };
            // order A C D E G P Sp Re ; missing -> ??
            Assert.Equal("aa Ccchm ?? eef ?? ?? ?? ??", ImportService.ComposeGenotype(g));
        }

        [Theory]
        [InlineData("01.02.2020", 2020, 2, 1)]
        [InlineData("5.3.21", 2021, 3, 5)]
        public void ParseDate_handles_german_dates(string s, int y, int m, int d)
        {
            var date = ImportService.ParseDate(s);
            Assert.Equal(new DateOnly(y, m, d), date);
        }

        // ---- fixtures ----
        private const string LittersJson = """
        [
          {"id":"L1","litterId":"A","date":"01.02.2020","damName":"Mama [X]","sireName":"Papa of Y","totalBorn":4,"zuchtnummer":"G01/ZdkC","note":"erster Wurf"},
          {"id":"L2","litterId":"B","date":"05.03.2021","damName":"Oma [Z]","sireName":"Opa of W","totalBorn":2,"zuchtnummer":"G02/ZdkC","note":""}
        ]
        """;

        private const string AnimalsJson = """
        [
          {"id":"a1","name":"Kind Eins","dob":"01.02.2020","death":"","gender":null,
           "farbschlag":"Agouti","farbschlagVariants":["Agouti"],
           "genotype":{"mapped8locus":{"A":["a","a"],"C":["C","C"],"D":["D","?"],"E":["e","e^f"]},"rawGenotype":"aa CC D- ee[f]","unmappedTokens":[]},
           "zucht":"","parentRefs":[],"photos":[],"sourceFiles":["f1"],"conflict":false,
           "litterRef":{"litterId":"L1","method":"geburtsdatum+eltern","confidence":"hoch"}},
          {"id":"a2","name":"Streit","dob":"01.01.2019","death":"","gender":null,
           "farbschlag":"Schwarz","farbschlagVariants":["Schwarz"],
           "genotype":{"mapped8locus":{"A":["a","a"]},"rawGenotype":"aa","unmappedTokens":[]},
           "zucht":"","parentRefs":[],"photos":[],"sourceFiles":["f1","f2"],"conflict":true},
          {"id":"a3","name":"Namenlos","dob":"","death":"","gender":null,
           "farbschlag":"","farbschlagVariants":[],
           "genotype":{"mapped8locus":{},"rawGenotype":"","unmappedTokens":[]},
           "zucht":"","parentRefs":[],"photos":[],"sourceFiles":["f1"],"conflict":false},
          {"id":"a4","name":"Unsicher","dob":"05.03.2021","death":"","gender":null,
           "farbschlag":"","farbschlagVariants":[],
           "genotype":{"mapped8locus":{"A":["A","a"]},"rawGenotype":"Aa","unmappedTokens":[]},
           "zucht":"","parentRefs":[],"photos":[],"sourceFiles":["f1"],"conflict":false,
           "litterRef":{"litterId":"L2","method":"geburtsdatum","confidence":"niedrig"}}
        ]
        """;
    }
}
