using System.Collections.Concurrent;
using System.Data;
using System.Globalization;
using MMoneyCoderSharp.Data;

namespace MBN_STOCK_WEBVIEW.Core.Tests;

public sealed class LegacyMarketDataServiceTests
{
    [Fact]
    public async Task GetAsync_Kospi_ForwardsBothLegacyQueriesToTheirProviders()
    {
        var executor = new RecordingQueryExecutor(call => CreateSingleRowTable(call.Name));
        var service = new LegacyMarketDataService(executor);
        using var cancellation = new CancellationTokenSource();

        var snapshot = await service.GetAsync(MarketDataView.Kospi, cancellationToken: cancellation.Token);

        Assert.Equal(MarketDataView.Kospi, snapshot.View);
        Assert.Collection(
            snapshot.Tables,
            table =>
            {
                Assert.Equal("코스피", table.Name);
                Assert.Equal(DataSourceKind.Oracle, table.Source);
            },
            table =>
            {
                Assert.Equal("NXT코스피", table.Name);
                Assert.Equal(DataSourceKind.MariaDb, table.Source);
            });

        var calls = executor.Calls.OrderBy(call => call.Name, StringComparer.Ordinal).ToArray();
        Assert.Equal(2, calls.Length);
        Assert.Contains(calls, call =>
            call.Source == DataSourceKind.Oracle &&
            call.Name == "코스피" &&
            call.Query == "SELECT F_STOCK_WANNAME, F_STOCK_CODE FROM T_STOCK WHERE F_MKT_HALT = 'N'");
        Assert.Contains(calls, call =>
            call.Source == DataSourceKind.MariaDb &&
            call.Name == "NXT코스피" &&
            call.Query == "SELECT CONCAT(F_STOCK_NAME, '(NXT)') F_STOCK_NAME, F_STOCK_CODE FROM N_STOCK WHERE F_STOP_GUBUN = 'N'");
        Assert.All(calls, call => Assert.Equal(cancellation.Token, call.CancellationToken));
        Assert.Equal(0, executor.SynchronousCallCount);
    }

    [Fact]
    public async Task GetAsync_Index_ForwardsFiveOracleQueriesAndPreservesLegacyKospiSql()
    {
        const string expectedKospiSql = @"SELECT '코스피' name, round(f_part_idx/100,2) part_idx, f_chg_type chg_type, round(f_part_chg/100,2) part_chg,
		round((f_part_chg / decode(f_chg_type, '+', f_part_idx - f_part_chg, '-', f_part_idx + f_part_chg, 1)) * 100, 2) rate
		FROM t_index
		WHERE f_part_code = '001'";

        var executor = new RecordingQueryExecutor(call => CreateSingleRowTable(call.Name));
        var service = new LegacyMarketDataService(executor);

        var snapshot = await service.GetAsync(MarketDataView.Index);

        Assert.Equal(5, snapshot.Tables.Count);
        Assert.Equal(
            ["코스피", "코스닥", "코스피200", "KRX100", "선물"],
            snapshot.Tables.Select(table => table.Name));

        var calls = executor.Calls.ToArray();
        Assert.Equal(5, calls.Length);
        Assert.All(calls, call => Assert.Equal(DataSourceKind.Oracle, call.Source));
        Assert.Equal(
            expectedKospiSql,
            Assert.Single(calls, call => call.Name == "코스피").Query);
    }

    [Fact]
    public async Task GetAsync_CapsReturnedRowsButReportsUncappedTotal()
    {
        var executor = new RecordingQueryExecutor(_ => CreateNumberedTable(5));
        var service = new LegacyMarketDataService(executor);

        var snapshot = await service.GetAsync(MarketDataView.Overseas, maximumRowsPerTable: 2);

        var table = Assert.Single(snapshot.Tables);
        Assert.Equal(["Number", "Label"], table.Columns);
        Assert.Equal(2, table.Rows.Count);
        Assert.Equal(["1", "row-1"], table.Rows[0]);
        Assert.Equal(["2", "row-2"], table.Rows[1]);
        Assert.Equal(5, table.TotalRowCount);
    }

    [Fact]
    public async Task GetAsync_FormatsNullNumbersAndDatesUsingInvariantCulture()
    {
        var previousCulture = CultureInfo.CurrentCulture;
        var previousUiCulture = CultureInfo.CurrentUICulture;
        try
        {
            CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("fr-FR");
            CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("fr-FR");

            var executor = new RecordingQueryExecutor(_ => CreateFormattingTable());
            var service = new LegacyMarketDataService(executor);

            var snapshot = await service.GetAsync(MarketDataView.Overseas);

            var row = Assert.Single(Assert.Single(snapshot.Tables).Rows);
            Assert.Equal(
                [null, "1234.56", "2026-07-10 14:15:16", "2026-07-10 14:15:16 +09:00", "plain"],
                row);
        }
        finally
        {
            CultureInfo.CurrentCulture = previousCulture;
            CultureInfo.CurrentUICulture = previousUiCulture;
        }
    }

    [Fact]
    public async Task GetAsync_PropagatesCancellationToExecutorAndCaller()
    {
        var executor = new RecordingQueryExecutor((call, cancellationToken) =>
        {
            cancellationToken.ThrowIfCancellationRequested();
            return Task.FromResult(CreateSingleRowTable(call.Name));
        });
        var service = new LegacyMarketDataService(executor);
        using var cancellation = new CancellationTokenSource();
        cancellation.Cancel();

        await Assert.ThrowsAnyAsync<OperationCanceledException>(
            () => service.GetAsync(MarketDataView.Kospi, cancellationToken: cancellation.Token));

        Assert.NotEmpty(executor.Calls);
        Assert.All(executor.Calls, call => Assert.Equal(cancellation.Token, call.CancellationToken));
    }

    [Theory]
    [InlineData(-1)]
    [InlineData(0)]
    [InlineData(2001)]
    [InlineData(int.MaxValue)]
    public async Task GetAsync_RejectsInvalidRowLimitBeforeExecutingQueries(int maximumRows)
    {
        var executor = new RecordingQueryExecutor(_ => CreateSingleRowTable("unused"));
        var service = new LegacyMarketDataService(executor);

        var exception = await Assert.ThrowsAsync<ArgumentOutOfRangeException>(
            () => service.GetAsync(MarketDataView.Kospi, maximumRows));

        Assert.Equal("maximumRowsPerTable", exception.ParamName);
        Assert.Empty(executor.Calls);
    }

    private static DataTable CreateSingleRowTable(string value)
    {
        var table = new DataTable();
        table.Columns.Add("Value", typeof(string));
        table.Rows.Add(value);
        return table;
    }

    private static DataTable CreateNumberedTable(int rowCount)
    {
        var table = new DataTable();
        table.Columns.Add("Number", typeof(int));
        table.Columns.Add("Label", typeof(string));
        for (var index = 1; index <= rowCount; index++)
        {
            table.Rows.Add(index, $"row-{index}");
        }

        return table;
    }

    private static DataTable CreateFormattingTable()
    {
        var table = new DataTable();
        table.Columns.Add("Null", typeof(object));
        table.Columns.Add("Decimal", typeof(decimal));
        table.Columns.Add("Date", typeof(DateTime));
        table.Columns.Add("DateOffset", typeof(DateTimeOffset));
        table.Columns.Add("Text", typeof(string));
        table.Rows.Add(
            DBNull.Value,
            1234.56m,
            new DateTime(2026, 7, 10, 14, 15, 16, DateTimeKind.Unspecified),
            new DateTimeOffset(2026, 7, 10, 14, 15, 16, TimeSpan.FromHours(9)),
            "plain");
        return table;
    }

    private sealed record QueryCall(
        DataSourceKind Source,
        string Name,
        string Query,
        CancellationToken CancellationToken);

    private sealed class RecordingQueryExecutor : IDataQueryExecutor
    {
        private readonly Func<QueryCall, CancellationToken, Task<DataTable>> _handler;
        private int _synchronousCallCount;

        public RecordingQueryExecutor(Func<QueryCall, DataTable> handler)
            : this((call, _) => Task.FromResult(handler(call)))
        {
        }

        public RecordingQueryExecutor(Func<QueryCall, CancellationToken, Task<DataTable>> handler)
        {
            _handler = handler;
        }

        public ConcurrentQueue<QueryCall> Calls { get; } = new();

        public int SynchronousCallCount => Volatile.Read(ref _synchronousCallCount);

        public DataTable Execute(DataSourceKind source, string tableName, string query)
        {
            Interlocked.Increment(ref _synchronousCallCount);
            throw new InvalidOperationException("The live market-data service must use the asynchronous executor API.");
        }

        public Task<DataTable> ExecuteAsync(
            DataSourceKind source,
            string tableName,
            string query,
            CancellationToken cancellationToken = default)
        {
            var call = new QueryCall(source, tableName, query, cancellationToken);
            Calls.Enqueue(call);
            return _handler(call, cancellationToken);
        }
    }
}
