using GerbilManagerWebAPI.Import;
using GerbilManagerWebAPI.Models;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;

namespace GerbilManager.Tests
{
    /// <summary>
    /// FEAT-8d-CREATE / FIX-8D-DEDUP: docx importer CREATE path tests.
    /// Uses SQLite (not InMemory) so FK + unique-index constraints are enforced —
    /// this is the only reliable way to catch duplicate-ExternalRef crashes.
    /// </summary>
    public class ImportDocxServiceTests : IDisposable
    {
        private readonly string _dir;

        public ImportDocxServiceTests()
        {
            _dir = Path.Combine(Path.GetTempPath(), "feat8d-" + Guid.NewGuid().ToString("N"));
            Directory.CreateDirectory(_dir);
        }

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

        // One open connection keeps the :memory: SQLite DB alive across calls.
        private (ApplicationContext db, SqliteConnection conn) NewSqliteDb()
        {
            var conn = new SqliteConnection("DataSource=:memory:");
            conn.Open();
            var opts = new DbContextOptionsBuilder<ApplicationContext>().UseSqlite(conn).Options;
            var db = new ApplicationContext(opts);
            db.Database.EnsureCreated();   // schema WITH unique index on ExternalRef
            return (db, conn);
        }

        private void WriteLitters(object litters) =>
            File.WriteAllText(Path.Combine(_dir, "docx_litters.json"),
                System.Text.Json.JsonSerializer.Serialize(litters));

        private void WriteAnimals(object animals) =>
            File.WriteAllText(Path.Combine(_dir, "docx_animals.json"),
                System.Text.Json.JsonSerializer.Serialize(animals));

        // ── Test 1: dry-run shows correct counts without writing ─────────────────

        [Fact]
        public async Task DryRun_counts_new_animal_without_writing()
        {
            var (db, conn) = NewSqliteDb();
            await using (conn)
            await using (db)
            {
                // Litter in DB — date-only lookup (±5 days, exactly one candidate)
                var litter = new Litter
                {
                    Id = Guid.NewGuid(),
                    Name = "Testwurf",
                    Date = new DateOnly(2023, 5, 1),
                };
                db.Litters.Add(litter);
                await db.SaveChangesAsync();

                WriteLitters(Array.Empty<object>());
                WriteAnimals(new[]
                {
                    new { wsCode = "4/4", litterDob = "01.05.2023", name = "Pepper", gender = "female",
                          owner = "Max Mustermann", abgabeDate = "01.07.2023",
                          deathDate = "", deathCause = "", farbschlag = "" }
                });

                var report = await new ImportDocxService(db, _dir).RunAsync(execute: false);

                Assert.False(report.Executed);
                Assert.Equal(1, report.Created);
                Assert.Equal(1, report.LitterLinked);    // date-only match finds the one litter
                Assert.Equal(1, report.GoHomeFilled);
                Assert.Equal(0, await db.Gerbils.CountAsync());    // nothing written
                Assert.Equal(0, await db.Contacts.CountAsync());   // nothing written
            }
        }

        // ── Test 2: execute creates animal with all fields + FK constraints ───────

        [Fact]
        public async Task Execute_creates_animal_with_litter_contact_and_goHomeDate()
        {
            var (db, conn) = NewSqliteDb();
            await using (conn)
            await using (db)
            {
                var litter = new Litter
                {
                    Id = Guid.NewGuid(),
                    Name = "Testwurf",
                    Date = new DateOnly(2023, 5, 1),
                };
                db.Litters.Add(litter);
                await db.SaveChangesAsync();

                WriteLitters(Array.Empty<object>());
                WriteAnimals(new[]
                {
                    new { wsCode = "4/4", litterDob = "01.05.2023", name = "Pepper", gender = "female",
                          owner = "Max Mustermann", abgabeDate = "01.07.2023",
                          deathDate = "", deathCause = "", farbschlag = "" }
                });

                var report = await new ImportDocxService(db, _dir).RunAsync(execute: true);

                Assert.True(report.Executed);
                Assert.Equal(1, report.Created);
                Assert.Equal(1, report.ContactsCreated);

                var gerbil = await db.Gerbils.SingleAsync();
                Assert.Equal("Pepper", gerbil.Name);
                Assert.Equal(new DateOnly(2023, 5, 1), gerbil.DateOfBirth);
                Assert.Equal(Gender.female, gerbil.Gender);
                Assert.Equal(GerbilStatus.GivenAway, gerbil.Status);
                Assert.Equal(litter.Id, gerbil.LitterId);          // date-only link worked
                Assert.Equal(new DateOnly(2023, 7, 1), gerbil.GoHomeDate);
                Assert.Equal("Zucht der kleinen Chaoten", gerbil.OriginBreeder);
                Assert.False(gerbil.IsResident);
                Assert.Equal("docx", gerbil.ImportSource);
                Assert.StartsWith("docx-", gerbil.ExternalRef);

                var contact = await db.Contacts.SingleAsync();
                Assert.Equal("Max Mustermann", contact.Name);
                Assert.Equal(contact.Id, gerbil.ReceiverContactId);
            }
        }

        // ── Test 3: idempotency — second run creates zero ────────────────────────

        [Fact]
        public async Task Execute_is_idempotent_second_run_creates_zero()
        {
            var (db, conn) = NewSqliteDb();
            await using (conn)
            await using (db)
            {
                WriteLitters(Array.Empty<object>());
                WriteAnimals(new[]
                {
                    new { wsCode = "2/3", litterDob = "15.03.2023", name = "Flash", gender = "male",
                          owner = "", abgabeDate = "01.05.2023",
                          deathDate = "", deathCause = "", farbschlag = "" }
                });

                var first = await new ImportDocxService(db, _dir).RunAsync(execute: true);
                Assert.Equal(1, first.Created);

                var second = await new ImportDocxService(db, _dir).RunAsync(execute: true);
                Assert.Equal(0, second.Created);          // ExternalRef path, no dupe
                Assert.Equal(1, await db.Gerbils.CountAsync());    // exactly one row
            }
        }

        // ── Test 4: existing main-import animal → enrich only, no duplicate ──────

        [Fact]
        public async Task Execute_enriches_existing_animal_does_not_duplicate()
        {
            var (db, conn) = NewSqliteDb();
            await using (conn)
            await using (db)
            {
                // Animal already in DB (from main import, ExternalRef ≠ "docx-…")
                var existing = new Gerbil
                {
                    Id = Guid.NewGuid(),
                    Name = "Eddie",
                    DateOfBirth = new DateOnly(2022, 8, 10),
                    Gender = Gender.male,
                    ExternalRef = "main-eddie-xyz",   // NOT a "docx-" ref
                    IsResident = false,
                };
                db.Gerbils.Add(existing);
                await db.SaveChangesAsync();

                WriteLitters(Array.Empty<object>());
                WriteAnimals(new[]
                {
                    // Same name+dob → enrich, not create
                    new { wsCode = "", litterDob = "10.08.2022", name = "Eddie", gender = "male",
                          owner = "Anna Beispiel", abgabeDate = "01.10.2022",
                          deathDate = "", deathCause = "", farbschlag = "" }
                });

                var report = await new ImportDocxService(db, _dir).RunAsync(execute: true);

                Assert.Equal(0, report.Created);
                Assert.Equal(1, await db.Gerbils.CountAsync());    // still exactly one

                // GoHomeDate was enriched
                var updated = await db.Gerbils.SingleAsync();
                Assert.Equal(new DateOnly(2022, 10, 1), updated.GoHomeDate);
            }
        }

        // ── Test 5: Deceased status when deathDate set ───────────────────────────

        [Fact]
        public async Task Execute_sets_Deceased_status_when_deathDate_provided()
        {
            var (db, conn) = NewSqliteDb();
            await using (conn)
            await using (db)
            {
                WriteLitters(Array.Empty<object>());
                WriteAnimals(new[]
                {
                    new { wsCode = "1/5", litterDob = "01.01.2022", name = "Ghost", gender = "male",
                          owner = "", abgabeDate = "",
                          deathDate = "15.06.2022", deathCause = "Tumor", farbschlag = "" }
                });

                await new ImportDocxService(db, _dir).RunAsync(execute: true);

                var gerbil = await db.Gerbils.SingleAsync();
                Assert.Equal(GerbilStatus.Deceased, gerbil.Status);
                Assert.Equal(new DateOnly(2022, 6, 15), gerbil.DateOfDeath);
                Assert.Equal("Tumor", gerbil.CauseOfDeath);
            }
        }

        // ── Test 7: P0 REGRESSION — execute works under a retrying execution strategy ──

        [Fact]
        public async Task Execute_works_under_retrying_execution_strategy()
        {
            // Regression: NpgsqlRetryingExecutionStrategy (MaxRetryCount>0) calls
            // OnFirstExecution() at the start of ExecuteAsync, which throws
            // InvalidOperationException when it detects a user-initiated transaction
            // that was NOT opened through the strategy. This test wires the same check
            // (via FakeRetryingStrategy, MaxRetryCount=1) so the bug would surface in CI
            // without a live Npgsql instance.
            //
            // With the BUG (direct BeginTransactionAsync before strategy.ExecuteAsync):
            //   → OnFirstExecution sees active user tx → InvalidOperationException
            // With the FIX (BeginTransactionAsync inside strategy.ExecuteAsync lambda):
            //   → OnFirstExecution: no tx yet → OK
            var conn = new SqliteConnection("DataSource=:memory:");
            conn.Open();
            var opts = new DbContextOptionsBuilder<ApplicationContext>()
                .UseSqlite(conn)
                .ReplaceService<IExecutionStrategyFactory, FakeRetryingStrategyFactory>()
                .Options;
            var db = new ApplicationContext(opts);
            db.Database.EnsureCreated();

            await using (conn)
            await using (db)
            {
                WriteLitters(Array.Empty<object>());
                WriteAnimals(new[]
                {
                    new { wsCode = "3/3", litterDob = "01.01.2023", name = "Pixie", gender = "female",
                          owner = "Retry Adopter", abgabeDate = "01.03.2023",
                          deathDate = "", deathCause = "", farbschlag = "" }
                });

                // Must NOT throw InvalidOperationException (user-initiated tx rejected)
                var report = await new ImportDocxService(db, _dir).RunAsync(execute: true);

                Assert.True(report.Executed);
                Assert.Equal(1, report.Created);
                Assert.Equal(1, await db.Gerbils.CountAsync());
                Assert.Equal(1, await db.Contacts.CountAsync());
            }
        }

        // ── Test 6: P0 REGRESSION — same-name siblings get distinct ExternalRefs ─

        [Fact]
        public async Task Execute_same_name_siblings_created_with_distinct_ExternalRefs()
        {
            // P0 regression: two animals in the same litter with the same normalized name
            // previously caused duplicate ExternalRef → Npgsql/SQLite 23505 unique-key crash.
            // Fix: ordinal disambiguation (-2) ensures uniqueness within the batch.
            // The unique index on IX_Gerbils_ExternalRef (via EnsureCreated on SQLite) makes
            // this test an authoritative regression gate.
            var (db, conn) = NewSqliteDb();
            await using (conn)
            await using (db)
            {
                WriteLitters(Array.Empty<object>());
                WriteAnimals(new[]
                {
                    new { wsCode = "4/4", litterDob = "10.06.2022", name = "Mochi", gender = "female",
                          owner = "Eva Müller",  abgabeDate = "10.08.2022",
                          deathDate = "", deathCause = "", farbschlag = "" },
                    // Identical name+wsCode+litterDob → base ExternalRef collision
                    new { wsCode = "4/4", litterDob = "10.06.2022", name = "Mochi", gender = "female",
                          owner = "Lena Braun", abgabeDate = "11.08.2022",
                          deathDate = "", deathCause = "", farbschlag = "" },
                });

                // Must NOT throw unique-key violation
                var report = await new ImportDocxService(db, _dir).RunAsync(execute: true);

                Assert.Equal(2, report.Created);
                Assert.Equal(2, await db.Gerbils.CountAsync());

                var refs = (await db.Gerbils.Select(g => g.ExternalRef!).ToListAsync()).OrderBy(r => r).ToList();
                // Both start with the docx- prefix
                Assert.All(refs, r => Assert.StartsWith("docx-", r));
                // Must be distinct (unique index enforces this in SQLite)
                Assert.Equal(2, refs.Distinct().Count());
                // Second occurrence gets the -2 suffix
                Assert.Single(refs, r => r.EndsWith("-2"));

                // Idempotent re-run: zero new, still 2 in DB
                var second = await new ImportDocxService(db, _dir).RunAsync(execute: true);
                Assert.Equal(0, second.Created);
                Assert.Equal(2, await db.Gerbils.CountAsync());
            }
        }
    }

    // ── Helpers for Test 7 ────────────────────────────────────────────────────────

    /// <summary>
    /// Execution strategy with MaxRetryCount=1 so that EF Core's base
    /// OnFirstExecution() throws when it detects a user-initiated transaction
    /// that was not opened through CreateExecutionStrategy().ExecuteAsync().
    /// ShouldRetryOn=false → no actual retry; the check alone is what we need.
    /// </summary>
    internal sealed class FakeRetryingStrategy(ExecutionStrategyDependencies deps)
        : ExecutionStrategy(deps, maxRetryCount: 1, maxRetryDelay: TimeSpan.Zero)
    {
        protected override bool ShouldRetryOn(Exception exception) => false;
    }

    internal sealed class FakeRetryingStrategyFactory(ExecutionStrategyDependencies deps)
        : IExecutionStrategyFactory
    {
        public IExecutionStrategy Create() => new FakeRetryingStrategy(deps);
    }
}
