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 GridMarketSceneDataLoadersTests
{
    private static readonly string[] DailyColumns =
        ["TRADING_DATE", "INDIVIDUAL_AMOUNT", "FOREIGN_AMOUNT", "INSTITUTION_AMOUNT"];
    private static readonly string[] TotalColumns =
        ["INDIVIDUAL_AMOUNT", "FOREIGN_AMOUNT", "INSTITUTION_AMOUNT"];
    private static readonly string[] ParticipantColumns = ["PARTICIPANT_CODE", "AMOUNT"];
    private static readonly string[] MetricColumns = ["METRIC_CODE", "METRIC_VALUE"];
    private static readonly string[] InstitutionColumns =
        ["MARKET_CODE", "INSTITUTION_CODE", "AMOUNT"];

    [Fact]
    public async Task S5023_loads_exact_daily_and_monthly_tables_then_proves_mutations()
    {
        var executor = new RecordingExecutor(DailyTable(), TotalTable());

        var data = await new S5023SceneDataLoader(executor).LoadAsync(
            new S5023SceneLoadRequest(LegacyDomesticMarket.Kospi),
            CancellationToken.None);
        var mutations = new S5023SceneMutationBuilder().Build(data);

        Assert.Equal(2, executor.Calls.Count);
        Assert.Equal(["SCENE_5023_DAILY", "SCENE_5023_MONTHLY"],
            executor.Calls.Select(call => call.TableName));
        Assert.All(executor.Calls, call =>
        {
            Assert.Equal(DataSourceKind.Oracle, call.Source);
            Assert.Empty(call.Spec.Parameters);
            Assert.Contains("t_invest", call.Spec.Sql, StringComparison.OrdinalIgnoreCase);
            Assert.DoesNotContain("t_kosdaq_invest", call.Spec.Sql, StringComparison.OrdinalIgnoreCase);
        });
        Assert.Equal(new DateOnly(2026, 7, 10), data.DailyRows[0]!.TradingDate);
        Assert.Equal(-20, data.DailyRows[1]!.Amounts.Foreign);
        Assert.Equal(new InvestorFlowAmounts(1500, -800, 200), data.MonthlyTotal);
        Assert.Contains(mutations, mutation =>
            mutation is PlayoutSetValue { ObjectName: "row1_value1", Value: "07/10" });
        Assert.Contains(mutations, mutation =>
            mutation is PlayoutSetValue { ObjectName: "row6_value2", Value: "-800" });
        Assert.Equal(0, executor.StringCalls);
    }

    [Fact]
    public async Task S5023_kosdaq_route_is_closed_and_never_uses_a_selector_parameter()
    {
        var executor = new RecordingExecutor(DailyTable(), TotalTable());

        var data = await new S5023SceneDataLoader(executor).LoadAsync(
            new S5023SceneLoadRequest(LegacyDomesticMarket.Kosdaq),
            CancellationToken.None);

        Assert.Equal(LegacyDomesticMarket.Kosdaq, data.Market);
        Assert.All(executor.Calls, call =>
        {
            Assert.Contains("t_kosdaq_invest", call.Spec.Sql, StringComparison.OrdinalIgnoreCase);
            Assert.Empty(call.Spec.Parameters);
        });
    }

    [Fact]
    public async Task S5023_rejects_wrong_schema_count_order_and_fractional_integer()
    {
        var wrongSchema = DailyTable();
        wrongSchema.Columns[0].SetOrdinal(3);
        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            new S5023SceneDataLoader(new RecordingExecutor(wrongSchema, TotalTable())).LoadAsync(
                new S5023SceneLoadRequest(LegacyDomesticMarket.Kospi)));

        var wrongCount = DailyTable();
        wrongCount.Rows.RemoveAt(4);
        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            new S5023SceneDataLoader(new RecordingExecutor(wrongCount, TotalTable())).LoadAsync(
                new S5023SceneLoadRequest(LegacyDomesticMarket.Kospi)));

        var wrongOrder = DailyTable();
        wrongOrder.Rows[1]["TRADING_DATE"] = "20260711";
        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            new S5023SceneDataLoader(new RecordingExecutor(wrongOrder, TotalTable())).LoadAsync(
                new S5023SceneLoadRequest(LegacyDomesticMarket.Kospi)));

        var fractional = TotalTable();
        fractional.Rows[0]["INDIVIDUAL_AMOUNT"] = 1.25m;
        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            new S5023SceneDataLoader(new RecordingExecutor(DailyTable(), fractional)).LoadAsync(
                new S5023SceneLoadRequest(LegacyDomesticMarket.Kospi)));
    }

    [Theory]
    [InlineData(LegacyDomesticMarket.Kospi, "t_invest")]
    [InlineData(LegacyDomesticMarket.Kosdaq, "t_kosdaq_invest")]
    public async Task S5024_maps_three_ordered_participants_and_proves_bar_mutations(
        LegacyDomesticMarket market,
        string tableName)
    {
        var table = Table(ParticipantColumns,
            ["INDIVIDUAL", 100d],
            ["FOREIGN", -50d],
            ["INSTITUTION", 0d]);
        var executor = new RecordingExecutor(table);

        var data = await new S5024SceneDataLoader(executor).LoadAsync(
            new S5024SceneLoadRequest(market),
            CancellationToken.None);
        var mutations = new S5024SceneMutationBuilder().Build(data);

        var call = Assert.Single(executor.Calls);
        Assert.Equal("SCENE_5024", call.TableName);
        Assert.Contains(tableName, call.Spec.Sql, StringComparison.OrdinalIgnoreCase);
        Assert.Empty(call.Spec.Parameters);
        Assert.Equal(
            [InvestorParticipant.Individual, InvestorParticipant.Foreign, InvestorParticipant.Institution],
            data.Amounts.Select(amount => amount.Participant));
        Assert.Contains(mutations, mutation =>
            mutation is PlayoutSetVisible { ObjectName: "barG2_2", IsVisible: true });
        Assert.Contains(mutations, mutation =>
            mutation is PlayoutSetValue { ObjectName: "value1_1", Value: "100" });
        Assert.Equal(0, executor.StringCalls);
    }

    [Fact]
    public async Task S5024_rejects_reordered_or_nonfinite_rows()
    {
        var reordered = Table(ParticipantColumns,
            ["FOREIGN", 1d],
            ["INDIVIDUAL", 2d],
            ["INSTITUTION", 3d]);
        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            new S5024SceneDataLoader(new RecordingExecutor(reordered)).LoadAsync(
                new S5024SceneLoadRequest(LegacyDomesticMarket.Kospi)));

        var nonfinite = Table(ParticipantColumns,
            ["INDIVIDUAL", double.PositiveInfinity],
            ["FOREIGN", 2d],
            ["INSTITUTION", 3d]);
        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            new S5024SceneDataLoader(new RecordingExecutor(nonfinite)).LoadAsync(
                new S5024SceneLoadRequest(LegacyDomesticMarket.Kospi)));
    }

    [Fact]
    public async Task S5085_maps_six_ordered_metrics_and_proves_value_color_mutations()
    {
        var executor = new RecordingExecutor(MetricTable());

        var data = await new S5085SceneDataLoader(executor).LoadAsync(CancellationToken.None);
        var mutations = new S5085SceneMutationBuilder().Build(data);

        var call = Assert.Single(executor.Calls);
        Assert.Equal("SCENE_5085", call.TableName);
        Assert.Empty(call.Spec.Parameters);
        Assert.Contains("WITH future_row AS", call.Spec.Sql, StringComparison.OrdinalIgnoreCase);
        Assert.Contains("a.f_stock_seq = 1", call.Spec.Sql, StringComparison.OrdinalIgnoreCase);
        Assert.Contains("MAX(open_day)", call.Spec.Sql, StringComparison.OrdinalIgnoreCase);
        Assert.Equal(6, data.Metrics.Count);
        Assert.Equal(ProgramTradingMetric.NetBuy, data.Metrics[0].Metric);
        Assert.Equal(ProgramTradingMetric.Futures, data.Metrics[^1].Metric);
        Assert.Contains(mutations, mutation =>
            mutation is PlayoutSetValue { ObjectName: "price1", Value: "1,234.5" });
        Assert.Contains(mutations, mutation =>
            mutation is PlayoutSetFaceColor
            {
                ObjectName: "price2",
                Red: 15,
                Green: 99,
                Blue: 188
            });
        Assert.Equal(0, executor.StringCalls);
    }

    [Fact]
    public async Task S5085_rejects_missing_reordered_or_nonfinite_metrics()
    {
        var missing = MetricTable();
        missing.Rows.RemoveAt(5);
        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            new S5085SceneDataLoader(new RecordingExecutor(missing)).LoadAsync());

        var reordered = MetricTable();
        reordered.Rows[0]["METRIC_CODE"] = "ARBITRAGE";
        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            new S5085SceneDataLoader(new RecordingExecutor(reordered)).LoadAsync());

        var nonfinite = MetricTable();
        nonfinite.Rows[0]["METRIC_VALUE"] = double.NaN;
        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            new S5085SceneDataLoader(new RecordingExecutor(nonfinite)).LoadAsync());
    }

    [Fact]
    public async Task S6067_maps_the_exact_fifteen_row_grid_and_proves_slot_mutations()
    {
        var executor = new RecordingExecutor(InstitutionTable());

        var data = await new S6067SceneDataLoader(executor).LoadAsync(CancellationToken.None);
        var mutations = new S6067SceneMutationBuilder().Build(data);

        var call = Assert.Single(executor.Calls);
        Assert.Equal("SCENE_6067", call.TableName);
        Assert.Empty(call.Spec.Parameters);
        Assert.Equal(15, data.Rows.Count);
        Assert.Equal(InstitutionMarket.Kospi, data.Rows[0].Market);
        Assert.Equal(InstitutionKind.Fund, data.Rows[4].Institution);
        Assert.Equal(InstitutionMarket.Other, data.Rows[10].Market);
        Assert.Contains(mutations, mutation =>
            mutation is PlayoutSetValue { ObjectName: "row1_value1", Value: "100" });
        Assert.Contains(mutations, mutation =>
            mutation is PlayoutSetValue { ObjectName: "row3_value5", Value: "-305" });
        Assert.Equal(0, executor.StringCalls);
    }

    [Fact]
    public async Task S6067_rejects_wrong_order_schema_and_fractional_values()
    {
        var reordered = InstitutionTable();
        reordered.Rows[1]["INSTITUTION_CODE"] = "BANK";
        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            new S6067SceneDataLoader(new RecordingExecutor(reordered)).LoadAsync());

        var schema = InstitutionTable();
        schema.Columns[2].ColumnName = "VALUE";
        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            new S6067SceneDataLoader(new RecordingExecutor(schema)).LoadAsync());

        var fractional = InstitutionTable();
        fractional.Rows[0]["AMOUNT"] = 1.1m;
        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            new S6067SceneDataLoader(new RecordingExecutor(fractional)).LoadAsync());
    }

    [Theory]
    [InlineData(99)]
    public async Task Closed_market_requests_fail_before_any_query(int rawMarket)
    {
        var executor = new RecordingExecutor();

        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            new S5023SceneDataLoader(executor).LoadAsync(
                new S5023SceneLoadRequest((LegacyDomesticMarket)rawMarket)));
        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            new S5024SceneDataLoader(executor).LoadAsync(
                new S5024SceneLoadRequest((LegacyDomesticMarket)rawMarket)));

        Assert.Empty(executor.Calls);
    }

    private static DataTable DailyTable() => Table(DailyColumns,
        ["20260710", 100, -10, 1],
        ["20260709", 200, -20, 2],
        ["20260708", 300, -30, 3],
        ["20260707", 400, -40, 4],
        ["20260706", 500, -50, 5]);

    private static DataTable TotalTable() => Table(TotalColumns, [1500, -800, 200]);

    private static DataTable MetricTable() => Table(MetricColumns,
        ["NET_BUY", 1234.5d],
        ["ARBITRAGE", -12d],
        ["NON_ARBITRAGE", 24d],
        ["BASIS", 1.25d],
        ["KOSPI200", 300.12d],
        ["FUTURES", 301.25d]);

    private static DataTable InstitutionTable()
    {
        string[] markets = ["KOSPI", "KOSDAQ", "KOSPI200"];
        string[] institutions =
            ["SECURITIES", "INSURANCE", "INVESTMENT_TRUST", "BANK", "FUND"];
        var table = Table(InstitutionColumns);
        for (var market = 0; market < markets.Length; market++)
        {
            for (var institution = 0; institution < institutions.Length; institution++)
            {
                var value = market == 2 && institution == 4
                    ? -305
                    : ((market + 1) * 100) + institution;
                table.Rows.Add(markets[market], institutions[institution], value);
            }
        }

        return table;
    }

    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());
        }
    }
}
