#nullable enable

using System.Data;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using MMoneyCoderSharp.Data;

namespace MBN_STOCK_WEBVIEW.Core.Tests;

public sealed class LegacyComparisonPairImportServiceTests
{
    private static readonly Encoding Cp949 = CreateCp949();

    [Fact]
    public void Parser_AcceptsTheExactUc3NineFieldCp949Shape()
    {
        var rows = LegacyComparisonFileParser.Parse(Encode(
            "1^삼성전자,삼성전자(NXT)^코스피^코스피_NXT^^^^^\r\n" +
            "2^코스닥 지수,코스피 지수^^^^^^^\r\n" +
            "3^엔비디아,삼성전자^^코스피^^^^^\r\n"));

        Assert.Collection(
            rows,
            row =>
            {
                Assert.Equal(1, row.RowNumber);
                Assert.Equal(new LegacyComparisonFileTarget("삼성전자", "코스피"), row.First);
                Assert.Equal(
                    new LegacyComparisonFileTarget("삼성전자(NXT)", "코스피_NXT"),
                    row.Second);
            },
            row =>
            {
                Assert.Equal(2, row.RowNumber);
                Assert.Equal(string.Empty, row.First.Market);
                Assert.Equal(string.Empty, row.Second.Market);
            },
            row =>
            {
                Assert.Equal(3, row.RowNumber);
                Assert.Equal("엔비디아", row.First.Name);
                Assert.Equal("코스피", row.Second.Market);
            });
    }

    [Fact]
    public void Parser_AcceptsTheAuditedEightRowOriginalFixture()
    {
        var bytes = Encode(
            "1^삼성전자,삼성전자(NXT)^코스피^코스피_NXT^^^^^\r\n" +
            "2^기아(NXT),기아^코스피_NXT^코스피^^^^^\r\n" +
            "3^기아(NXT),삼성전자(NXT)^코스피_NXT^코스피_NXT^^^^^\r\n" +
            "4^SK하이닉스,삼성전자^코스피^코스피^^^^^\r\n" +
            "5^에이비엘바이오(NXT),에이비엘바이오^코스닥_NXT^코스닥^^^^^\r\n" +
            "6^코스닥 지수,코스피 지수^^^^^^^\r\n" +
            "7^엔비디아,삼성전자^^코스피^^^^^\r\n" +
            "8^삼성전자(NXT),엔비디아^코스피_NXT^^^^^^\r\n");

        Assert.Equal(
            "1EE76BC464FB1C44C8B6546E99CDC21C726C0E2C24AD344F5C19AE41BFC0D2F2",
            Convert.ToHexString(SHA256.HashData(bytes)));
        var rows = LegacyComparisonFileParser.Parse(bytes);
        Assert.Equal(8, rows.Count);
        Assert.Equal("엔비디아", rows[7].Second.Name);
        Assert.Equal(string.Empty, rows[7].Second.Market);
    }

    [Theory]
    [InlineData("1^삼성전자,기아^코스피^코스피^^^^\r\n")]
    [InlineData("01^삼성전자,기아^코스피^코스피^^^^^\r\n")]
    [InlineData("2^삼성전자,기아^코스피^코스피^^^^^\r\n")]
    [InlineData("1^삼성전자^코스피^코스피^^^^^\r\n")]
    [InlineData("1^삼성전자,기아^코스피^코스피^unexpected^^^^\r\n")]
    [InlineData("1^삼성전자(NXT),기아^코스피^코스피^^^^^\r\n")]
    [InlineData("1^삼성전자,기아^코스피_NXT^코스피^^^^^\r\n")]
    [InlineData("1^ 삼성전자,기아^코스피^코스피^^^^^\r\n")]
    public void Parser_RejectsMalformedRows(string text)
    {
        var exception = Assert.Throws<LegacyComparisonImportException>(
            () => LegacyComparisonFileParser.Parse(Encode(text)));

        Assert.Equal(LegacyComparisonImportFailure.SourceInvalid, exception.Failure);
    }

    [Fact]
    public void Parser_AllowsDuplicatePairsAsDistinctOrderedRows()
    {
        var duplicate = Encode(
            "1^삼성전자,기아^코스피^코스피^^^^^\r\n" +
            "2^삼성전자,기아^코스피^코스피^^^^^\r\n");

        var rows = LegacyComparisonFileParser.Parse(duplicate);

        Assert.Collection(
            rows,
            row => Assert.Equal(1, row.RowNumber),
            row => Assert.Equal(2, row.RowNumber));
        Assert.Equal(rows[0].First, rows[1].First);
        Assert.Equal(rows[0].Second, rows[1].Second);
    }

    [Fact]
    public void Parser_RejectsInvalidCp949()
    {
        Assert.Equal(
            LegacyComparisonImportFailure.SourceInvalid,
            Assert.Throws<LegacyComparisonImportException>(
                () => LegacyComparisonFileParser.Parse([0x81])).Failure);
    }

    [Fact]
    public async Task TrustedSource_UsesOnlyTheFixedReadOnlyPathAndReturnsItsDigest()
    {
        using var directory = new TemporaryDirectory();
        var source = new TrustedLegacyComparisonFileSource(directory.Path);
        var expected = Encode("1^코스닥 지수,코스피 지수^^^^^^^\r\n");
        Directory.CreateDirectory(Path.GetDirectoryName(source.SourcePath)!);
        await File.WriteAllBytesAsync(source.SourcePath, expected);

        var result = await source.ReadAsync();

        Assert.Equal(expected, result.Bytes);
        Assert.Equal(Convert.ToHexString(SHA256.HashData(expected)), result.Sha256);
        Assert.Equal(
            Path.GetFullPath(Path.Combine(directory.Path, TrustedLegacyComparisonFileSource.FixedRelativePath)),
            source.SourcePath);
    }

    [Fact]
    public async Task TrustedSource_RejectsMissingAndOversizedFiles()
    {
        using var directory = new TemporaryDirectory();
        var source = new TrustedLegacyComparisonFileSource(directory.Path);

        var missing = await Assert.ThrowsAsync<LegacyComparisonImportException>(
            () => source.ReadAsync());
        Assert.Equal(LegacyComparisonImportFailure.SourceUnavailable, missing.Failure);

        Directory.CreateDirectory(Path.GetDirectoryName(source.SourcePath)!);
        await File.WriteAllBytesAsync(
            source.SourcePath,
            new byte[TrustedLegacyComparisonFileSource.MaximumFileBytes + 1]);
        var oversized = await Assert.ThrowsAsync<LegacyComparisonImportException>(
            () => source.ReadAsync());
        Assert.Equal(LegacyComparisonImportFailure.SourceUnavailable, oversized.Failure);
    }

    [Fact]
    public async Task TrustedSource_RejectsAHardLinkedFileEvenWhenItsBytesAreValid()
    {
        if (!OperatingSystem.IsWindows())
        {
            return;
        }

        using var directory = new TemporaryDirectory();
        var source = new TrustedLegacyComparisonFileSource(directory.Path);
        Directory.CreateDirectory(Path.GetDirectoryName(source.SourcePath)!);
        var outside = Path.Combine(directory.Path, "outside-valid-comparison.dat");
        await File.WriteAllBytesAsync(
            outside,
            Encode("1^코스피 지수,코스닥 지수^^^^^^\r\n"));
        Assert.True(CreateHardLinkW(source.SourcePath, outside, IntPtr.Zero));

        var error = await Assert.ThrowsAsync<LegacyComparisonImportException>(
            () => source.ReadAsync());

        Assert.Equal(LegacyComparisonImportFailure.SourceUnavailable, error.Failure);
    }

    [Fact]
    public async Task ImportAsync_ResolvesEveryTargetKindWithBoundExactReadsAndCachesIdentity()
    {
        var bytes = Encode(
            "1^삼성전자,삼성전자(NXT)^코스피^코스피_NXT^^^^^\r\n" +
            "2^코스닥 지수,삼성전자^^코스피^^^^^\r\n" +
            "3^엔비디아,삼성전자^^코스피^^^^^\r\n");
        var source = new MemorySource(bytes);
        var executor = new RecordingExecutor(call => call.TableName switch
        {
            "LEGACY_COMPARISON_IMPORT_KOSPI" => StockTable("삼성전자", "005930"),
            "LEGACY_COMPARISON_IMPORT_NXT_KOSPI" => StockTable("삼성전자", "005930"),
            "LEGACY_COMPARISON_IMPORT_WORLD_STOCK" =>
                string.Equals(
                    call.Spec.Parameters[0].Value as string,
                    "엔비디아",
                    StringComparison.Ordinal)
                    ? WorldTable("엔비디아", "NAS@NVDA", "US")
                    : WorldTable(),
            _ => throw new InvalidOperationException("Unexpected identity query.")
        });
        var service = new LegacyComparisonPairImportService(executor, source);

        var result = await service.ImportAsync();

        Assert.Equal(3, result.SourceRowCount);
        Assert.Equal(source.Digest, result.SourceSha256);
        Assert.Collection(
            result.Pairs,
            row =>
            {
                Assert.Equal(LegacyComparisonImportTargetKind.KrxStock, row.First.Kind);
                Assert.Equal("kospi", row.First.Market);
                Assert.Equal("005930", row.First.StockCode);
                Assert.Equal(LegacyComparisonImportTargetKind.NxtStock, row.Second.Kind);
                Assert.Equal("삼성전자", row.Second.StockName);
            },
            row =>
            {
                Assert.Equal("Kosdaq", row.First.MarketTarget);
                Assert.Equal(LegacyComparisonImportTargetKind.MarketTarget, row.First.Kind);
                Assert.Equal("005930", row.Second.StockCode);
            },
            row =>
            {
                Assert.Equal(LegacyComparisonImportTargetKind.WorldStock, row.First.Kind);
                Assert.Equal("NAS@NVDA", row.First.Symbol);
                Assert.Equal("US", row.First.Nation);
                Assert.Equal("005930", row.Second.StockCode);
            });

        Assert.Equal(4, executor.Calls.Count);
        Assert.Equal(0, executor.StringSqlCallCount);
        Assert.All(executor.Calls, call =>
        {
            call.Spec.ValidateFor(call.Source);
            Assert.Single(call.Spec.Parameters);
            Assert.DoesNotContain(
                Assert.IsType<string>(call.Spec.Parameters[0].Value),
                call.Spec.Sql,
                StringComparison.Ordinal);
        });
        Assert.Equal("삼성전자", executor.Calls[0].Spec.Parameters[0].Value);
        Assert.Equal("삼성전자", executor.Calls[1].Spec.Parameters[0].Value);
        Assert.Equal("코스닥 지수", executor.Calls[2].Spec.Parameters[0].Value);
        Assert.Equal("엔비디아", executor.Calls[3].Spec.Parameters[0].Value);
    }

    [Fact]
    public async Task ImportAsync_PreservesDuplicatePairsAsDistinctStableRows()
    {
        var bytes = Encode(
            "1^삼성전자,기아^코스피^코스피^^^^^\r\n" +
            "2^삼성전자,기아^코스피^코스피^^^^^\r\n");
        var executor = new RecordingExecutor(call =>
            string.Equals(
                call.Spec.Parameters[0].Value as string,
                "삼성전자",
                StringComparison.Ordinal)
                ? StockTable("삼성전자", "005930")
                : StockTable("기아", "000270"));
        var service = new LegacyComparisonPairImportService(
            executor,
            new MemorySource(bytes));

        var result = await service.ImportAsync();

        Assert.Equal(2, result.SourceRowCount);
        Assert.Equal([1, 2], result.Pairs.Select(static pair => pair.RowNumber));
        Assert.Equal(result.Pairs[0].First, result.Pairs[1].First);
        Assert.Equal(result.Pairs[0].Second, result.Pairs[1].Second);
        Assert.Equal(2, executor.Calls.Count);
    }

    [Fact]
    public async Task ImportAsync_MapsAllTwentyFourOriginalFixedTargetsExactly()
    {
        (string Name, string Target)[] expected =
        [
            ("코스피 지수", "Kospi"),
            ("코스닥 지수", "Kosdaq"),
            ("코스피200 지수", "Kospi200"),
            ("선물 지수", "Futures"),
            ("KRX100지수", "Krx100"),
            ("환율-원달러", "WonDollar"),
            ("환율-원엔", "WonYen"),
            ("환율-원위엔", "WonYuan"),
            ("환율-원유로", "WonEuro"),
            ("해외지수-다우", "Dow"),
            ("해외지수-나스닥", "Nasdaq"),
            ("해외지수-S&P", "Sp500"),
            ("해외지수-독일", "GermanyDax"),
            ("해외지수-영국", "UnitedKingdomFtse"),
            ("해외지수-프랑스", "FranceCac"),
            ("해외지수-일본", "Nikkei"),
            ("해외지수-홍콩", "HangSeng"),
            ("해외지수-대만", "TaiwanWeighted"),
            ("해외지수-싱가포르", "SingaporeStraitsTimes"),
            ("해외지수-태국", "ThailandSet"),
            ("해외지수-필리핀", "PhilippinesComposite"),
            ("해외지수-말레이시아", "MalaysiaKlse"),
            ("해외지수-인도네시아", "IndonesiaComposite"),
            ("해외지수-중국", "ShanghaiComposite")
        ];
        var text = new StringBuilder();
        for (var index = 0; index < expected.Length; index += 2)
        {
            text.Append(index / 2 + 1)
                .Append('^')
                .Append(expected[index].Name)
                .Append(',')
                .Append(expected[index + 1].Name)
                .Append("^^^^^^^\r\n");
        }

        var executor = new RecordingExecutor(call =>
        {
            Assert.Equal("LEGACY_COMPARISON_IMPORT_WORLD_STOCK", call.TableName);
            return WorldTable();
        });
        var service = new LegacyComparisonPairImportService(
            executor,
            new MemorySource(Encode(text.ToString())));

        var result = await service.ImportAsync();
        var actual = result.Pairs
            .SelectMany(row => new[] { row.First, row.Second })
            .Select(target => target.MarketTarget)
            .ToArray();

        Assert.Equal(expected.Select(value => value.Target), actual);
        Assert.Equal(24, executor.Calls.Count);
        Assert.Equal(
            expected.Select(value => value.Name),
            executor.Calls.Select(call => Assert.IsType<string>(call.Spec.Parameters[0].Value)));
    }

    [Fact]
    public async Task ImportAsync_FailsClosedForMissingAmbiguousAndMalformedMasterRows()
    {
        var bytes = Encode("1^삼성전자,기아^코스피^코스피^^^^^\r\n");

        await AssertFailureAsync(
            bytes,
            _ => StockTable(),
            LegacyComparisonImportFailure.IdentityNotFound);
        await AssertFailureAsync(
            bytes,
            _ => StockTable(("삼성전자", "005930"), ("삼성전자", "999999")),
            LegacyComparisonImportFailure.IdentityAmbiguous);

        var wrongSchema = new DataTable();
        wrongSchema.Columns.Add("stock_name", typeof(string));
        wrongSchema.Columns.Add("STOCK_CODE", typeof(string));
        await AssertFailureAsync(
            bytes,
            _ => wrongSchema,
            LegacyComparisonImportFailure.DatabaseDataInvalid);
    }

    [Fact]
    public async Task ImportAsync_RejectsAWorldNameThatCollidesWithAFixedMarketLabel()
    {
        var bytes = Encode("1^코스닥 지수,코스피 지수^^^^^^^\r\n");
        var service = new LegacyComparisonPairImportService(
            new RecordingExecutor(call =>
                WorldTable(
                    Assert.IsType<string>(call.Spec.Parameters[0].Value),
                    "COLLISION",
                    "US")),
            new MemorySource(bytes));

        var exception = await Assert.ThrowsAsync<LegacyComparisonImportException>(
            () => service.ImportAsync());

        Assert.Equal(LegacyComparisonImportFailure.IdentityAmbiguous, exception.Failure);
    }

    [Fact]
    public async Task ImportAsync_RejectsAChangedDigestBeforeDatabaseAccess()
    {
        var bytes = Encode("1^코스닥 지수,코스피 지수^^^^^^^\r\n");
        var executor = new RecordingExecutor(_ => throw new InvalidOperationException());
        var source = new CorruptDigestSource(bytes);
        var service = new LegacyComparisonPairImportService(executor, source);

        var exception = await Assert.ThrowsAsync<LegacyComparisonImportException>(
            () => service.ImportAsync());

        Assert.Equal(LegacyComparisonImportFailure.SourceInvalid, exception.Failure);
        Assert.Empty(executor.Calls);
    }

    [Fact]
    public async Task ImportAsync_HonorsPreCanceledRequestsWithoutReadingOrQuerying()
    {
        var source = new CountingSource();
        var executor = new RecordingExecutor(_ => throw new InvalidOperationException());
        var service = new LegacyComparisonPairImportService(executor, source);
        using var cancellation = new CancellationTokenSource();
        cancellation.Cancel();

        await Assert.ThrowsAnyAsync<OperationCanceledException>(
            () => service.ImportAsync(cancellation.Token));

        Assert.Equal(0, source.ReadCount);
        Assert.Empty(executor.Calls);
    }

    private static async Task AssertFailureAsync(
        byte[] bytes,
        Func<RecordedCall, DataTable> handler,
        LegacyComparisonImportFailure expected)
    {
        var service = new LegacyComparisonPairImportService(
            new RecordingExecutor(handler),
            new MemorySource(bytes));
        var exception = await Assert.ThrowsAsync<LegacyComparisonImportException>(
            () => service.ImportAsync());
        Assert.Equal(expected, exception.Failure);
    }

    private static DataTable StockTable(params (string Name, string Code)[] rows)
    {
        var table = new DataTable();
        table.Columns.Add("STOCK_NAME", typeof(string));
        table.Columns.Add("STOCK_CODE", typeof(string));
        foreach (var row in rows)
        {
            table.Rows.Add(row.Name, row.Code);
        }

        return table;
    }

    private static DataTable StockTable(string name, string code) =>
        StockTable((name, code));

    private static DataTable WorldTable(
        params (string InputName, string Symbol, string Nation)[] rows)
    {
        var table = new DataTable();
        table.Columns.Add("F_INPUT_NAME", typeof(string));
        table.Columns.Add("F_SYMB", typeof(string));
        table.Columns.Add("F_NATC", typeof(string));
        foreach (var row in rows)
        {
            table.Rows.Add(row.InputName, row.Symbol, row.Nation);
        }

        return table;
    }

    private static DataTable WorldTable(string inputName, string symbol, string nation) =>
        WorldTable((inputName, symbol, nation));

    private static byte[] Encode(string value) => Cp949.GetBytes(value);

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

    private sealed class MemorySource(byte[] bytes) : ILegacyComparisonFileSource
    {
        public string Digest { get; } = Convert.ToHexString(SHA256.HashData(bytes));

        public Task<LegacyComparisonSourceSnapshot> ReadAsync(
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            return Task.FromResult(new LegacyComparisonSourceSnapshot(bytes, Digest));
        }
    }

    private sealed class CorruptDigestSource(byte[] bytes) : ILegacyComparisonFileSource
    {
        public Task<LegacyComparisonSourceSnapshot> ReadAsync(
            CancellationToken cancellationToken = default) =>
            Task.FromResult(new LegacyComparisonSourceSnapshot(bytes, new string('0', 64)));
    }

    private sealed class CountingSource : ILegacyComparisonFileSource
    {
        public int ReadCount { get; private set; }

        public Task<LegacyComparisonSourceSnapshot> ReadAsync(
            CancellationToken cancellationToken = default)
        {
            ReadCount++;
            throw new InvalidOperationException();
        }
    }

    private sealed record RecordedCall(
        DataSourceKind Source,
        string TableName,
        DataQuerySpec Spec,
        CancellationToken CancellationToken);

    private sealed class RecordingExecutor(Func<RecordedCall, DataTable> handler)
        : IDataQueryExecutor
    {
        private int _stringSqlCallCount;

        public List<RecordedCall> Calls { get; } = [];

        public int StringSqlCallCount => Volatile.Read(ref _stringSqlCallCount);

        public DataTable Execute(DataSourceKind source, string tableName, string query)
        {
            Interlocked.Increment(ref _stringSqlCallCount);
            throw new InvalidOperationException("String SQL fallback is forbidden.");
        }

        public Task<DataTable> ExecuteAsync(
            DataSourceKind source,
            string tableName,
            string query,
            CancellationToken cancellationToken = default)
        {
            Interlocked.Increment(ref _stringSqlCallCount);
            throw new InvalidOperationException("String SQL fallback is forbidden.");
        }

        public Task<DataTable> ExecuteAsync(
            DataSourceKind source,
            string tableName,
            DataQuerySpec query,
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            query.ValidateFor(source);
            var call = new RecordedCall(source, tableName, query, cancellationToken);
            Calls.Add(call);
            return Task.FromResult(handler(call).Copy());
        }
    }

    [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool CreateHardLinkW(
        string fileName,
        string existingFileName,
        IntPtr securityAttributes);

    private sealed class TemporaryDirectory : IDisposable
    {
        public TemporaryDirectory()
        {
            Path = System.IO.Path.Combine(
                System.IO.Path.GetTempPath(),
                "mbn-comparison-import-" + Guid.NewGuid().ToString("N"));
            Directory.CreateDirectory(Path);
        }

        public string Path { get; }

        public void Dispose()
        {
            try
            {
                Directory.Delete(Path, recursive: true);
            }
            catch
            {
                // Best-effort test cleanup only.
            }
        }
    }
}
