using System.Text.Json;
using System.Text.Json.Nodes;
using GerbilManagerWebAPI.Import;
using GerbilManagerWebAPI.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;

namespace GerbilManager.Tests
{
    /// <summary>
    /// Regression tests for the "Freeze/Override" mechanism (verified key animals + manual work
    /// survive the re-ingest). Exercised at the service + ingest level with an InMemory DB, mirroring
    /// the harness in <see cref="IngestResolvedServiceTests"/>.
    /// </summary>
    public class VerifiedGerbilTests : IDisposable
    {
        private static readonly Guid Schwarz = new("00000000-0000-0000-0000-000000000006");
        private readonly List<string> _dirs = new();

        public void Dispose()
        {
            foreach (var d in _dirs)
                try { Directory.Delete(d, recursive: true); } catch { }
        }

        // ── Harness helpers ──────────────────────────────────────────────────

        private string NewDir()
        {
            var dir = Path.Combine(Path.GetTempPath(), "verified-" + Guid.NewGuid().ToString("N"));
            Directory.CreateDirectory(dir);
            _dirs.Add(dir);
            return dir;
        }

        private static ApplicationContext NewDb()
        {
            var opts = new DbContextOptionsBuilder<ApplicationContext>()
                .UseInMemoryDatabase("verified-" + Guid.NewGuid().ToString("N"))
                .Options;
            var db = new ApplicationContext(opts);
            db.Database.EnsureCreated();
            return db;
        }

        private static IngestResolvedService Service(ApplicationContext db, string dir)
        {
            var config = new ConfigurationBuilder()
                .AddInMemoryCollection(new Dictionary<string, string?>
                {
                    { "Import:SourcePath", dir },
                    { "Photos:RootPath", dir },
                })
                .Build();
            return new IngestResolvedService(db, config, null!);
        }

        private static void WritePayload(string dir, object[] gerbils, object[]? litters = null, object[]? contacts = null) =>
            File.WriteAllText(Path.Combine(dir, "resolved_import.json"), JsonSerializer.Serialize(new
            {
                Contacts = contacts ?? Array.Empty<object>(),
                Litters = litters ?? Array.Empty<object>(),
                Gerbils = gerbils,
                GerbilPhotos = Array.Empty<object>(),
                SaleContracts = Array.Empty<object>(),
            }));

        private static object ImportGerbil(Guid id, string name, string genotype, string gender = "female", string dob = "2022-01-01") => new
        {
            Id = id,
            Name = name,
            Gender = gender,
            Status = "Breeding",
            DateOfBirth = dob,
            Genotype = genotype,
            ColorVarietyId = Schwarz,
            IsResident = true,
            OriginBreeder = "Test Zucht",
        };

        // ── 1. Manual animal survives the ingest (never wiped) ───────────────

        [Fact]
        public async Task Manual_animal_survives_ingest_that_omits_it()
        {
            using var db = NewDb();
            var dir = NewDir();

            var manualId = Guid.NewGuid();
            db.Gerbils.Add(new Gerbil { Id = manualId, Name = "Handmaus", Gender = Gender.female, IsManual = true });
            await db.SaveChangesAsync();

            var importedId = Guid.NewGuid();
            WritePayload(dir, new[] { ImportGerbil(importedId, "Importiert", "aa CC DD EE GG PP spsp") });

            var result = await Service(db, dir).RunAsync();

            Assert.StartsWith("Ingestion successful!", result);
            // Manual row is still there, unchanged.
            var manual = await db.Gerbils.FindAsync(manualId);
            Assert.NotNull(manual);
            Assert.True(manual!.IsManual);
            Assert.Equal("Handmaus", manual.Name);
            // Imported row was loaded.
            Assert.NotNull(await db.Gerbils.FindAsync(importedId));
            Assert.Equal(2, await db.Gerbils.CountAsync());
        }

        // ── 2. Verified freeze forces the golden values over a drifted import ─

        [Fact]
        public async Task Verified_override_forces_golden_and_records_drift()
        {
            using var db = NewDb();
            var dir = NewDir();
            var id = new Guid("11111111-1111-1111-1111-111111111111");

            // First ingest: import brings X in with the WRONG genotype/name.
            WritePayload(dir, new[] { ImportGerbil(id, "Falschname", "aa dd DD EE GG PP spsp") });
            var svc = Service(db, dir);
            await svc.RunAsync();

            // The breeder corrects X and marks it "vollständig korrekt".
            var g = await db.Gerbils.FindAsync(id);
            Assert.NotNull(g);
            g!.Name = "Korrekt";
            g.Genotype = "aa CC DD EE GG PP spsp";
            await db.SaveChangesAsync();

            var golden = await GerbilSnapshotService.BuildSnapshotAsync(db, id);
            db.GerbilOverrides.Add(new GerbilOverride
            {
                Id = Guid.NewGuid(),
                GerbilId = id,
                EntityName = g.Name,
                IsVerified = true,
                OverrideJson = GerbilSnapshotService.BuildFreezeJson(g),
                SnapshotJson = GerbilSnapshotService.SerializeSnapshot(golden!),
                VerifiedAt = DateTimeOffset.UtcNow,
                UpdatedAt = DateTimeOffset.UtcNow,
            });
            await db.SaveChangesAsync();

            // A later import DRIFTS back to wrong values.
            WritePayload(dir, new[] { ImportGerbil(id, "Wiederfalsch", "aa dd DD EE GG PP spsp") });
            await svc.RunAsync();

            // Golden wins (freeze re-applied on top of the fresh import).
            var after = await db.Gerbils.FindAsync(id);
            Assert.Equal("Korrekt", after!.Name);
            Assert.Equal("aa CC DD EE GG PP spsp", after.Genotype);

            // Drift was detected and recorded, naming the offending fields.
            var ov = await db.GerbilOverrides.SingleAsync();
            Assert.False(string.IsNullOrEmpty(ov.LastImportDiffJson));
            Assert.Contains("genotype", ov.LastImportDiffJson);
            Assert.Contains("name", ov.LastImportDiffJson);
            Assert.NotNull(ov.LastImportSnapshotJson);
        }

        // ── 3. No drift → LastImportDiffJson stays null ──────────────────────

        [Fact]
        public async Task Verified_override_without_drift_leaves_diff_null()
        {
            using var db = NewDb();
            var dir = NewDir();
            var id = new Guid("22222222-2222-2222-2222-222222222222");

            WritePayload(dir, new[] { ImportGerbil(id, "Stabil", "aa CC DD EE GG PP spsp") });
            var svc = Service(db, dir);
            await svc.RunAsync();

            var g = await db.Gerbils.FindAsync(id);
            var golden = await GerbilSnapshotService.BuildSnapshotAsync(db, id);
            db.GerbilOverrides.Add(new GerbilOverride
            {
                Id = Guid.NewGuid(),
                GerbilId = id,
                EntityName = g!.Name,
                IsVerified = true,
                OverrideJson = GerbilSnapshotService.BuildFreezeJson(g),
                SnapshotJson = GerbilSnapshotService.SerializeSnapshot(golden!),
                VerifiedAt = DateTimeOffset.UtcNow,
                UpdatedAt = DateTimeOffset.UtcNow,
            });
            await db.SaveChangesAsync();

            // Re-ingest with the IDENTICAL payload (== golden).
            await svc.RunAsync();

            var ov = await db.GerbilOverrides.SingleAsync();
            Assert.Null(ov.LastImportDiffJson);
        }

        // ── 4. Removing the override → import-driven again ───────────────────

        [Fact]
        public async Task Removing_override_restores_import_driven_values()
        {
            using var db = NewDb();
            var dir = NewDir();
            var id = new Guid("33333333-3333-3333-3333-333333333333");

            WritePayload(dir, new[] { ImportGerbil(id, "Import-Wert", "aa CC DD EE GG PP spsp") });
            var svc = Service(db, dir);
            await svc.RunAsync();

            var g = await db.Gerbils.FindAsync(id);
            g!.Genotype = "aa cc DD EE GG PP spsp"; // "golden"
            await db.SaveChangesAsync();
            var golden = await GerbilSnapshotService.BuildSnapshotAsync(db, id);
            var ov = new GerbilOverride
            {
                Id = Guid.NewGuid(),
                GerbilId = id,
                IsVerified = true,
                OverrideJson = GerbilSnapshotService.BuildFreezeJson(g),
                SnapshotJson = GerbilSnapshotService.SerializeSnapshot(golden!),
                UpdatedAt = DateTimeOffset.UtcNow,
            };
            db.GerbilOverrides.Add(ov);
            await db.SaveChangesAsync();

            // Freeze wins while the override exists.
            await svc.RunAsync();
            Assert.Equal("aa cc DD EE GG PP spsp", (await db.Gerbils.FindAsync(id))!.Genotype);

            // Remove the override → the animal follows the import again.
            db.GerbilOverrides.Remove(ov);
            await db.SaveChangesAsync();
            await svc.RunAsync();
            Assert.Equal("aa CC DD EE GG PP spsp", (await db.Gerbils.FindAsync(id))!.Genotype);
        }

        // ── 5. Per-field protection (pure service level) ─────────────────────

        [Fact]
        public void PerField_change_pins_only_the_changed_field()
        {
            var g = new Gerbil { Name = "Quelle", Notes = "alte Notiz", Genotype = "aa CC DD EE GG PP spsp" };

            var before = GerbilSnapshotService.BuildFreezeObject(g);
            g.Notes = "neue Notiz";
            var after = GerbilSnapshotService.BuildFreezeObject(g);

            var changed = GerbilSnapshotService.ChangedFields(before, after);
            var only = Assert.Single(changed); // ONLY notes changed
            Assert.Equal("notes", only.Key);

            var merged = GerbilSnapshotService.MergeOverrideJson("{}", changed);
            var mergedObj = JsonNode.Parse(merged)!.AsObject();
            Assert.True(mergedObj.ContainsKey("notes"));
            Assert.False(mergedObj.ContainsKey("genotype")); // untouched fields not pinned

            // Applying it onto a different animal overwrites ONLY notes, not the genotype.
            var target = new Gerbil { Name = "Ziel", Notes = "Ziel-Notiz", Genotype = "zz ZZ dd ee gg pp spsp" };
            GerbilSnapshotService.ApplyOverride(target, merged);
            Assert.Equal("neue Notiz", target.Notes);
            Assert.Equal("zz ZZ dd ee gg pp spsp", target.Genotype);
        }

        // ── 6. Dangling reference from a manual row is nulled + warned ────────

        [Fact]
        public async Task Stale_parent_reference_from_manual_litter_is_nulled_with_warning()
        {
            using var db = NewDb();
            var dir = NewDir();
            var fatherId = new Guid("44444444-4444-4444-4444-444444444444");

            // Ingest 1 brings in the (imported) father.
            WritePayload(dir, new[] { ImportGerbil(fatherId, "Vater", "aa CC DD EE GG PP spsp", gender: "male") });
            var svc = Service(db, dir);
            await svc.RunAsync();

            // The breeder wires a MANUAL litter to that imported father.
            var litterId = Guid.NewGuid();
            db.Litters.Add(new Litter { Id = litterId, Name = "Manueller Wurf", FatherId = fatherId, IsManual = true });
            await db.SaveChangesAsync();

            // Ingest 2 no longer contains the father → it becomes stale and is removed.
            WritePayload(dir, Array.Empty<object>());
            var result = await svc.RunAsync();

            // The manual litter survives, but its dangling parent link was nulled...
            var litter = await db.Litters.FindAsync(litterId);
            Assert.NotNull(litter);
            Assert.True(litter!.IsManual);
            Assert.Null(litter.FatherId);
            // ...and a warning was surfaced.
            Assert.Contains("warning", result);
            Assert.Contains("nicht mehr existiert", result);
            // The stale imported father is gone.
            Assert.Null(await db.Gerbils.FindAsync(fatherId));
        }

        // ── 7. Diff detects ancestry / offspring differences ─────────────────

        [Fact]
        public void Diff_detects_parent_and_children_differences()
        {
            var self = new SnapshotSelf(
                "Proband", "female", "2022-01-01", null, null, null, "aa CC DD EE GG PP spsp",
                null, "Schwarz", true, null, false, null, null, null, null,
                new List<string>(), null, null);

            var golden = new GerbilSnapshot(
                self,
                new SnapshotParent("Papa Alt", "2020-01-01", "aa CC"),
                null,
                new List<SnapshotLitter>
                {
                    new("Wurf1", "2023-01-01", "mother", 3, true, new List<SnapshotChild>
                    {
                        new("Kind1", "2023-01-01", "female"),
                    }),
                });

            var other = new GerbilSnapshot(
                self,
                new SnapshotParent("Papa Neu", "2020-01-01", "aa CC"), // different father
                null,
                new List<SnapshotLitter>
                {
                    new("Wurf1", "2023-01-01", "mother", 3, true, new List<SnapshotChild>
                    {
                        new("Kind1", "2023-01-01", "female"),
                        new("Kind2", "2023-01-02", "male"), // extra child
                    }),
                });

            var diffs = GerbilSnapshotService.Diff(golden, other);
            Assert.NotEmpty(diffs);
            Assert.Contains(diffs, d => d.Path == "father");
            Assert.Contains(diffs, d => d.Path.StartsWith("wurf["));
        }
    }
}
