using Microsoft.EntityFrameworkCore;

using RolemasterDb.App.Data;
using RolemasterDb.App.Domain;
using RolemasterDb.ImportTool.Parsing;

namespace RolemasterDb.ImportTool.Tests;

public sealed class CriticalImportMergeIntegrationTests
{
    private static readonly PdfXmlExtractor Extractor = new();
    private static readonly StandardCriticalTableParser StandardParser = new();

    [Fact]
    public async Task Reimport_updates_uncurated_results_and_refreshes_source_metadata()
    {
        var (parseResult, _) = await LoadPreparedSlashParseResultAsync();
        var databasePath = CreateEmptyDatabasePath();
        var loader = new CriticalImportLoader(databasePath);

        await loader.LoadAsync(parseResult.Table);

        await using (var dbContext = CreateDbContext(databasePath))
        {
            var result = await LoadResultAsync(dbContext, "36-45", "B");
            result.IsCurated = false;
            result.RawCellText = "Temporary raw text";
            result.DescriptionText = "Temporary description";
            result.RawAffixText = "+99H";
            result.ParseStatus = "manually_curated";
            result.SourcePageNumber = null;
            result.SourceImagePath = null;
            result.SourceImageCropJson = null;

            dbContext.CriticalEffects.RemoveRange(result.Effects);
            foreach (var branch in result.Branches)
            {
                dbContext.CriticalEffects.RemoveRange(branch.Effects);
            }

            dbContext.CriticalBranches.RemoveRange(result.Branches);
            result.Effects.Clear();
            result.Branches.Clear();
            await dbContext.SaveChangesAsync();
        }

        await loader.LoadAsync(parseResult.Table);

        await using (var dbContext = CreateDbContext(databasePath))
        {
            var result = await LoadResultAsync(dbContext, "36-45", "B");

            Assert.NotEqual("Temporary raw text", result.RawCellText);
            Assert.NotEqual("Temporary description", result.DescriptionText);
            Assert.NotEqual("+99H", result.RawAffixText);
            Assert.NotEqual("manually_curated", result.ParseStatus);
            Assert.NotNull(result.SourcePageNumber);
            Assert.False(string.IsNullOrWhiteSpace(result.SourceImagePath));
            Assert.False(string.IsNullOrWhiteSpace(result.SourceImageCropJson));
            Assert.NotEmpty(result.Branches);
        }
    }

    [Fact]
    public async Task Reimport_preserves_curated_results_and_child_rows()
    {
        var (parseResult, _) = await LoadPreparedSlashParseResultAsync();
        var databasePath = CreateEmptyDatabasePath();
        var loader = new CriticalImportLoader(databasePath);

        await loader.LoadAsync(parseResult.Table);

        await using (var dbContext = CreateDbContext(databasePath))
        {
            var result = await LoadResultAsync(dbContext, "36-45", "B");

            result.IsCurated = true;
            result.RawCellText = "Curated raw text";
            result.DescriptionText = "Curated result prose.";
            result.RawAffixText = "+12H";
            result.ParseStatus = "manually_curated";
            result.ParsedJson = "{\"reviewed\":true}";

            dbContext.CriticalEffects.RemoveRange(result.Effects);
            foreach (var branch in result.Branches)
            {
                dbContext.CriticalEffects.RemoveRange(branch.Effects);
            }

            dbContext.CriticalBranches.RemoveRange(result.Branches);
            result.Effects.Clear();
            result.Branches.Clear();

            result.Effects.Add(new CriticalEffect
            {
                EffectCode = CriticalEffectCodes.DirectHits,
                ValueInteger = 12,
                IsPermanent = false,
                SourceType = "manual",
                SourceText = "+12H"
            });

            result.Branches.Add(new CriticalBranch
            {
                BranchKind = "conditional",
                ConditionKey = "with_shield",
                ConditionText = "with shield",
                ConditionJson = "{}",
                RawText = "with shield: glancing blow.",
                DescriptionText = "Glancing blow.",
                ParsedJson = "{}",
                SortOrder = 1,
                Effects =
                [
                    new CriticalEffect
                    {
                        EffectCode = CriticalEffectCodes.MustParryRounds,
                        DurationRounds = 2,
                        IsPermanent = false,
                        SourceType = "manual",
                        SourceText = "2mp"
                    }
                ]
            });

            await dbContext.SaveChangesAsync();
        }

        await loader.LoadAsync(parseResult.Table);

        await using (var dbContext = CreateDbContext(databasePath))
        {
            var result = await LoadResultAsync(dbContext, "36-45", "B");

            Assert.True(result.IsCurated);
            Assert.Equal("Curated raw text", result.RawCellText);
            Assert.Equal("Curated result prose.", result.DescriptionText);
            Assert.Equal("+12H", result.RawAffixText);
            Assert.Equal("manually_curated", result.ParseStatus);
            Assert.Equal("{\"reviewed\":true}", result.ParsedJson);
            Assert.Single(result.Effects);
            Assert.Equal(12, result.Effects[0].ValueInteger);
            Assert.Single(result.Branches);
            Assert.Equal("with_shield", result.Branches[0].ConditionKey);
            Assert.Single(result.Branches[0].Effects);
            Assert.Equal(2, result.Branches[0].Effects[0].DurationRounds);
            Assert.False(string.IsNullOrWhiteSpace(result.SourceImagePath));
            Assert.False(string.IsNullOrWhiteSpace(result.SourceImageCropJson));
        }
    }

    [Fact]
    public async Task Reimport_deletes_only_unmatched_uncurated_results()
    {
        var (parseResult, _) = await LoadPreparedSlashParseResultAsync();
        var databasePath = CreateEmptyDatabasePath();
        var loader = new CriticalImportLoader(databasePath);

        await loader.LoadAsync(parseResult.Table);

        await using (var dbContext = CreateDbContext(databasePath))
        {
            var curatedMissing = await LoadResultAsync(dbContext, "01-05", "A");
            curatedMissing.IsCurated = true;
            curatedMissing.DescriptionText = "Keep me.";

            var uncuratedMissing = await LoadResultAsync(dbContext, "01-05", "B");
            uncuratedMissing.IsCurated = false;

            await dbContext.SaveChangesAsync();
        }

        var trimmedTable = CreateTrimmedTable(
            parseResult.Table,
            ("01-05", "A"),
            ("01-05", "B"));

        await loader.LoadAsync(trimmedTable);

        await using (var dbContext = CreateDbContext(databasePath))
        {
            var results = await dbContext.CriticalResults
                .Include(item => item.CriticalTable)
                .Include(item => item.CriticalColumn)
                .Include(item => item.CriticalRollBand)
                .Where(item => item.CriticalTable.Slug == "slash" && item.CriticalRollBand.Label == "01-05")
                .ToListAsync();

            Assert.Contains(results, item => item.CriticalColumn.ColumnKey == "A" && item.IsCurated && item.DescriptionText == "Keep me.");
            Assert.DoesNotContain(results, item => item.CriticalColumn.ColumnKey == "B");
        }
    }

    [Fact]
    public async Task Reimport_images_only_refreshes_provenance_without_touching_curated_content()
    {
        var (parseResult, _) = await LoadPreparedSlashParseResultAsync();
        var databasePath = CreateEmptyDatabasePath();
        var loader = new CriticalImportLoader(databasePath);

        await loader.LoadAsync(parseResult.Table);

        await using (var dbContext = CreateDbContext(databasePath))
        {
            var result = await LoadResultAsync(dbContext, "36-45", "B");

            result.IsCurated = true;
            result.RawCellText = "Curated raw text";
            result.DescriptionText = "Curated description";
            result.RawAffixText = "+12H";
            result.ParseStatus = "manually_curated";
            result.SourcePageNumber = null;
            result.SourceImagePath = null;
            result.SourceImageCropJson = null;

            await dbContext.SaveChangesAsync();
        }

        await loader.RefreshImageArtifactsAsync(parseResult.Table);

        await using (var dbContext = CreateDbContext(databasePath))
        {
            var result = await LoadResultAsync(dbContext, "36-45", "B");

            Assert.True(result.IsCurated);
            Assert.Equal("Curated raw text", result.RawCellText);
            Assert.Equal("Curated description", result.DescriptionText);
            Assert.Equal("+12H", result.RawAffixText);
            Assert.Equal("manually_curated", result.ParseStatus);
            Assert.NotNull(result.SourcePageNumber);
            Assert.False(string.IsNullOrWhiteSpace(result.SourceImagePath));
            Assert.False(string.IsNullOrWhiteSpace(result.SourceImageCropJson));
        }
    }

    private static ParsedCriticalTable CreateTrimmedTable(
        ParsedCriticalTable table,
        params (string RollBandLabel, string ColumnKey)[] excludedResults)
    {
        var excludedKeys = excludedResults
            .Select(item => $"{item.RollBandLabel}|{item.ColumnKey}")
            .ToHashSet(StringComparer.Ordinal);

        return new ParsedCriticalTable(
            table.Slug,
            table.DisplayName,
            table.Family,
            table.SourceDocument,
            table.Notes,
            table.Groups,
            table.Columns,
            table.RollBands,
            table.Results
                .Where(item => !excludedKeys.Contains($"{item.RollBandLabel}|{item.ColumnKey}"))
                .ToList());
    }

    private static async Task<(CriticalTableParseResult ParseResult, ImportArtifactPaths ArtifactPaths)> LoadPreparedSlashParseResultAsync()
    {
        var entry = LoadManifest().Tables.Single(item => item.Slug == "slash");
        var xmlPath = Path.Combine(GetArtifactCacheRoot(), $"{entry.Slug}.xml");

        if (!File.Exists(xmlPath))
        {
            await Extractor.ExtractAsync(Path.Combine(GetRepositoryRoot(), entry.PdfPath), xmlPath);
        }

        var parseResult = StandardParser.Parse(entry, await File.ReadAllTextAsync(xmlPath));
        var artifactRoot = Path.Combine(GetArtifactCacheRoot(), Guid.NewGuid().ToString("N"));
        var artifactPaths = ImportArtifactPaths.Create(artifactRoot, entry.Slug);
        var generator = new CriticalSourceImageArtifactGenerator(new PdfXmlExtractor());

        await generator.GenerateAsync(Path.Combine(GetRepositoryRoot(), entry.PdfPath), artifactPaths, parseResult);
        return (parseResult, artifactPaths);
    }

    private static async Task<CriticalResult> LoadResultAsync(RolemasterDbContext dbContext, string rollBandLabel, string columnKey) =>
        await dbContext.CriticalResults
            .Include(item => item.CriticalTable)
            .Include(item => item.CriticalColumn)
            .Include(item => item.CriticalRollBand)
            .Include(item => item.Effects)
            .Include(item => item.Branches)
                .ThenInclude(branch => branch.Effects)
            .SingleAsync(item =>
                item.CriticalTable.Slug == "slash" &&
                item.CriticalRollBand.Label == rollBandLabel &&
                item.CriticalColumn.ColumnKey == columnKey);

    private static ParsedCriticalResult FindResult(CriticalTableParseResult parseResult, string rollBandLabel, string columnKey) =>
        parseResult.Table.Results.Single(item =>
            item.GroupKey is null &&
            item.RollBandLabel == rollBandLabel &&
            item.ColumnKey == columnKey);

    private static CriticalImportManifest LoadManifest() =>
        new CriticalImportManifestLoader().Load(Path.Combine(GetRepositoryRoot(), "sources", "critical-import-manifest.json"));

    private static RolemasterDbContext CreateDbContext(string databasePath)
    {
        var options = new DbContextOptionsBuilder<RolemasterDbContext>()
            .UseSqlite($"Data Source={databasePath}")
            .Options;

        return new RolemasterDbContext(options);
    }

    private static string CreateEmptyDatabasePath() =>
        Path.Combine(GetArtifactCacheRoot(), $"critical-import-{Guid.NewGuid():N}.db");

    private static string GetArtifactCacheRoot()
    {
        var cacheRoot = Path.Combine(Path.GetTempPath(), "RolemasterDb.ImportTool.MergeTests");
        Directory.CreateDirectory(cacheRoot);
        return cacheRoot;
    }

    private static string GetRepositoryRoot() => TestRepositoryPaths.GetRepositoryRoot();
}
