using System.Text;
using MBN_STOCK_WEBVIEW.LegacyApplication;

namespace MBN_STOCK_WEBVIEW.LegacyApplication.Tests;

public sealed class LegacyRuntimeStockCutMenuLoaderTests
{
    [Fact]
    public void TryParse_UsesCp949AndPreservesTrimmedPhysicalOrder()
    {
        const string first = "1열판기본_현재가";
        Assert.Equal(first, LegacyStockCutCatalog.Rows[1].RawLabel);
        var second = LegacyStockCutCatalog.Rows[0].RawLabel;
        var bytes = EncodeCp949($"  \r\n  [종목]  \r\n  {first}  \r\n\r\n - \r\n{second}\r\n{first}\r\n");

        var loaded = LegacyRuntimeStockCutMenuLoader.TryParse(
            bytes,
            out var rows,
            out var failure);

        Assert.True(loaded);
        Assert.Equal(LegacyRuntimeStockCutMenuFailure.None, failure);
        Assert.Collection(
            rows,
            row => Assert.Equal((0, 1, first, false), Fields(row)),
            row => Assert.Equal((1, null, string.Empty, true), Fields(row)),
            row => Assert.Equal((2, 2, second, false), Fields(row)),
            row => Assert.Equal((3, 3, first, false), Fields(row)));
    }

    [Fact]
    public void TryLoad_ReadsAStandaloneRuntimeFileWithoutBundlingTheAsset()
    {
        var directory = Path.Combine(
            Path.GetTempPath(),
            $"mbn-stock-cut-menu-{Guid.NewGuid():N}");
        var path = Path.Combine(directory, "종목.ini");
        Directory.CreateDirectory(directory);
        try
        {
            var label = LegacyStockCutCatalog.Rows[0].RawLabel;
            File.WriteAllBytes(path, EncodeCp949($"[종목]\r\n{label}\r\n"));

            var loaded = LegacyRuntimeStockCutMenuLoader.TryLoad(
                path,
                out var rows,
                out var failure);

            Assert.True(loaded);
            Assert.Equal(LegacyRuntimeStockCutMenuFailure.None, failure);
            var row = Assert.Single(rows);
            Assert.Equal((0, 1, label, false), Fields(row));
        }
        finally
        {
            Directory.Delete(directory, recursive: true);
        }
    }

    [Theory]
    [InlineData(50, true, LegacyRuntimeStockCutMenuFailure.None)]
    [InlineData(51, false, LegacyRuntimeStockCutMenuFailure.TooManyRows)]
    public void TryParse_EnforcesTheLegacyFiftyPhysicalRowLimit(
        int rowCount,
        bool expectedSuccess,
        LegacyRuntimeStockCutMenuFailure expectedFailure)
    {
        var label = LegacyStockCutCatalog.Rows[0].RawLabel;
        var text = LegacyRuntimeStockCutMenuLoader.StockSectionHeader + "\r\n" +
            string.Join("\r\n", Enumerable.Repeat(label, rowCount));

        var loaded = LegacyRuntimeStockCutMenuLoader.TryParse(
            EncodeCp949(text),
            out var rows,
            out var failure);

        Assert.Equal(expectedSuccess, loaded);
        Assert.Equal(expectedFailure, failure);
        Assert.Equal(expectedSuccess ? rowCount : 0, rows.Count);
    }

    public static TheoryData<string, LegacyRuntimeStockCutMenuFailure> MalformedMenus => new()
    {
        { string.Empty, LegacyRuntimeStockCutMenuFailure.InvalidFormat },
        { "[종목", LegacyRuntimeStockCutMenuFailure.InvalidFormat },
        { LegacyStockCutCatalog.Rows[0].RawLabel, LegacyRuntimeStockCutMenuFailure.InvalidFormat },
        { "[다른메뉴]\r\n-", LegacyRuntimeStockCutMenuFailure.InvalidFormat },
        { "[종목]\r\n[다른메뉴]", LegacyRuntimeStockCutMenuFailure.InvalidFormat },
        { "[종목]", LegacyRuntimeStockCutMenuFailure.EmptyMenu },
        { "[종목]\r\n-", LegacyRuntimeStockCutMenuFailure.EmptyMenu },
        { "[종목]\r\n지원하지않는_컷", LegacyRuntimeStockCutMenuFailure.UnknownCut }
    };

    [Theory]
    [MemberData(nameof(MalformedMenus))]
    public void TryParse_FailsClosedWithoutReturningPartialRows(
        string text,
        LegacyRuntimeStockCutMenuFailure expectedFailure)
    {
        var loaded = LegacyRuntimeStockCutMenuLoader.TryParse(
            EncodeCp949(text),
            out var rows,
            out var failure);

        Assert.False(loaded);
        Assert.Empty(rows);
        Assert.Equal(expectedFailure, failure);
    }

    [Fact]
    public void TryParse_RejectsAnIncompleteCp949Sequence()
    {
        var loaded = LegacyRuntimeStockCutMenuLoader.TryParse(
            new byte[] { 0x81 },
            out var rows,
            out var failure);

        Assert.False(loaded);
        Assert.Empty(rows);
        Assert.Equal(LegacyRuntimeStockCutMenuFailure.InvalidEncoding, failure);
    }

    [Fact]
    public void TryLoad_MissingOrBlankPathLetsTheCallerUseTheBuiltInCatalog()
    {
        var missing = Path.Combine(
            Path.GetTempPath(),
            $"missing-cut-menu-{Guid.NewGuid():N}.ini");

        Assert.False(LegacyRuntimeStockCutMenuLoader.TryLoad(
            missing,
            out var missingRows,
            out var missingFailure));
        Assert.Empty(missingRows);
        Assert.Equal(LegacyRuntimeStockCutMenuFailure.FileNotFound, missingFailure);

        Assert.False(LegacyRuntimeStockCutMenuLoader.TryLoad(
            " ",
            out var blankRows,
            out var blankFailure));
        Assert.Empty(blankRows);
        Assert.Equal(LegacyRuntimeStockCutMenuFailure.InvalidPath, blankFailure);
        Assert.NotEmpty(LegacyStockCutCatalog.Rows);
    }

    [Fact]
    public void TryLoad_RejectsAnOversizedFileBeforeAllocatingOrDecodingItsContent()
    {
        var path = Path.Combine(
            Path.GetTempPath(),
            $"oversized-cut-menu-{Guid.NewGuid():N}.ini");
        try
        {
            using (var stream = new FileStream(path, FileMode.CreateNew, FileAccess.Write))
            {
                stream.SetLength(LegacyRuntimeStockCutMenuLoader.MaximumFileBytes + 1L);
            }

            var loaded = LegacyRuntimeStockCutMenuLoader.TryLoad(
                path,
                out var rows,
                out var failure);

            Assert.False(loaded);
            Assert.Empty(rows);
            Assert.Equal(LegacyRuntimeStockCutMenuFailure.FileTooLarge, failure);
        }
        finally
        {
            File.Delete(path);
        }
    }

    [Fact]
    public void ResolveFromBaseDirectory_MissingFileReturnsBoundedPathFreeFallback()
    {
        var directory = Path.Combine(
            Path.GetTempPath(),
            $"mbn-stock-cut-composition-{Guid.NewGuid():N}");
        Directory.CreateDirectory(directory);
        try
        {
            var composition =
                LegacyRuntimeStockCutMenuLoader.ResolveFromBaseDirectory(directory);

            Assert.True(composition.UsesBuiltInCatalog);
            Assert.Equal(LegacyRuntimeStockCutMenuSource.BuiltIn, composition.Source);
            Assert.Same(LegacyStockCutCatalog.Rows, composition.Rows);
            Assert.Equal(
                LegacyRuntimeStockCutMenuFailure.FileNotFound,
                composition.Failure);
            Assert.Equal(
                LegacyRuntimeStockCutMenuLoader.FallbackWarningMessage,
                composition.WarningMessage);
            var warning = Assert.IsType<string>(composition.WarningMessage);
            Assert.InRange(warning.Length, 1, 80);
            Assert.DoesNotContain(directory, warning,
                StringComparison.OrdinalIgnoreCase);
            Assert.DoesNotContain("종목.ini", warning,
                StringComparison.Ordinal);
            Assert.DoesNotContain('\\', warning);
            Assert.DoesNotContain('/', warning);
            var controller = new LegacyOperatorController(
                new EmptyStockLookup(),
                cutRows: composition.Rows);
            var warningState = controller.ReportWarning(warning);
            Assert.Equal(LegacyOperatorStatusKind.Warning, warningState.StatusKind);
            Assert.Equal(composition.WarningMessage, warningState.StatusMessage);

            var resourceDirectory = Path.Combine(directory, "Res");
            Directory.CreateDirectory(resourceDirectory);
            File.WriteAllBytes(
                Path.Combine(resourceDirectory, "종목.ini"),
                EncodeCp949("[종목]\r\n지원하지않는_컷\r\n"));
            var invalid =
                LegacyRuntimeStockCutMenuLoader.ResolveFromBaseDirectory(directory);
            Assert.True(invalid.UsesBuiltInCatalog);
            Assert.Same(LegacyStockCutCatalog.Rows, invalid.Rows);
            Assert.Equal(LegacyRuntimeStockCutMenuFailure.UnknownCut, invalid.Failure);
            Assert.Equal(composition.WarningMessage, invalid.WarningMessage);
        }
        finally
        {
            Directory.Delete(directory, recursive: true);
        }
    }

    [Fact]
    public void ResolveFromBaseDirectory_ValidRuntimeFileReturnsOnlyItsRows()
    {
        var directory = Path.Combine(
            Path.GetTempPath(),
            $"mbn-stock-cut-composition-{Guid.NewGuid():N}");
        var resourceDirectory = Path.Combine(directory, "Res");
        Directory.CreateDirectory(resourceDirectory);
        try
        {
            const string label = "1열판기본_현재가";
            File.WriteAllBytes(
                Path.Combine(resourceDirectory, "종목.ini"),
                EncodeCp949($"[종목]\r\n{label}\r\n"));

            var composition =
                LegacyRuntimeStockCutMenuLoader.ResolveFromBaseDirectory(directory);

            Assert.False(composition.UsesBuiltInCatalog);
            Assert.Equal(LegacyRuntimeStockCutMenuSource.Packaged, composition.Source);
            Assert.Equal(LegacyRuntimeStockCutMenuFailure.None, composition.Failure);
            Assert.Null(composition.WarningMessage);
            Assert.Equal(label, Assert.Single(composition.Rows).RawLabel);
        }
        finally
        {
            Directory.Delete(directory, recursive: true);
        }
    }

    [Fact]
    public void ResolveFromResourceDirectory_ReadsTheDirectNativeSelectedDirectory()
    {
        var directory = Path.Combine(
            Path.GetTempPath(),
            $"mbn-stock-cut-direct-resource-{Guid.NewGuid():N}");
        Directory.CreateDirectory(directory);
        try
        {
            WriteMenu(Path.Combine(directory, "종목.ini"), "1열판기본_현재가");

            var composition = LegacyRuntimeStockCutMenuLoader
                .ResolveFromResourceDirectory(directory);

            Assert.Equal(
                LegacyRuntimeStockCutMenuSource.ConfiguredResourceDirectory,
                composition.Source);
            Assert.Equal(
                "1열판기본_현재가",
                Assert.Single(composition.Rows).RawLabel);
        }
        finally
        {
            Directory.Delete(directory, recursive: true);
        }
    }

    [Fact]
    public void ResolveFromCandidates_ExternalOverrideTakesPrecedenceOverPackagedMenu()
    {
        var paths = CreateCandidatePaths();
        try
        {
            WriteMenu(paths.ExternalPath, "1열판기본_현재가");
            WriteMenu(paths.PackagedPath, "1열판기본_예상체결가");

            var composition = LegacyRuntimeStockCutMenuLoader.ResolveFromCandidates(
                paths.ExternalPath,
                paths.PackagedBaseDirectory);

            Assert.Equal(
                LegacyRuntimeStockCutMenuSource.ExternalOverride,
                composition.Source);
            Assert.False(composition.UsesBuiltInCatalog);
            Assert.Null(composition.WarningMessage);
            Assert.Equal(
                "1열판기본_현재가",
                Assert.Single(composition.Rows).RawLabel);
        }
        finally
        {
            Directory.Delete(paths.Root, recursive: true);
        }
    }

    [Fact]
    public void ResolveFromCandidates_MissingExternalContinuesToPackagedMenu()
    {
        var paths = CreateCandidatePaths();
        try
        {
            WriteMenu(paths.PackagedPath, "1열판기본_예상체결가");

            var composition = LegacyRuntimeStockCutMenuLoader.ResolveFromCandidates(
                paths.ExternalPath,
                paths.PackagedBaseDirectory);

            Assert.Equal(LegacyRuntimeStockCutMenuSource.Packaged, composition.Source);
            Assert.False(composition.UsesBuiltInCatalog);
            Assert.Null(composition.WarningMessage);
            Assert.Equal(
                "1열판기본_예상체결가",
                Assert.Single(composition.Rows).RawLabel);
        }
        finally
        {
            Directory.Delete(paths.Root, recursive: true);
        }
    }

    [Fact]
    public void ResolveFromCandidates_InvalidExternalFailsClosedWithoutPackagedFallback()
    {
        var paths = CreateCandidatePaths();
        try
        {
            WriteMenu(paths.ExternalPath, "지원하지않는_컷");
            WriteMenu(paths.PackagedPath, "1열판기본_예상체결가");

            var composition = LegacyRuntimeStockCutMenuLoader.ResolveFromCandidates(
                paths.ExternalPath,
                paths.PackagedBaseDirectory);

            Assert.Equal(LegacyRuntimeStockCutMenuSource.BuiltIn, composition.Source);
            Assert.True(composition.UsesBuiltInCatalog);
            Assert.Same(LegacyStockCutCatalog.Rows, composition.Rows);
            Assert.Equal(LegacyRuntimeStockCutMenuFailure.UnknownCut, composition.Failure);
            Assert.Equal(
                LegacyRuntimeStockCutMenuLoader.FallbackWarningMessage,
                composition.WarningMessage);
        }
        finally
        {
            Directory.Delete(paths.Root, recursive: true);
        }
    }

    [Fact]
    public void ResolveFromBaseDirectory_RejectsInvalidOrControlContainingPathsWithoutLeakage()
    {
        string?[] invalidPaths =
        [
            null,
            string.Empty,
            " ",
            "relative\\package",
            "C:\\sensitive-root\nprivate",
            "C:\\sensitive-root\0private"
        ];

        foreach (var invalidPath in invalidPaths)
        {
            var composition =
                LegacyRuntimeStockCutMenuLoader.ResolveFromBaseDirectory(invalidPath);

            Assert.True(composition.UsesBuiltInCatalog);
            Assert.Equal(LegacyRuntimeStockCutMenuSource.BuiltIn, composition.Source);
            Assert.Same(LegacyStockCutCatalog.Rows, composition.Rows);
            Assert.Equal(
                LegacyRuntimeStockCutMenuFailure.InvalidPath,
                composition.Failure);
            Assert.Equal(
                LegacyRuntimeStockCutMenuLoader.FallbackWarningMessage,
                composition.WarningMessage);
            Assert.DoesNotContain("sensitive-root", composition.WarningMessage!,
                StringComparison.OrdinalIgnoreCase);
        }
    }

    private static (int PhysicalIndex, int? DisplayOrdinal, string RawLabel, bool IsSeparator)
        Fields(LegacyCutCatalogRow row) =>
        (row.PhysicalIndex, row.DisplayOrdinal, row.RawLabel, row.IsSeparator);

    private static byte[] EncodeCp949(string text)
    {
        Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
        var cp949 = Encoding.GetEncoding(
            949,
            EncoderFallback.ExceptionFallback,
            DecoderFallback.ExceptionFallback);
        return cp949.GetBytes(text);
    }

    private static (
        string Root,
        string ExternalPath,
        string PackagedBaseDirectory,
        string PackagedPath) CreateCandidatePaths()
    {
        var root = Path.Combine(
            Path.GetTempPath(),
            $"mbn-stock-cut-candidates-{Guid.NewGuid():N}");
        var externalPath = Path.Combine(
            root,
            "local",
            "MBN_STOCK_WEBVIEW",
            "Res",
            "종목.ini");
        var packagedBaseDirectory = Path.Combine(root, "package");
        return (
            root,
            externalPath,
            packagedBaseDirectory,
            Path.Combine(packagedBaseDirectory, "Res", "종목.ini"));
    }

    private static void WriteMenu(string path, string label)
    {
        Directory.CreateDirectory(Path.GetDirectoryName(path)!);
        File.WriteAllBytes(path, EncodeCp949($"[종목]\r\n{label}\r\n"));
    }

    private sealed class EmptyStockLookup : ILegacyStockLookup
    {
        public Task<LegacyStockSearchData> SearchAsync(
            string query,
            CancellationToken cancellationToken = default) =>
            Task.FromResult(new LegacyStockSearchData(query, [], []));
    }
}
