using System.Text;
using MMoneyCoderSharp.Data;

namespace MBN_STOCK_WEBVIEW.LegacyApplication.Tests;

public sealed class LegacyRuntimeUiCatalogLoaderTests
{
    private static readonly Encoding Cp949 = CreateCp949Encoding();

    [Fact]
    public void ConfiguredLegacyBinDebugFiles_MatchEveryRuntimeProjection()
    {
        var legacyBinDebug = Environment.GetEnvironmentVariable(
            "MBN_STOCK_N_LEGACY_BIN_DEBUG");
        if (string.IsNullOrWhiteSpace(legacyBinDebug))
        {
            return;
        }

        var composition = LegacyRuntimeUiCatalogLoader.ResolveFromBaseDirectory(
            legacyBinDebug);

        Assert.Empty(composition.Warnings);
        AssertRuntimeFixedFile(
            legacyBinDebug,
            "해외.ini",
            LegacyFixedMarket.Overseas,
            LegacyFixedActionCatalog.ExpectedOverseasCount,
            composition.FixedCatalog);
        AssertRuntimeFixedFile(
            legacyBinDebug,
            "환율.ini",
            LegacyFixedMarket.Exchange,
            LegacyFixedActionCatalog.ExpectedExchangeCount,
            composition.FixedCatalog);
        AssertRuntimeFixedFile(
            legacyBinDebug,
            "지수.ini",
            LegacyFixedMarket.Index,
            LegacyFixedActionCatalog.ExpectedIndexCount,
            composition.FixedCatalog);
        AssertRuntimeIndustryFile(
            legacyBinDebug,
            "업종_코스피.ini",
            IndustryMarket.Kospi,
            composition.IndustryLayout);
        AssertRuntimeIndustryFile(
            legacyBinDebug,
            "업종_코스닥.ini",
            IndustryMarket.Kosdaq,
            composition.IndustryLayout);
        AssertRuntimeComparisonFile(
            legacyBinDebug,
            composition.ComparisonLayout);
    }

    [Fact]
    public void RuntimeFiles_ControlOnlyTrustedDisplayOrder()
    {
        using var directory = new TemporaryDirectory();
        var fixedOrders = Enum.GetValues<LegacyFixedMarket>()
            .ToDictionary(
                market => market,
                market => LegacyFixedActionCatalog.Default.GetActions(market)
                    .Reverse()
                    .ToArray());
        var kospi = LegacyIndustryActionCatalog.GetActions(IndustryMarket.Kospi)
            .Reverse()
            .ToArray();
        var kosdaq = LegacyIndustryActionCatalog.GetActions(IndustryMarket.Kosdaq)
            .Reverse()
            .ToArray();
        var targets = LegacyComparisonWorkflowCatalog.FixedTargets
            .Reverse()
            .ToArray();
        var directActions = LegacyComparisonWorkflowCatalog.Actions
            .Where(action => action.Kind != LegacyComparisonActionKind.Return)
            .Reverse()
            .ToArray();
        var returnActions = LegacyComparisonWorkflowCatalog.Actions
            .Where(action => action.Kind == LegacyComparisonActionKind.Return)
            .Reverse()
            .ToArray();

        WriteAllValidFiles(
            directory.Path,
            fixedOrders,
            kospi,
            kosdaq,
            targets,
            directActions,
            returnActions,
            returnSectionFirst: true);

        var composition = LegacyRuntimeUiCatalogLoader.ResolveFromBaseDirectory(
            directory.Path);

        Assert.Empty(composition.Warnings);
        foreach (var market in Enum.GetValues<LegacyFixedMarket>())
        {
            Assert.Equal(
                FlattenForIni(fixedOrders[market]).Select(action => action.Id),
                composition.FixedCatalog.GetActions(market).Select(action => action.Id));
        }

        Assert.Equal(
            kospi.Select(action => action.ActionId),
            composition.IndustryLayout.GetActions(IndustryMarket.Kospi)
                .Select(action => action.ActionId));
        Assert.Equal(
            kosdaq.Select(action => action.ActionId),
            composition.IndustryLayout.GetActions(IndustryMarket.Kosdaq)
                .Select(action => action.ActionId));
        Assert.Equal(
            targets.Select(target => target.TargetId),
            composition.ComparisonLayout.FixedTargets.Select(target => target.TargetId));
        Assert.Equal(
            returnActions.Select(action => action.ActionId)
                .Concat(directActions.Select(action => action.ActionId)),
            composition.ComparisonLayout.CreateActionViews(null, null)
                .Select(action => action.ActionId));

        var industryWorkflow = new LegacyIndustrySelectionWorkflow(
            new EmptyIndustrySelectionService(),
            actionLayout: composition.IndustryLayout);
        Assert.Equal(
            kospi.Select(action => action.ActionId),
            industryWorkflow.ResetForTabActivation(IndustryMarket.Kospi)
                .Actions.Select(action => action.ActionId));

        var comparisonWorkflow = new LegacyComparisonWorkflowController(
            new EmptyComparisonDataService(),
            new EmptyComparisonPairStore(),
            composition.ComparisonLayout);
        Assert.Equal(
            targets.Select(target => target.DisplayName),
            comparisonWorkflow.Current.FixedTargets.Select(target => target.DisplayName));
        Assert.Equal(
            returnActions.Select(action => action.ActionId)
                .Concat(directActions.Select(action => action.ActionId)),
            comparisonWorkflow.Current.Actions.Select(action => action.ActionId));
    }

    [Fact]
    public void ResolveFromResourceDirectory_ReadsTheDirectNativeSelectedDirectory()
    {
        using var directory = new TemporaryDirectory();
        WriteAllValidFiles(directory.Path);
        var resourceDirectory = System.IO.Path.Combine(directory.Path, "Res");

        var composition = LegacyRuntimeUiCatalogLoader
            .ResolveFromResourceDirectory(resourceDirectory);

        Assert.Empty(composition.Warnings);
        AssertRuntimeFixedFile(
            directory.Path,
            "해외.ini",
            LegacyFixedMarket.Overseas,
            LegacyFixedActionCatalog.ExpectedOverseasCount,
            composition.FixedCatalog);
    }

    [Fact]
    public void UnknownFixedRow_FallsBackWithoutRegisteringAnAction()
    {
        using var directory = new TemporaryDirectory();
        WriteAllValidFiles(directory.Path);
        WriteFixedFile(
            directory.Path,
            LegacyFixedMarket.Overseas,
            LegacyFixedActionCatalog.Default.GetActions(LegacyFixedMarket.Overseas),
            (action, index) => index == 0 ? "untrusted-new-action" : action.Label);

        var composition = LegacyRuntimeUiCatalogLoader.ResolveFromBaseDirectory(
            directory.Path);

        var warning = Assert.Single(composition.Warnings);
        Assert.Equal("해외.ini", warning.FileName);
        Assert.Equal(LegacyRuntimeUiCatalogFailure.UnknownEntry, warning.Failure);
        Assert.Equal(
            LegacyFixedActionCatalog.Default.GetActions(LegacyFixedMarket.Overseas)
                .Select(action => action.Id),
            composition.FixedCatalog.GetActions(LegacyFixedMarket.Overseas)
                .Select(action => action.Id));
        Assert.DoesNotContain(
            composition.FixedCatalog.Actions,
            action => string.Equals(
                action.Label,
                "untrusted-new-action",
                StringComparison.Ordinal));
    }

    [Fact]
    public void ExcessDuplicateRow_FallsBackWithDuplicateWarning()
    {
        using var directory = new TemporaryDirectory();
        WriteAllValidFiles(directory.Path);
        WriteFixedFile(
            directory.Path,
            LegacyFixedMarket.Exchange,
            LegacyFixedActionCatalog.Default.GetActions(LegacyFixedMarket.Exchange),
            appendDuplicate: true);

        var composition = LegacyRuntimeUiCatalogLoader.ResolveFromBaseDirectory(
            directory.Path);

        var warning = Assert.Single(composition.Warnings);
        Assert.Equal("환율.ini", warning.FileName);
        Assert.Equal(LegacyRuntimeUiCatalogFailure.DuplicateEntry, warning.Failure);
        Assert.Equal(
            LegacyFixedActionCatalog.Default.GetActions(LegacyFixedMarket.Exchange)
                .Select(action => action.Id),
            composition.FixedCatalog.GetActions(LegacyFixedMarket.Exchange)
                .Select(action => action.Id));
    }

    [Fact]
    public void InvalidCp949_FallsBackOnlyForThatFile()
    {
        using var directory = new TemporaryDirectory();
        WriteAllValidFiles(directory.Path);
        File.WriteAllBytes(
            System.IO.Path.Combine(directory.Path, "Res", "지수.ini"),
            [0x81]);

        var composition = LegacyRuntimeUiCatalogLoader.ResolveFromBaseDirectory(
            directory.Path);

        var warning = Assert.Single(composition.Warnings);
        Assert.Equal("지수.ini", warning.FileName);
        Assert.Equal(LegacyRuntimeUiCatalogFailure.InvalidEncoding, warning.Failure);
        Assert.Equal(
            LegacyFixedActionCatalog.Default.GetActions(LegacyFixedMarket.Index)
                .Select(action => action.Id),
            composition.FixedCatalog.GetActions(LegacyFixedMarket.Index)
                .Select(action => action.Id));
    }

    [Fact]
    public void RowLimit_FailsClosedBeforeCatalogJoin()
    {
        using var directory = new TemporaryDirectory();
        WriteAllValidFiles(directory.Path);
        var section = LegacyFixedActionCatalog.Default
            .GetActions(LegacyFixedMarket.Overseas)[0].Section;
        WriteCp949(
            System.IO.Path.Combine(directory.Path, "Res", "해외.ini"),
            [$"[{section}]", .. Enumerable.Range(1, 51).Select(index => $"row-{index}")]);

        var composition = LegacyRuntimeUiCatalogLoader.ResolveFromBaseDirectory(
            directory.Path);

        var warning = Assert.Single(composition.Warnings);
        Assert.Equal("해외.ini", warning.FileName);
        Assert.Equal(LegacyRuntimeUiCatalogFailure.TooManyRows, warning.Failure);
    }

    [Fact]
    public void ComparisonDirectActionCannotBeReinterpretedAsAChildRow()
    {
        using var directory = new TemporaryDirectory();
        WriteAllValidFiles(directory.Path);
        var directActions = LegacyComparisonWorkflowCatalog.Actions
            .Where(action => action.Kind != LegacyComparisonActionKind.Return)
            .ToArray();
        var returnActions = LegacyComparisonWorkflowCatalog.Actions
            .Where(action => action.Kind == LegacyComparisonActionKind.Return)
            .ToArray();
        var lines = new List<string> { "[지수]" };
        lines.AddRange(LegacyComparisonWorkflowCatalog.FixedTargets
            .Select(target => target.DisplayName));
        foreach (var action in directActions)
        {
            lines.Add($"[{action.Label}]");
        }

        lines.Add($"[{returnActions[0].Section}]");
        lines.Add(directActions[0].Label);
        lines.AddRange(returnActions.Select(action => action.Label));
        WriteCp949(
            System.IO.Path.Combine(directory.Path, "Res", "종목비교.ini"),
            lines);

        var composition = LegacyRuntimeUiCatalogLoader.ResolveFromBaseDirectory(
            directory.Path);

        var warning = Assert.Single(composition.Warnings);
        Assert.Equal("종목비교.ini", warning.FileName);
        Assert.Equal(LegacyRuntimeUiCatalogFailure.UnknownEntry, warning.Failure);
        Assert.Equal(
            LegacyComparisonWorkflowCatalog.Actions.Select(action => action.ActionId),
            composition.ComparisonLayout.CreateActionViews(null, null)
                .Select(action => action.ActionId));
    }

    private static void WriteAllValidFiles(
        string baseDirectory,
        IReadOnlyDictionary<LegacyFixedMarket, LegacyFixedAction[]>? fixedOrders = null,
        IReadOnlyList<LegacyIndustryActionDefinition>? kospi = null,
        IReadOnlyList<LegacyIndustryActionDefinition>? kosdaq = null,
        IReadOnlyList<LegacyComparisonTarget>? targets = null,
        IReadOnlyList<LegacyComparisonActionDefinition>? directActions = null,
        IReadOnlyList<LegacyComparisonActionDefinition>? returnActions = null,
        bool returnSectionFirst = false)
    {
        Directory.CreateDirectory(System.IO.Path.Combine(baseDirectory, "Res"));
        foreach (var market in Enum.GetValues<LegacyFixedMarket>())
        {
            WriteFixedFile(
                baseDirectory,
                market,
                fixedOrders is not null && fixedOrders.TryGetValue(market, out var ordered)
                    ? ordered
                    : LegacyFixedActionCatalog.Default.GetActions(market));
        }

        WriteIndustryFile(
            baseDirectory,
            IndustryMarket.Kospi,
            kospi ?? LegacyIndustryActionCatalog.GetActions(IndustryMarket.Kospi));
        WriteIndustryFile(
            baseDirectory,
            IndustryMarket.Kosdaq,
            kosdaq ?? LegacyIndustryActionCatalog.GetActions(IndustryMarket.Kosdaq));
        WriteComparisonFile(
            baseDirectory,
            targets ?? LegacyComparisonWorkflowCatalog.FixedTargets,
            directActions ?? LegacyComparisonWorkflowCatalog.Actions
                .Where(action => action.Kind != LegacyComparisonActionKind.Return)
                .ToArray(),
            returnActions ?? LegacyComparisonWorkflowCatalog.Actions
                .Where(action => action.Kind == LegacyComparisonActionKind.Return)
                .ToArray(),
            returnSectionFirst);
    }

    private static void WriteFixedFile(
        string baseDirectory,
        LegacyFixedMarket market,
        IReadOnlyList<LegacyFixedAction> actions,
        Func<LegacyFixedAction, int, string>? labelSelector = null,
        bool appendDuplicate = false)
    {
        var lines = new List<string>();
        var rowIndex = 0;
        foreach (var section in actions.GroupBy(action => action.Section, StringComparer.Ordinal))
        {
            lines.Add($"[{section.Key}]");
            foreach (var action in section)
            {
                lines.Add(labelSelector?.Invoke(action, rowIndex) ?? action.Label);
                rowIndex++;
            }

            if (appendDuplicate)
            {
                lines.Add(section.First().Label);
                appendDuplicate = false;
            }
        }

        WriteCp949(
            System.IO.Path.Combine(baseDirectory, "Res", FixedFileName(market)),
            lines);
    }

    private static IEnumerable<LegacyFixedAction> FlattenForIni(
        IEnumerable<LegacyFixedAction> actions) =>
        actions.GroupBy(action => action.Section, StringComparer.Ordinal)
            .SelectMany(group => group);

    private static void WriteIndustryFile(
        string baseDirectory,
        IndustryMarket market,
        IReadOnlyList<LegacyIndustryActionDefinition> actions)
    {
        var section = market == IndustryMarket.Kospi ? "코스피" : "코스닥";
        WriteCp949(
            System.IO.Path.Combine(baseDirectory, "Res", IndustryFileName(market)),
            [$"[{section}]", .. actions.Select(action => action.Label)]);
    }

    private static void WriteComparisonFile(
        string baseDirectory,
        IReadOnlyList<LegacyComparisonTarget> targets,
        IReadOnlyList<LegacyComparisonActionDefinition> directActions,
        IReadOnlyList<LegacyComparisonActionDefinition> returnActions,
        bool returnSectionFirst)
    {
        var lines = new List<string> { "[지수]" };
        lines.AddRange(targets.Select(target => target.DisplayName));
        void AddDirectSections()
        {
            foreach (var action in directActions)
            {
                lines.Add($"[{action.Label}]");
            }
        }

        void AddReturnSection()
        {
            lines.Add($"[{returnActions[0].Section}]");
            lines.AddRange(returnActions.Select(action => action.Label));
        }

        if (returnSectionFirst)
        {
            AddReturnSection();
            AddDirectSections();
        }
        else
        {
            AddDirectSections();
            AddReturnSection();
        }

        WriteCp949(
            System.IO.Path.Combine(baseDirectory, "Res", "종목비교.ini"),
            lines);
    }

    private static void WriteCp949(string path, IEnumerable<string> lines) =>
        File.WriteAllText(path, string.Join("\r\n", lines) + "\r\n", Cp949);

    private static void AssertRuntimeFixedFile(
        string baseDirectory,
        string fileName,
        LegacyFixedMarket market,
        int expectedCount,
        LegacyFixedActionCatalog catalog)
    {
        var document = ReadTestIni(baseDirectory, fileName);
        var actions = catalog.GetActions(market);
        Assert.Equal(expectedCount, actions.Count);
        Assert.Equal(
            document.SelectMany(section => section.Rows),
            actions.Select(action => action.Label));
        Assert.Equal(
            document.SelectMany(section => section.Rows.Select(_ => section.Name)),
            actions.Select(action => action.Section));
    }

    private static void AssertRuntimeIndustryFile(
        string baseDirectory,
        string fileName,
        IndustryMarket market,
        LegacyIndustryActionLayout layout)
    {
        var section = Assert.Single(ReadTestIni(baseDirectory, fileName));
        var actions = layout.GetActions(market);
        Assert.Equal(LegacyIndustryActionCatalog.ActionsPerMarket, actions.Count);
        Assert.Equal(section.Rows, actions.Select(action => action.Label));
        Assert.All(actions, action => Assert.Equal(section.Name, action.Section));
    }

    private static void AssertRuntimeComparisonFile(
        string baseDirectory,
        LegacyComparisonUiLayout layout)
    {
        var document = ReadTestIni(baseDirectory, "종목비교.ini");
        Assert.Equal(6, document.Count);
        Assert.Equal(24, layout.FixedTargets.Count);
        Assert.Equal(
            document[0].Rows,
            layout.FixedTargets.Select(target => target.DisplayName));
        var expectedActions = document.Skip(1).SelectMany(section =>
            section.Rows.Count == 0
                ? new[] { (Label: section.Name, Section: section.Name) }
                : section.Rows.Select(row => (Label: row, Section: section.Name)));
        var actualActions = layout.CreateActionViews(null, null);
        Assert.Equal(9, actualActions.Count);
        Assert.Equal(
            expectedActions,
            actualActions.Select(action => (action.Label, action.Section)));
    }

    private static IReadOnlyList<TestIniSection> ReadTestIni(
        string baseDirectory,
        string fileName)
    {
        var sections = new List<TestIniSection>();
        List<string>? rows = null;
        foreach (var source in File.ReadAllLines(
                     System.IO.Path.Combine(baseDirectory, "Res", fileName),
                     Cp949))
        {
            var line = source.Trim();
            if (line.Length == 0)
            {
                continue;
            }

            if (line[0] == '[' && line[^1] == ']')
            {
                rows = [];
                sections.Add(new TestIniSection(line[1..^1].Trim(), rows));
            }
            else
            {
                Assert.NotNull(rows);
                rows.Add(line);
            }
        }

        return sections;
    }

    private static string FixedFileName(LegacyFixedMarket market) => market switch
    {
        LegacyFixedMarket.Overseas => "해외.ini",
        LegacyFixedMarket.Exchange => "환율.ini",
        LegacyFixedMarket.Index => "지수.ini",
        _ => throw new ArgumentOutOfRangeException(nameof(market), market, null)
    };

    private static string IndustryFileName(IndustryMarket market) => market switch
    {
        IndustryMarket.Kospi => "업종_코스피.ini",
        IndustryMarket.Kosdaq => "업종_코스닥.ini",
        _ => throw new ArgumentOutOfRangeException(nameof(market), market, null)
    };

    private static Encoding CreateCp949Encoding()
    {
        Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
        return Encoding.GetEncoding(
            949,
            EncoderFallback.ExceptionFallback,
            DecoderFallback.ExceptionFallback);
    }

    private sealed class EmptyIndustrySelectionService : IIndustrySelectionService
    {
        public Task<IReadOnlyList<IndustrySelection>> GetAsync(
            IndustryMarket market,
            CancellationToken cancellationToken = default) =>
            Task.FromResult<IReadOnlyList<IndustrySelection>>(
                Array.Empty<IndustrySelection>());
    }

    private sealed class EmptyComparisonDataService : ILegacyComparisonDataService
    {
        public Task<IReadOnlyList<LegacyComparisonTarget>> SearchDomesticAsync(
            string query,
            int maximumResults,
            CancellationToken cancellationToken = default) =>
            Task.FromResult<IReadOnlyList<LegacyComparisonTarget>>(
                Array.Empty<LegacyComparisonTarget>());

        public Task<IReadOnlyList<LegacyComparisonTarget>> SearchWorldAsync(
            string query,
            int maximumResults,
            CancellationToken cancellationToken = default) =>
            Task.FromResult<IReadOnlyList<LegacyComparisonTarget>>(
                Array.Empty<LegacyComparisonTarget>());

        public Task<LegacyComparisonTarget?> ResolveStoredTargetAsync(
            LegacyComparisonStoredTargetReference target,
            CancellationToken cancellationToken = default) =>
            Task.FromResult<LegacyComparisonTarget?>(null);
    }

    private sealed class EmptyComparisonPairStore : ILegacyComparisonPairStore
    {
        public Task<byte[]?> ReadCp949Async(
            CancellationToken cancellationToken = default) =>
            Task.FromResult<byte[]?>(null);

        public Task WriteCp949Async(
            IReadOnlyList<LegacyComparisonPairWriteRow> rows,
            CancellationToken cancellationToken = default) =>
            Task.CompletedTask;
    }

    private sealed class TemporaryDirectory : IDisposable
    {
        public TemporaryDirectory()
        {
            Path = System.IO.Path.Combine(
                System.IO.Path.GetTempPath(),
                $"MBN_STOCK_WEBVIEW_UI_INI_{Guid.NewGuid():N}");
            Directory.CreateDirectory(Path);
        }

        public string Path { get; }

        public void Dispose()
        {
            if (Directory.Exists(Path))
            {
                Directory.Delete(Path, recursive: true);
            }
        }
    }

    private sealed record TestIniSection(string Name, IReadOnlyList<string> Rows);
}
