using Microsoft.EntityFrameworkCore;

using RolemasterDb.App.Data;
using RolemasterDb.App.Domain;
using RolemasterDb.App.Features;
using RolemasterDb.CriticalParsing;
using AppCriticalEffectCodes = RolemasterDb.App.Domain.CriticalEffectCodes;

namespace RolemasterDb.ImportTool.Tests;

public sealed class CriticalCellReparseIntegrationTests
{
    [Fact]
    public void Shared_quick_notation_parser_supports_all_available_affix_shorthand()
    {
        var legend = new AffixLegend(
            new Dictionary<string, string>(StringComparer.Ordinal)
            {
                ["π"] = AppCriticalEffectCodes.MustParryRounds,
                ["∑"] = AppCriticalEffectCodes.StunnedRounds,
                ["∏"] = AppCriticalEffectCodes.NoParryRounds,
                ["∫"] = AppCriticalEffectCodes.BleedPerRound
            },
            ["P"],
            supportsFoePenalty: true,
            supportsAttackerBonus: true,
            supportsPowerPointModifier: true);

        var content = CriticalQuickNotationParser.Parse(
            "Mana tears through the target.\r\n+15, 3s, 1mp, 2np, 1hpr, -20, +20b, +2d10-3pp\r\nw/o shield: glancing blow, +5, π",
            legend);

        Assert.Equal("Mana tears through the target.", content.DescriptionText);
        Assert.Contains(content.Effects, effect => effect.EffectCode == AppCriticalEffectCodes.DirectHits && effect.ValueInteger == 15);
        Assert.Contains(content.Effects, effect => effect.EffectCode == AppCriticalEffectCodes.StunnedRounds && effect.DurationRounds == 3);
        Assert.Contains(content.Effects, effect => effect.EffectCode == AppCriticalEffectCodes.MustParryRounds && effect.DurationRounds == 1);
        Assert.Contains(content.Effects, effect => effect.EffectCode == AppCriticalEffectCodes.NoParryRounds && effect.DurationRounds == 2);
        Assert.Contains(content.Effects, effect => effect.EffectCode == AppCriticalEffectCodes.BleedPerRound && effect.PerRound == 1);
        Assert.Contains(content.Effects, effect => effect.EffectCode == AppCriticalEffectCodes.FoePenalty && effect.Modifier == -20);
        Assert.Contains(content.Effects, effect => effect.EffectCode == AppCriticalEffectCodes.AttackerBonusNextRound && effect.Modifier == 20);
        Assert.Contains(content.Effects, effect => effect.EffectCode == AppCriticalEffectCodes.PowerPointModifier && effect.ValueExpression == "+2d10-3");
        Assert.Single(content.Branches);
        Assert.Equal("glancing blow", content.Branches[0].DescriptionText);
        Assert.Contains(content.Branches[0].Effects, effect => effect.EffectCode == AppCriticalEffectCodes.DirectHits && effect.ValueInteger == 5);
        Assert.Contains(content.Branches[0].Effects, effect => effect.EffectCode == AppCriticalEffectCodes.MustParryRounds && effect.DurationRounds == 1);
        Assert.Empty(content.ValidationErrors);
    }

    [Fact]
    public void Shared_quick_notation_parser_supports_negative_power_point_shorthand()
    {
        var legend = new AffixLegend(
            new Dictionary<string, string>(StringComparer.Ordinal),
            ["P"],
            supportsFoePenalty: false,
            supportsAttackerBonus: false,
            supportsPowerPointModifier: true);

        var content = CriticalQuickNotationParser.Parse(
            "Void drains the foe.\r\n-2d10+4pp",
            legend);

        Assert.Contains(content.Effects, effect => effect.EffectCode == AppCriticalEffectCodes.PowerPointModifier && effect.ValueExpression == "-2d10+4");
        Assert.Equal("-2d10+4pp", content.RawAffixText);
        Assert.Empty(content.ValidationErrors);
    }

    [Fact]
    public void Shared_quick_notation_parser_surfaces_unknown_base_tokens_explicitly()
    {
        var legend = new AffixLegend(
            new Dictionary<string, string>(StringComparer.Ordinal)
            {
                ["π"] = AppCriticalEffectCodes.MustParryRounds
            },
            [],
            supportsFoePenalty: false,
            supportsAttackerBonus: false,
            supportsPowerPointModifier: false);

        var content = CriticalQuickNotationParser.Parse(
            "Foe reels away from the blast.\r\n+5, mystery",
            legend);

        Assert.Contains(content.Effects, effect => effect.EffectCode == AppCriticalEffectCodes.DirectHits && effect.ValueInteger == 5);
        Assert.Single(content.TokenReviewIssues);
        Assert.Equal("mystery", content.TokenReviewIssues[0].Token);
        Assert.Contains("unrecognized affix token 'mystery'", content.TokenReviewIssues[0].ReviewText, StringComparison.Ordinal);
        Assert.Contains(content.ValidationErrors, message => message.Contains("'mystery'", StringComparison.Ordinal));
    }

    [Fact]
    public void Shared_quick_notation_parser_keeps_recognized_effects_when_a_token_is_only_partially_understood()
    {
        var legend = new AffixLegend(
            new Dictionary<string, string>(StringComparer.Ordinal)
            {
                ["π"] = AppCriticalEffectCodes.MustParryRounds
            },
            [],
            supportsFoePenalty: false,
            supportsAttackerBonus: false,
            supportsPowerPointModifier: false);

        var content = CriticalQuickNotationParser.Parse(
            "Foe hesitates.\r\nπ?",
            legend);

        Assert.Contains(content.Effects, effect => effect.EffectCode == AppCriticalEffectCodes.MustParryRounds && effect.DurationRounds == 1);
        Assert.Single(content.TokenReviewIssues);
        Assert.Equal("π?", content.TokenReviewIssues[0].Token);
        Assert.Contains("unresolved fragment '?'", content.TokenReviewIssues[0].ReviewText, StringComparison.Ordinal);
    }

    [Fact]
    public void Shared_symbol_affix_parser_supports_negative_power_point_notation()
    {
        var legend = new AffixLegend(
            new Dictionary<string, string>(StringComparer.Ordinal),
            ["P"],
            supportsFoePenalty: false,
            supportsAttackerBonus: false,
            supportsPowerPointModifier: true);

        var effects = AffixEffectParser.Parse("-(2d10+4)P", legend);

        Assert.Contains(effects, effect => effect.EffectCode == AppCriticalEffectCodes.PowerPointModifier && effect.ValueExpression == "-2d10+4");
    }

    [Fact]
    public void Quick_notation_formatter_keeps_power_point_modifier_signs()
    {
        var formatted = CriticalQuickNotationFormatter.Format(
            "Arcane force twists around the foe.",
            [
                new CriticalEffectEditorItem(
                    AppCriticalEffectCodes.PowerPointModifier,
                    "foe",
                    null,
                    null,
                    "2d10-3",
                    null,
                    null,
                    null,
                    null,
                    false,
                    "import",
                    null,
                    null,
                    false),
                new CriticalEffectEditorItem(
                    AppCriticalEffectCodes.PowerPointModifier,
                    "foe",
                    null,
                    null,
                    "-1d10+4",
                    null,
                    null,
                    null,
                    null,
                    false,
                    "import",
                    null,
                    null,
                    false)
            ],
            []);

        Assert.Equal(
            "Arcane force twists around the foe.\n+2d10-3pp, -1d10+4pp",
            formatted.Replace("\r\n", "\n", StringComparison.Ordinal));
    }

    [Fact]
    public void Shared_cell_parser_extracts_base_effects_and_condition_branches()
    {
        var legend = new AffixLegend(
            new Dictionary<string, string>(StringComparer.Ordinal)
            {
                ["∫"] = AppCriticalEffectCodes.StunnedRounds
            },
            [],
            supportsFoePenalty: false,
            supportsAttackerBonus: false,
            supportsPowerPointModifier: false);

        var content = CriticalCellTextParser.Parse(
            "Strike to thigh.\r\n+10H\r\nWith greaves: glancing blow.\r\n2∫",
            legend);

        Assert.Equal("Strike to thigh.", content.DescriptionText);
        Assert.Equal("+10H", content.RawAffixText);
        Assert.Contains(content.Effects, effect => effect.EffectCode == AppCriticalEffectCodes.DirectHits && effect.ValueInteger == 10);
        Assert.Single(content.Branches);
        Assert.Equal("with_greaves", content.Branches[0].ConditionKey);
        Assert.Equal("glancing blow.", content.Branches[0].DescriptionText);
        Assert.Contains(content.Branches[0].Effects, effect => effect.EffectCode == AppCriticalEffectCodes.StunnedRounds && effect.DurationRounds == 2);
        Assert.Empty(content.ValidationErrors);
    }

    [Fact]
    public async Task Lookup_service_reparse_uses_shared_parser_and_table_legend_data()
    {
        var databasePath = Path.Combine(Path.GetTempPath(), $"rolemaster-reparse-{Guid.NewGuid():N}.db");
        await SeedSlashResultAsync(databasePath);

        var lookupService = new LookupService(CreateDbContextFactory(databasePath));
        var resultId = await GetSlashResultIdAsync(databasePath);

        var response = await lookupService.ReparseCriticalCellAsync(
            "slash",
            resultId,
            CreateEditorRequest(
                "Imported OCR source",
                "Old description",
                quickParseInput: "Strike to thigh.\n+10\nWith greaves: glancing blow., 2∫"));

        Assert.NotNull(response);
        Assert.Equal("Strike to thigh.", response!.DescriptionText);
        Assert.Contains(response.Effects, effect => effect.EffectCode == AppCriticalEffectCodes.DirectHits && effect.ValueInteger == 10);
        Assert.Contains(response.Branches, branch =>
            branch.ConditionKey == "with_greaves" &&
            branch.DescriptionText == "glancing blow." &&
            branch.Effects.Any(effect => effect.EffectCode == AppCriticalEffectCodes.StunnedRounds && effect.DurationRounds == 2));
        Assert.Empty(response.ValidationMessages);
        Assert.NotNull(response.GeneratedState);
        Assert.Empty(response.GeneratedState!.TokenReviewItems);
    }

    [Fact]
    public async Task Lookup_service_reparse_preserves_overridden_description_and_effect_value()
    {
        var databasePath = Path.Combine(Path.GetTempPath(), $"rolemaster-reparse-overrides-{Guid.NewGuid():N}.db");
        await SeedSlashResultAsync(databasePath);

        var lookupService = new LookupService(CreateDbContextFactory(databasePath));
        var resultId = await GetSlashResultIdAsync(databasePath);

        var response = await lookupService.ReparseCriticalCellAsync(
            "slash",
            resultId,
            CreateEditorRequest(
                "Imported OCR source",
                "Curated thigh strike.",
                quickParseInput: "Strike to thigh.\n+10",
                isDescriptionOverridden: true,
                effects:
                [
                    new CriticalEffectEditorItem(
                        AppCriticalEffectCodes.DirectHits,
                        null,
                        12,
                        null,
                        null,
                        null,
                        null,
                        null,
                        null,
                        false,
                        "manual",
                        "+12H",
                        "base:direct_hits:1",
                        true)
                ]));

        Assert.NotNull(response);
        Assert.Equal("Curated thigh strike.", response!.DescriptionText);
        Assert.Single(response.Effects);
        Assert.Equal(12, response.Effects[0].ValueInteger);
        Assert.True(response.IsDescriptionOverridden);
        Assert.True(response.Effects[0].IsOverridden);
    }

    [Fact]
    public async Task Lookup_service_reparse_preserves_manual_branch_collection_when_overridden()
    {
        var databasePath = Path.Combine(Path.GetTempPath(), $"rolemaster-reparse-branches-{Guid.NewGuid():N}.db");
        await SeedSlashResultAsync(databasePath);

        var lookupService = new LookupService(CreateDbContextFactory(databasePath));
        var resultId = await GetSlashResultIdAsync(databasePath);

        var response = await lookupService.ReparseCriticalCellAsync(
            "slash",
            resultId,
            CreateEditorRequest(
                "Imported OCR source",
                "Strike to thigh.",
                quickParseInput: "Strike to thigh.\n+10",
                areBranchesOverridden: true,
                branches:
                [
                    new CriticalBranchEditorItem(
                        "conditional",
                        "with_shield",
                        "With shield",
                        "{}",
                        "With shield: glancing blow.",
                        "Glancing blow.",
                        null,
                        "{}",
                        1,
                        null,
                        true,
                        false,
                        [])
                ]));

        Assert.NotNull(response);
        Assert.Single(response!.Branches);
        Assert.Equal("with_shield", response.Branches[0].ConditionKey);
        Assert.True(response.AreBranchesOverridden);
    }

    [Fact]
    public async Task Lookup_service_save_and_load_preserves_override_metadata()
    {
        var databasePath = Path.Combine(Path.GetTempPath(), $"rolemaster-save-load-{Guid.NewGuid():N}.db");
        await SeedSlashResultAsync(databasePath);

        var lookupService = new LookupService(CreateDbContextFactory(databasePath));
        var resultId = await GetSlashResultIdAsync(databasePath);
        var request = CreateEditorRequest(
            "Imported OCR source",
            "Curated thigh strike.",
            quickParseInput: "Strike to thigh.\n+10",
            isDescriptionOverridden: true,
            effects:
            [
                new CriticalEffectEditorItem(
                    AppCriticalEffectCodes.DirectHits,
                    null,
                    12,
                    null,
                    null,
                    null,
                    null,
                    null,
                    null,
                    false,
                    "manual",
                    "+12H",
                    "base:direct_hits:1",
                    true)
            ]);

        var saveResponse = await lookupService.UpdateCriticalCellAsync("slash", resultId, request);
        var loadResponse = await lookupService.GetCriticalCellEditorAsync("slash", resultId);

        Assert.NotNull(saveResponse);
        Assert.NotNull(loadResponse);
        Assert.True(loadResponse!.IsDescriptionOverridden);
        Assert.Single(loadResponse.Effects);
        Assert.Equal("base:direct_hits:1", loadResponse.Effects[0].OriginKey);
        Assert.True(loadResponse.Effects[0].IsOverridden);
        Assert.Contains("\"version\":1", loadResponse.ParsedJson, StringComparison.Ordinal);
    }

    [Fact]
    public async Task Lookup_service_formats_existing_results_into_quick_notation_and_keeps_arcane_aether_b16_must_parry()
    {
        var databasePath = CreateTemporaryDatabaseCopy();
        var lookupService = new LookupService(CreateDbContextFactory(databasePath));
        var resultId = await GetResultIdAsync(databasePath, "arcane-aether", "B", "16-20");

        var response = await lookupService.GetCriticalCellEditorAsync("arcane-aether", resultId);

        Assert.NotNull(response);
        Assert.Equal(
            "Foe brings his guard up, frightened by your display.\n+5, 1mp",
            response!.QuickParseInput.Replace("\r\n", "\n", StringComparison.Ordinal));
        Assert.NotNull(response.GeneratedState);
        Assert.Contains(response.GeneratedState!.Effects, effect => effect.EffectCode == AppCriticalEffectCodes.MustParryRounds && effect.DurationRounds == 1);
        Assert.Empty(response.GeneratedState.TokenReviewItems);

        var reparsed = await lookupService.ReparseCriticalCellAsync(
            "arcane-aether",
            resultId,
            CreateEditorRequest(response.RawCellText, response.DescriptionText, response.QuickParseInput));

        Assert.NotNull(reparsed);
        Assert.NotNull(reparsed!.GeneratedState);
        Assert.Contains(reparsed.GeneratedState!.Effects, effect => effect.EffectCode == AppCriticalEffectCodes.MustParryRounds && effect.DurationRounds == 1);
        Assert.Empty(reparsed.GeneratedState.TokenReviewItems);
    }

    [Fact]
    public async Task Lookup_service_reparse_surfaces_unknown_tokens_in_generated_review()
    {
        var databasePath = Path.Combine(Path.GetTempPath(), $"rolemaster-reparse-unknown-{Guid.NewGuid():N}.db");
        await SeedSlashResultAsync(databasePath);

        var lookupService = new LookupService(CreateDbContextFactory(databasePath));
        var resultId = await GetSlashResultIdAsync(databasePath);

        var response = await lookupService.ReparseCriticalCellAsync(
            "slash",
            resultId,
            CreateEditorRequest(
                "Imported OCR source",
                "Strike to thigh.",
                quickParseInput: "Strike to thigh.\n+10, mystery"));

        Assert.NotNull(response);
        Assert.NotNull(response!.GeneratedState);
        Assert.Single(response.GeneratedState!.TokenReviewItems);
        Assert.Equal("mystery", response.GeneratedState.TokenReviewItems[0].Token);
        Assert.Contains("unrecognized affix token 'mystery'", response.GeneratedState.TokenReviewItems[0].ReviewText, StringComparison.Ordinal);
        Assert.Contains(response.ValidationMessages, message => message.Contains("'mystery'", StringComparison.Ordinal));
    }

    [Fact]
    public async Task Lookup_service_reparse_updates_mana_c36_power_point_sign_when_quick_input_changes()
    {
        var databasePath = CreateTemporaryDatabaseCopy();
        var lookupService = new LookupService(CreateDbContextFactory(databasePath));
        var resultId = await GetResultIdAsync(databasePath, "mana", "C", "36-45");

        var initialResponse = await lookupService.GetCriticalCellEditorAsync("mana", resultId);

        Assert.NotNull(initialResponse);
        Assert.Contains("+2d10-16pp", initialResponse!.QuickParseInput, StringComparison.Ordinal);

        var changedQuickParseInput = initialResponse.QuickParseInput.Replace("+2d10-16pp", "-2d10-16pp", StringComparison.Ordinal);
        var reparsed = await lookupService.ReparseCriticalCellAsync(
            "mana",
            resultId,
            new CriticalCellUpdateRequest(
                initialResponse.RawCellText,
                changedQuickParseInput,
                initialResponse.DescriptionText,
                initialResponse.RawAffixText,
                initialResponse.ParseStatus,
                initialResponse.ParsedJson,
                initialResponse.IsCurated,
                initialResponse.IsDescriptionOverridden,
                initialResponse.IsRawAffixTextOverridden,
                initialResponse.AreEffectsOverridden,
                initialResponse.AreBranchesOverridden,
                initialResponse.Effects,
                initialResponse.Branches));

        Assert.NotNull(reparsed);
        Assert.Equal(changedQuickParseInput, reparsed!.QuickParseInput);
        Assert.Contains(reparsed.Effects, effect => effect.EffectCode == AppCriticalEffectCodes.PowerPointModifier && effect.ValueExpression == "-2d10-16");
        Assert.NotNull(reparsed.GeneratedState);
        Assert.Contains(reparsed.GeneratedState!.Effects, effect => effect.EffectCode == AppCriticalEffectCodes.PowerPointModifier && effect.ValueExpression == "-2d10-16");
    }

    [Fact]
    public async Task Lookup_service_reparse_updates_mana_c36_after_quick_input_was_saved_without_generation()
    {
        var databasePath = CreateTemporaryDatabaseCopy();
        var lookupService = new LookupService(CreateDbContextFactory(databasePath));
        var resultId = await GetResultIdAsync(databasePath, "mana", "C", "36-45");

        var initialResponse = await lookupService.GetCriticalCellEditorAsync("mana", resultId);
        Assert.NotNull(initialResponse);

        var editedQuickParseInput = initialResponse!.QuickParseInput.Replace("+2d10-16pp", "-2d10-16pp", StringComparison.Ordinal);

        var saveResponse = await lookupService.UpdateCriticalCellAsync(
            "mana",
            resultId,
            new CriticalCellUpdateRequest(
                initialResponse.RawCellText,
                editedQuickParseInput,
                initialResponse.DescriptionText,
                initialResponse.RawAffixText,
                initialResponse.ParseStatus,
                initialResponse.ParsedJson,
                initialResponse.IsCurated,
                initialResponse.IsDescriptionOverridden,
                initialResponse.IsRawAffixTextOverridden,
                initialResponse.AreEffectsOverridden,
                initialResponse.AreBranchesOverridden,
                initialResponse.Effects,
                initialResponse.Branches));

        Assert.NotNull(saveResponse);
        Assert.Contains(saveResponse!.Effects, effect => effect.EffectCode == AppCriticalEffectCodes.PowerPointModifier && effect.ValueExpression == "+2d10-16");

        var reopenedResponse = await lookupService.GetCriticalCellEditorAsync("mana", resultId);
        Assert.NotNull(reopenedResponse);
        Assert.Contains("-2d10-16pp", reopenedResponse!.QuickParseInput, StringComparison.Ordinal);
        Assert.Contains(reopenedResponse.Effects, effect => effect.EffectCode == AppCriticalEffectCodes.PowerPointModifier && effect.ValueExpression == "+2d10-16");

        var reparsed = await lookupService.ReparseCriticalCellAsync(
            "mana",
            resultId,
            new CriticalCellUpdateRequest(
                reopenedResponse.RawCellText,
                reopenedResponse.QuickParseInput,
                reopenedResponse.DescriptionText,
                reopenedResponse.RawAffixText,
                reopenedResponse.ParseStatus,
                reopenedResponse.ParsedJson,
                reopenedResponse.IsCurated,
                reopenedResponse.IsDescriptionOverridden,
                reopenedResponse.IsRawAffixTextOverridden,
                reopenedResponse.AreEffectsOverridden,
                reopenedResponse.AreBranchesOverridden,
                reopenedResponse.Effects,
                reopenedResponse.Branches));

        Assert.NotNull(reparsed);
        Assert.Contains(reparsed!.Effects, effect => effect.EffectCode == AppCriticalEffectCodes.PowerPointModifier && effect.ValueExpression == "-2d10-16");
        Assert.NotNull(reparsed.GeneratedState);
        Assert.Contains(reparsed.GeneratedState!.Effects, effect => effect.EffectCode == AppCriticalEffectCodes.PowerPointModifier && effect.ValueExpression == "-2d10-16");
    }

    private static CriticalCellUpdateRequest CreateEditorRequest(
        string rawCellText,
        string descriptionText,
        string? quickParseInput = null,
        bool isDescriptionOverridden = false,
        bool isRawAffixTextOverridden = false,
        bool areEffectsOverridden = false,
        bool areBranchesOverridden = false,
        string? rawAffixText = null,
        IReadOnlyList<CriticalEffectEditorItem>? effects = null,
        IReadOnlyList<CriticalBranchEditorItem>? branches = null) =>
        new(
            rawCellText,
            quickParseInput ?? rawCellText,
            descriptionText,
            rawAffixText,
            "partial",
            "{}",
            false,
            isDescriptionOverridden,
            isRawAffixTextOverridden,
            areEffectsOverridden,
            areBranchesOverridden,
            effects ?? [],
            branches ?? []);

    private static async Task SeedSlashResultAsync(string databasePath)
    {
        await using var seedContext = CreateDbContext(databasePath);
        await seedContext.Database.EnsureCreatedAsync();

        var table = new CriticalTable
        {
            Slug = "slash",
            DisplayName = "Slash Critical Strike Table",
            Family = "standard",
            SourceDocument = "Slash.pdf",
            Notes = null
        };
        var column = new CriticalColumn
        {
            CriticalTable = table,
            ColumnKey = "B",
            Label = "B",
            Role = "severity",
            SortOrder = 2
        };
        var rollBand = new CriticalRollBand
        {
            CriticalTable = table,
            Label = "36-40",
            MinRoll = 36,
            MaxRoll = 40,
            SortOrder = 8
        };
        var result = new CriticalResult
        {
            CriticalTable = table,
            CriticalColumn = column,
            CriticalRollBand = rollBand,
            RawCellText = "Old text",
            DescriptionText = "Old description",
            ParseStatus = "verified",
            ParsedJson = "{}"
        };

        result.Effects.Add(new CriticalEffect
        {
            EffectCode = AppCriticalEffectCodes.StunnedRounds,
            Target = "foe",
            DurationRounds = 1,
            IsPermanent = false,
            SourceType = "symbol",
            SourceText = "∫"
        });

        seedContext.CriticalTables.Add(table);
        seedContext.CriticalResults.Add(result);
        await seedContext.SaveChangesAsync();
    }

    private static async Task<int> GetSlashResultIdAsync(string databasePath)
    {
        await using var verifyContext = CreateDbContext(databasePath);
        return await verifyContext.CriticalResults
            .Where(item => item.CriticalTable.Slug == "slash")
            .Select(item => item.Id)
            .SingleAsync();
    }

    private static async Task<int> GetResultIdAsync(string databasePath, string slug, string columnLabel, string rollBandLabel)
    {
        await using var verifyContext = CreateDbContext(databasePath);
        return await verifyContext.CriticalResults
            .Where(item =>
                item.CriticalTable.Slug == slug &&
                item.CriticalColumn.Label == columnLabel &&
                item.CriticalRollBand.Label == rollBandLabel)
            .Select(item => item.Id)
            .SingleAsync();
    }

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

        return new RolemasterDbContext(options);
    }

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

        return new TestRolemasterDbContextFactory(options);
    }

    private static string CreateTemporaryDatabaseCopy()
    {
        var databasePath = Path.Combine(Path.GetTempPath(), $"rolemaster-app-{Guid.NewGuid():N}.db");
        File.Copy(Path.Combine(GetRepositoryRoot(), "src", "RolemasterDb.App", "rolemaster.db"), databasePath, true);
        UpgradeDatabase(databasePath).GetAwaiter().GetResult();
        return databasePath;
    }

    private static async Task UpgradeDatabase(string databasePath)
    {
        await using var dbContext = CreateDbContext(databasePath);
        await RolemasterDbSchemaUpgrader.EnsureLatestAsync(dbContext);
    }

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