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 EquityPanelSceneDataLoadersTests
{
    private static readonly string[] S5006Columns =
    [
        "STOCK_NAME", "DIRECTION_CODE", "CURRENT_PRICE", "NET_CHANGE", "RATE",
        "OPEN_PRICE", "OPEN_RATE", "HIGH_PRICE", "HIGH_RATE", "LOW_PRICE", "LOW_RATE"
    ];

    private static readonly string[] S5011Columns =
    [
        "STOCK_NAME", "DIRECTION_CODE", "CURRENT_PRICE", "NET_CHANGE", "RATE",
        "DETAIL_1", "DETAIL_2", "DETAIL_3", "DETAIL_4"
    ];

    public static TheoryData<LegacyDomesticEquityMarket, string> Domestic5006Cases => new()
    {
        { LegacyDomesticEquityMarket.Kospi, "FROM V_2 a" },
        { LegacyDomesticEquityMarket.Kosdaq, "FROM V_2_KOSDAQ a" }
    };

    public static TheoryData<S5011RouteCase> S5011Cases => new()
    {
        new(LegacyDomesticEquityMarket.Kospi, S5011DetailBranch.FaceValue,
            DataSourceKind.Oracle, "FROM V_2 a", "t_online1"),
        new(LegacyDomesticEquityMarket.Kosdaq, S5011DetailBranch.FaceValue,
            DataSourceKind.Oracle, "FROM V_2_KOSDAQ a", "t_kosdaq_online1"),
        new(LegacyDomesticEquityMarket.Kospi, S5011DetailBranch.Valuation,
            DataSourceKind.Oracle, "FROM V_2 a", "a.pbr DETAIL_1"),
        new(LegacyDomesticEquityMarket.Kosdaq, S5011DetailBranch.Valuation,
            DataSourceKind.Oracle, "FROM V_2_KOSDAQ a", "a.pbr DETAIL_1"),
        new(LegacyDomesticEquityMarket.Kospi, S5011DetailBranch.Volume,
            DataSourceKind.Oracle, "FROM V_2 a", "T_CANDLE_HISTORY"),
        new(LegacyDomesticEquityMarket.Kosdaq, S5011DetailBranch.Volume,
            DataSourceKind.Oracle, "FROM V_2_KOSDAQ a", "T_KOSDAQ_CANDLE_HISTORY"),
        new(LegacyDomesticEquityMarket.NxtKospi, S5011DetailBranch.FaceValue,
            DataSourceKind.MariaDb, "FROM n_online a", "RANK() OVER"),
        new(LegacyDomesticEquityMarket.NxtKosdaq, S5011DetailBranch.FaceValue,
            DataSourceKind.MariaDb, "FROM n_kosdaq_online a", "RANK() OVER"),
        new(LegacyDomesticEquityMarket.NxtKospi, S5011DetailBranch.Volume,
            DataSourceKind.MariaDb, "FROM n_online a", "0 DETAIL_3"),
        new(LegacyDomesticEquityMarket.NxtKosdaq, S5011DetailBranch.Volume,
            DataSourceKind.MariaDb, "FROM n_kosdaq_online a", "0 DETAIL_3")
    };

    [Theory]
    [MemberData(nameof(Domestic5006Cases))]
    public async Task S5006_domestic_routes_oracle_by_closed_market_and_generates_mutations(
        LegacyDomesticEquityMarket market,
        string sourceFragment)
    {
        const string stockCode = "A005930";
        var executor = new RecordingExecutor(S5006Table());

        var data = await new S5006DomesticSceneDataLoader(executor).LoadAsync(
            new S5006DomesticSceneDataRequest(market, stockCode),
            CancellationToken.None);
        var mutations = new S5006SceneMutationBuilder().Build(data);

        var call = Assert.Single(executor.Calls);
        Assert.Equal(DataSourceKind.Oracle, call.Source);
        Assert.Equal("S5006_DOMESTIC", call.TableName);
        Assert.Contains(sourceFragment, 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.Up, data.Direction);
        Assert.Equal(1000, data.CurrentPrice);
        Assert.Contains(mutations, mutation =>
            mutation is PlayoutSetValue { ObjectName: "price", Value: "1,000" });
        Assert.Equal(0, executor.StringCalls);
    }

    [Theory]
    [MemberData(nameof(S5011Cases))]
    public async Task S5011_supported_source_and_branch_matrix_is_parameterized_and_typed(
        S5011RouteCase item)
    {
        const string stockCode = "A123456";
        var executor = new RecordingExecutor(S5011Table(item.Branch));

        var data = await new S5011SceneDataLoader(executor).LoadAsync(
            new S5011SceneDataRequest(item.Market, item.Branch, stockCode),
            CancellationToken.None);

        var call = Assert.Single(executor.Calls);
        Assert.Equal(item.Source, call.Source);
        Assert.Equal("S5011_DETAIL", call.TableName);
        call.Spec.ValidateFor(item.Source);
        Assert.Contains(item.SourceFragment, call.Spec.Sql, StringComparison.OrdinalIgnoreCase);
        Assert.Contains(item.BranchFragment, call.Spec.Sql, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain(stockCode, call.Spec.Sql, StringComparison.Ordinal);
        Assert.All(call.Spec.Parameters, parameter =>
        {
            Assert.Equal(stockCode, parameter.Value);
            Assert.Equal(DbType.String, parameter.DbType);
        });
        Assert.Equal(item.Branch, data.Detail.Branch);
        Assert.Equal(item.Market, data.Market);
        Assert.Equal(0, executor.StringCalls);

        var mutations = new S5011SceneMutationBuilder().Build(data);
        Assert.Contains(mutations, mutation =>
            mutation is PlayoutSetVisible
            {
                ObjectName: "mart",
                IsVisible: var visible
            } && visible == item.Market.ToString().StartsWith("Nxt", StringComparison.Ordinal));
        Assert.Contains(mutations, mutation =>
            mutation is PlayoutSetValue { ObjectName: "r_value4" });

        if (item.Branch == S5011DetailBranch.Volume && item.Source == DataSourceKind.Oracle)
        {
            Assert.Equal(
                ["stockCode5", "stockCode20", "stockCode"],
                call.Spec.Parameters.Select(parameter => parameter.Name));
        }
        else
        {
            Assert.Single(call.Spec.Parameters);
        }
    }

    [Theory]
    [InlineData(LegacyDomesticEquityMarket.NxtKospi)]
    [InlineData(LegacyDomesticEquityMarket.NxtKosdaq)]
    public async Task S5011_nxt_valuation_fails_closed_instead_of_fabricating_zeroes(
        LegacyDomesticEquityMarket market)
    {
        var executor = new RecordingExecutor();

        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            new S5011SceneDataLoader(executor).LoadAsync(
                new S5011SceneDataRequest(
                    market,
                    S5011DetailBranch.Valuation,
                    "005930"),
                CancellationToken.None));

        Assert.Empty(executor.Calls);
    }

    [Theory]
    [InlineData(LegacyDomesticEquityMarket.NxtKospi, "005930")]
    [InlineData(LegacyDomesticEquityMarket.NxtKosdaq, "005930")]
    [InlineData(LegacyDomesticEquityMarket.Kospi, "bad-code")]
    [InlineData(LegacyDomesticEquityMarket.Kosdaq, "")]
    public async Task S5006_invalid_market_or_code_fails_before_query(
        LegacyDomesticEquityMarket market,
        string code)
    {
        var executor = new RecordingExecutor();

        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            new S5006DomesticSceneDataLoader(executor).LoadAsync(
                new S5006DomesticSceneDataRequest(market, code),
                CancellationToken.None));

        Assert.Empty(executor.Calls);
    }

    [Fact]
    public async Task Equity_loaders_require_exact_single_row_schema_and_do_not_leak_code()
    {
        const string stockCode = "SENSITIVE123";
        var reordered = Table(
            [
                "DIRECTION_CODE", "STOCK_NAME", "CURRENT_PRICE", "NET_CHANGE", "RATE",
                "OPEN_PRICE", "OPEN_RATE", "HIGH_PRICE", "HIGH_RATE", "LOW_PRICE", "LOW_RATE"
            ],
            ["2", "Stock", 1000, 10, 1, 900, -1, 1100, 10, 800, -20]);
        var exception = await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            new S5006DomesticSceneDataLoader(new RecordingExecutor(reordered)).LoadAsync(
                new S5006DomesticSceneDataRequest(
                    LegacyDomesticEquityMarket.Kospi,
                    stockCode),
                CancellationToken.None));
        Assert.DoesNotContain(stockCode, exception.Message, StringComparison.Ordinal);

        var duplicate = S5011Table(S5011DetailBranch.FaceValue);
        duplicate.Rows.Add(duplicate.Rows[0].ItemArray);
        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            new S5011SceneDataLoader(new RecordingExecutor(duplicate)).LoadAsync(
                new S5011SceneDataRequest(
                    LegacyDomesticEquityMarket.Kospi,
                    S5011DetailBranch.FaceValue,
                    stockCode),
                CancellationToken.None));
    }

    private static DataTable S5006Table() => Table(
        S5006Columns,
        ["Stock", "2", 1000m, 50m, 5.26m, 960m, 1m, 1100m, 10m, 900m, -10m]);

    private static DataTable S5011Table(S5011DetailBranch branch)
    {
        object?[] details = branch switch
        {
            S5011DetailBranch.FaceValue => [5000m, 100_000_000m, 900_000_000m, 7m],
            S5011DetailBranch.Valuation => [1.2m, 8.75m, 12_345m, -321m],
            S5011DetailBranch.Volume => [12_345m, 9_876_543m, 1_111m, 2_222m],
            _ => throw new InvalidOperationException()
        };
        return Table(
            S5011Columns,
            ["Stock(NXT)", "2", 1000m, 50m, 5.26m, .. details]);
    }

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

        if (row is not null)
        {
            table.Rows.Add(row);
        }

        return table;
    }

    public sealed record S5011RouteCase(
        LegacyDomesticEquityMarket Market,
        S5011DetailBranch Branch,
        DataSourceKind Source,
        string SourceFragment,
        string BranchFragment);

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