using System.Data;
using MBN_STOCK_WEBVIEW.Core.Playout;
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;
using MMoneyCoderSharp.Data;

namespace MBN_STOCK_WEBVIEW.Core.Tests;

public sealed class TraderQuoteSceneDataLoadersTests
{
    private static readonly string[] TraderQuoteColumns =
        ["STOCK_NAME", "CURRENT_PRICE", "CHANGE_PRICE", "DIRECTION_CODE", "RATE"];
    private static readonly string[] DealerColumns = ["DEALER_NAME", "VOLUME"];
    private static readonly string[] OrderBookColumns =
    [
        "STOCK_NAME", "CURRENT_PRICE", "CHANGE_PRICE", "DIRECTION_CODE", "RATE",
        "SELL_PRICE_1", "SELL_PRICE_2", "SELL_PRICE_3",
        "SELL_QUANTITY_1", "SELL_QUANTITY_2", "SELL_QUANTITY_3",
        "BUY_PRICE_1", "BUY_PRICE_2", "BUY_PRICE_3",
        "BUY_QUANTITY_1", "BUY_QUANTITY_2", "BUY_QUANTITY_3",
        "TOTAL_SELL_QUANTITY", "TOTAL_BUY_QUANTITY"
    ];

    public static TheoryData<LegacyDomesticMarket, string, string> TraderRoutes => new()
    {
        { LegacyDomesticMarket.Kospi, "t_online1", "t_dealer" },
        { LegacyDomesticMarket.Kosdaq, "t_kosdaq_online1", "t_kosdaq_dealer" }
    };

    public static TheoryData<LegacyDomesticMarket, string> QuoteRoutes => new()
    {
        { LegacyDomesticMarket.Kospi, "t_online1_call" },
        { LegacyDomesticMarket.Kosdaq, "t_kosdaq_online1_call" }
    };

    [Theory]
    [MemberData(nameof(TraderRoutes))]
    public async Task S5037_routes_closed_market_binds_name_and_proves_quote_dealer_mutations(
        LegacyDomesticMarket market,
        string quoteTable,
        string dealerTable)
    {
        const string stockName = "삼성전자";
        var executor = new RecordingExecutor(
            TraderQuoteTable(stockName),
            DealerTable("BUY"),
            DealerTable("SELL"));

        var data = await new S5037SceneDataLoader(executor).LoadAsync(
            new S5037SceneLoadRequest(market, stockName),
            CancellationToken.None);
        var mutations = new S5037SceneMutationBuilder().Build(data);

        Assert.Equal(3, executor.Calls.Count);
        Assert.Equal(
            ["SCENE_5037_QUOTE", "SCENE_5037_BUY", "SCENE_5037_SELL"],
            executor.Calls.Select(call => call.TableName));
        Assert.Contains(quoteTable, executor.Calls[0].Spec.Sql, StringComparison.OrdinalIgnoreCase);
        Assert.Contains(dealerTable, executor.Calls[1].Spec.Sql, StringComparison.OrdinalIgnoreCase);
        Assert.Contains("f_buy_dealer_vol_1", executor.Calls[1].Spec.Sql, StringComparison.OrdinalIgnoreCase);
        Assert.Contains("f_sell_dealer_vol_1", executor.Calls[2].Spec.Sql, StringComparison.OrdinalIgnoreCase);
        Assert.All(executor.Calls, call =>
        {
            Assert.Equal(DataSourceKind.Oracle, call.Source);
            Assert.DoesNotContain(stockName, call.Spec.Sql, StringComparison.Ordinal);
            var parameter = Assert.Single(call.Spec.Parameters);
            Assert.Equal("stockName", parameter.Name);
            Assert.Equal(stockName, parameter.Value);
            Assert.Equal(DbType.String, parameter.DbType);
        });
        Assert.Equal(stockName, data.StockName);
        Assert.Equal(ScenePriceDirection.Up, data.Direction);
        Assert.Equal([500L, 400L, 300L, 200L, 100L], data.BuyDealers.Select(row => row.Volume));
        Assert.Equal("SELL5", data.SellDealers[^1].DealerName);
        Assert.Contains(mutations, mutation =>
            mutation is PlayoutSetValue { ObjectName: "price", Value: "70,000" });
        Assert.Contains(mutations, mutation =>
            mutation is PlayoutSetValue { ObjectName: "buy_title_1", Value: "BUY1" });
        Assert.Contains(mutations, mutation =>
            mutation is PlayoutSetValue { ObjectName: "sell_vol_5", Value: "100" });
        Assert.Equal(0, executor.StringCalls);
    }

    [Fact]
    public async Task S5037_rejects_wrong_quote_name_schema_dealer_count_order_and_numeric_shape()
    {
        const string requested = "Requested";
        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            new S5037SceneDataLoader(new RecordingExecutor(
                TraderQuoteTable("Other"), DealerTable("B"), DealerTable("S"))).LoadAsync(
                    new S5037SceneLoadRequest(LegacyDomesticMarket.Kospi, requested)));

        var schema = TraderQuoteTable(requested);
        schema.Columns[0].SetOrdinal(4);
        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            new S5037SceneDataLoader(new RecordingExecutor(
                schema, DealerTable("B"), DealerTable("S"))).LoadAsync(
                    new S5037SceneLoadRequest(LegacyDomesticMarket.Kospi, requested)));

        var missing = DealerTable("B");
        missing.Rows.RemoveAt(4);
        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            new S5037SceneDataLoader(new RecordingExecutor(
                TraderQuoteTable(requested), missing, DealerTable("S"))).LoadAsync(
                    new S5037SceneLoadRequest(LegacyDomesticMarket.Kospi, requested)));

        var unordered = DealerTable("B");
        unordered.Rows[2]["VOLUME"] = 900;
        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            new S5037SceneDataLoader(new RecordingExecutor(
                TraderQuoteTable(requested), unordered, DealerTable("S"))).LoadAsync(
                    new S5037SceneLoadRequest(LegacyDomesticMarket.Kospi, requested)));

        var fractional = DealerTable("B");
        fractional.Rows[0]["VOLUME"] = 1.5m;
        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            new S5037SceneDataLoader(new RecordingExecutor(
                TraderQuoteTable(requested), fractional, DealerTable("S"))).LoadAsync(
                    new S5037SceneLoadRequest(LegacyDomesticMarket.Kospi, requested)));
    }

    [Theory]
    [MemberData(nameof(QuoteRoutes))]
    public async Task S8003_routes_closed_market_binds_code_and_proves_order_book_mutations(
        LegacyDomesticMarket market,
        string callTable)
    {
        const string stockCode = "A005930";
        var executor = new RecordingExecutor(OrderBookTable());

        var data = await new S8003SceneDataLoader(executor).LoadAsync(
            new S8003SceneLoadRequest(market, stockCode),
            CancellationToken.None);
        var mutations = new S8003SceneMutationBuilder().Build(data);

        var call = Assert.Single(executor.Calls);
        Assert.Equal(DataSourceKind.Oracle, call.Source);
        Assert.Equal("SCENE_8003", call.TableName);
        Assert.Contains(callTable, call.Spec.Sql, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain(stockCode, call.Spec.Sql, StringComparison.Ordinal);
        var parameter = Assert.Single(call.Spec.Parameters);
        Assert.Equal("stockCode", parameter.Name);
        Assert.Equal(stockCode, parameter.Value);
        Assert.Equal(DbType.String, parameter.DbType);
        Assert.Equal(ScenePriceDirection.Down, data.Direction);
        Assert.Equal(new OrderBookLevelData(69_900, 600), data.BuyLevels[0]);
        Assert.Equal(new OrderBookLevelData(70_100, 300), data.SellLevels[0]);
        Assert.Equal(1200, data.TotalBuyQuantity);
        Assert.Equal(600, data.TotalSellQuantity);
        Assert.Contains(mutations, mutation =>
            mutation is PlayoutSetValue { ObjectName: "price_buy_1", Value: "69,900" });
        Assert.Contains(mutations, mutation =>
            mutation is PlayoutSetValue { ObjectName: "sell_jan_total", Value: "600" });
        Assert.Contains(mutations, mutation =>
            mutation is PlayoutSetScale { ObjectName: "buy_bar_1", X: 1f });
        Assert.Equal(0, executor.StringCalls);
    }

    [Fact]
    public async Task S8003_requires_exact_single_row_schema_finite_rate_and_integral_nonnegative_quantities()
    {
        var schema = OrderBookTable();
        schema.Columns[0].SetOrdinal(1);
        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            new S8003SceneDataLoader(new RecordingExecutor(schema)).LoadAsync(
                new S8003SceneLoadRequest(LegacyDomesticMarket.Kospi, "A005930")));

        var duplicate = OrderBookTable();
        duplicate.Rows.Add(duplicate.Rows[0].ItemArray);
        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            new S8003SceneDataLoader(new RecordingExecutor(duplicate)).LoadAsync(
                new S8003SceneLoadRequest(LegacyDomesticMarket.Kospi, "A005930")));

        var nonfinite = OrderBookTable();
        nonfinite.Rows[0]["RATE"] = double.NegativeInfinity;
        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            new S8003SceneDataLoader(new RecordingExecutor(nonfinite)).LoadAsync(
                new S8003SceneLoadRequest(LegacyDomesticMarket.Kospi, "A005930")));

        var fractional = OrderBookTable();
        fractional.Rows[0]["BUY_QUANTITY_1"] = 1.5m;
        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            new S8003SceneDataLoader(new RecordingExecutor(fractional)).LoadAsync(
                new S8003SceneLoadRequest(LegacyDomesticMarket.Kospi, "A005930")));

        var negative = OrderBookTable();
        negative.Rows[0]["SELL_QUANTITY_1"] = -1;
        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            new S8003SceneDataLoader(new RecordingExecutor(negative)).LoadAsync(
                new S8003SceneLoadRequest(LegacyDomesticMarket.Kospi, "A005930")));
    }

    [Theory]
    [InlineData(99, "A005930")]
    [InlineData(0, "bad-code")]
    [InlineData(1, "")]
    public async Task Invalid_S8003_requests_fail_before_query(int rawMarket, string code)
    {
        var executor = new RecordingExecutor();

        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            new S8003SceneDataLoader(executor).LoadAsync(
                new S8003SceneLoadRequest((LegacyDomesticMarket)rawMarket, code)));

        Assert.Empty(executor.Calls);
    }

    [Theory]
    [InlineData(99, "Stock")]
    [InlineData(0, "")]
    public async Task Invalid_S5037_requests_fail_before_query(int rawMarket, string name)
    {
        var executor = new RecordingExecutor();

        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            new S5037SceneDataLoader(executor).LoadAsync(
                new S5037SceneLoadRequest((LegacyDomesticMarket)rawMarket, name)));

        Assert.Empty(executor.Calls);
    }

    private static DataTable TraderQuoteTable(string stockName) => Table(
        TraderQuoteColumns,
        [stockName, 70_000, 500, "2", 0.72d]);

    private static DataTable DealerTable(string prefix) => Table(
        DealerColumns,
        [$"{prefix}1", 500],
        [$"{prefix}2", 400],
        [$"{prefix}3", 300],
        [$"{prefix}4", 200],
        [$"{prefix}5", 100]);

    private static DataTable OrderBookTable() => Table(
        OrderBookColumns,
        [
            "삼성전자", 70_000, -500, "5", -0.71d,
            70_100, 70_200, 70_300,
            300, 200, 100,
            69_900, 69_800, 69_700,
            600, 400, 200,
            600, 1200
        ]);

    private static DataTable Table(string[] columns, params object?[][] rows)
    {
        var table = new DataTable();
        foreach (var column in columns)
        {
            table.Columns.Add(column, typeof(object));
        }

        foreach (var row in rows)
        {
            table.Rows.Add(row);
        }

        return table;
    }

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

    private sealed class RecordingExecutor(params DataTable[] results) : IDataQueryExecutor
    {
        private readonly Queue<DataTable> _results = new(results);

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

        public int StringCalls { get; private set; }

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

        public Task<DataTable> ExecuteAsync(
            DataSourceKind source,
            string tableName,
            string query,
            CancellationToken cancellationToken = default)
        {
            StringCalls++;
            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);
            Calls.Add(new QueryCall(source, tableName, query));
            if (_results.Count == 0)
            {
                throw new InvalidOperationException("No result was configured.");
            }

            return Task.FromResult(_results.Dequeue().Copy());
        }
    }
}
