#nullable enable

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

namespace MBN_STOCK_WEBVIEW.Core.Tests;

public sealed class LegacyParameterizedSceneRequestResolverTests
{
    [Fact]
    public void S5001_maps_legacy_dispatch_fields_to_closed_requests()
    {
        var resolver = new LegacyParameterizedSceneRequestResolver(
            new RecordingExecutor(),
            new FixedTimeProvider(new DateTimeOffset(2026, 7, 10, 12, 0, 0, TimeSpan.Zero)));

        var commodity = resolver.CreateS5001Request(Entry(
            "5001", "원자재", "국제 금", "현재가"));
        var expectedIndex = resolver.CreateS5001Request(Entry(
            "5001", "코스피200", "지수", "예상지수"));
        var nxt = resolver.CreateS5001Request(Entry(
            "N5001", "코스피_NXT", "삼성전자(NXT)", "현재가"));

        Assert.Equal(
            S5001CommodityTarget.InternationalGold,
            Assert.IsType<S5001CommodityLoadRequest>(commodity).Target);
        var index = Assert.IsType<S5001DomesticIndexLoadRequest>(expectedIndex);
        Assert.Equal(S5001DomesticIndexTarget.Kospi200, index.Target);
        Assert.Equal(S5001IndexMode.Expected, index.Mode);
        var stock = Assert.IsType<S5001DomesticStockLoadRequest>(nxt);
        Assert.Equal(LegacyDomesticEquityMarket.NxtKospi, stock.Market);
        Assert.Equal(S5001NxtBadge.PreMarket, stock.Badge);
    }

    [Fact]
    public void S5001_foreign_stock_uses_exact_hidden_identity_when_present_and_keeps_legacy_rows_compatible()
    {
        var resolver = new LegacyParameterizedSceneRequestResolver(new RecordingExecutor());
        var exact = resolver.CreateS5001Request(new LegacyPlaylistEntry(
            "exact",
            "5001",
            Selection: new LegacySceneSelection(
                "FOREIGN_STOCK",
                "Shared Name",
                "1열판기본",
                "CURRENT",
                "LNS@LON|GB|1")));
        var legacy = resolver.CreateS5001Request(new LegacyPlaylistEntry(
            "legacy",
            "5001",
            Selection: new LegacySceneSelection(
                "FOREIGN_STOCK",
                "Legacy Name",
                "1열판기본",
                "CURRENT",
                string.Empty)));

        var exactRequest = Assert.IsType<S5001ExactForeignStockLoadRequest>(exact);
        Assert.Equal("Shared Name", exactRequest.StockName);
        Assert.Equal("LNS@LON", exactRequest.ForeignSymbol);
        Assert.Equal("GB", exactRequest.ForeignNationCode);
        Assert.Equal("Legacy Name", Assert.IsType<S5001ForeignStockLoadRequest>(legacy).StockName);
    }

    [Fact]
    public void S5001_foreign_industry_binds_native_tuple_and_keeps_historical_name_only_fallback()
    {
        var resolver = new LegacyParameterizedSceneRequestResolver(new RecordingExecutor());
        var exact = resolver.CreateS5001Request(new LegacyPlaylistEntry(
            "exact-industry",
            "5001",
            Selection: new LegacySceneSelection(
                "FOREIGN_INDUSTRY",
                "Shared Korean Name",
                "plate",
                "CURRENT",
                "US Technology|NAS@USTECH|US|0")));
        var legacy = resolver.CreateS5001Request(new LegacyPlaylistEntry(
            "legacy-industry",
            "5001",
            Selection: new LegacySceneSelection(
                "FOREIGN_INDUSTRY",
                "Shared Korean Name",
                "plate",
                "CURRENT",
                string.Empty)));

        var exactRequest = Assert.IsType<S5001ExactForeignIndustryLoadRequest>(exact);
        Assert.Equal("Shared Korean Name", exactRequest.IndustryName);
        Assert.Equal("US Technology", exactRequest.InputName);
        Assert.Equal("NAS@USTECH", exactRequest.ForeignSymbol);
        Assert.Equal("US", exactRequest.ForeignNationCode);
        Assert.Equal(
            "Shared Korean Name",
            Assert.IsType<S5001ForeignIndustryLoadRequest>(legacy).IndustryName);
    }

    [Theory]
    [InlineData("US Technology|NAS@USTECH|TW|0")]
    [InlineData("US Technology|NAS@USTECH|US|1")]
    [InlineData("US Technology|bad^symbol|US|0")]
    [InlineData("US Technology|NAS@USTECH|US")]
    public void S5001_foreign_industry_rejects_open_or_malformed_hidden_identity(string dataCode)
    {
        var resolver = new LegacyParameterizedSceneRequestResolver(new RecordingExecutor());
        var entry = new LegacyPlaylistEntry(
            "invalid-industry",
            "5001",
            Selection: new LegacySceneSelection(
                "FOREIGN_INDUSTRY",
                "Shared Korean Name",
                "plate",
                "CURRENT",
                dataCode));

        Assert.Throws<LegacySceneDataException>(() => resolver.CreateS5001Request(entry));
    }

    [Fact]
    public async Task Two_column_alias_routes_futures_to_s5032_and_normalizes_side_order()
    {
        var resolver = new LegacyParameterizedSceneRequestResolver(new RecordingExecutor());

        var result = await resolver.ResolveTwoColumnRequestAsync(Entry(
            "8018", "지수", "선물,코스닥 지수", "현재가"));

        var routed = Assert.IsType<LegacyS5032LoadRequest>(result);
        var request = Assert.IsType<S5032IndexAndFuturesLoadRequest>(routed.Request);
        Assert.Equal(LegacyMarketQuoteTarget.Kosdaq, request.PrimaryTarget);
    }

    [Theory]
    [InlineData("Futures,Kospi", LegacyMarketQuoteTarget.Kospi)]
    [InlineData("Kosdaq,Futures", LegacyMarketQuoteTarget.Kosdaq)]
    public async Task Two_column_alias_recognizes_the_canonical_stored_futures_token(
        string subject,
        LegacyMarketQuoteTarget expectedPrimary)
    {
        var resolver = new LegacyParameterizedSceneRequestResolver(new RecordingExecutor());

        var result = await resolver.ResolveTwoColumnRequestAsync(Entry(
            "5032", "INDEX", subject, "CURRENT"));

        var routed = Assert.IsType<LegacyS5032LoadRequest>(result);
        var request = Assert.IsType<S5032IndexAndFuturesLoadRequest>(routed.Request);
        Assert.Equal(expectedPrimary, request.PrimaryTarget);
    }

    [Theory]
    [InlineData(
        "코스피 지수,삼성전자(NXT)",
        1,
        0,
        LegacyDomesticEquityMarket.NxtKospi,
        S8018PairSide.Second,
        LegacyMarketQuoteTarget.Kospi)]
    [InlineData(
        "에이비엘바이오(NXT),원달러 환율",
        0,
        1,
        LegacyDomesticEquityMarket.NxtKosdaq,
        S8018PairSide.First,
        LegacyMarketQuoteTarget.WonDollar)]
    public async Task Current_mixed_market_and_nxt_stock_preserves_the_original_nxt_branch(
        string subject,
        int nxtKospiMatches,
        int nxtKosdaqMatches,
        LegacyDomesticEquityMarket expectedStockMarket,
        S8018PairSide expectedStockSide,
        LegacyMarketQuoteTarget expectedMarketTarget)
    {
        var executor = new RecordingExecutor(
            Count(nxtKospiMatches),
            Count(nxtKosdaqMatches));
        var resolver = new LegacyParameterizedSceneRequestResolver(executor);

        var result = await resolver.ResolveTwoColumnRequestAsync(Entry(
            "8018", "종목", subject, "현재가"));

        var routed = Assert.IsType<LegacyS8018LoadRequest>(result);
        var request = Assert.IsType<S8018MixedMarketAndStockLoadRequest>(routed.Request);
        Assert.Equal(expectedStockSide, request.StockSide);
        Assert.Equal(expectedMarketTarget, request.MarketTarget);
        Assert.Equal(expectedStockMarket, request.StockMarket);
        Assert.Equal(S8018MarketDataMode.Current, request.Mode);
        Assert.Equal(
            ["SCENE_RESOLVE_NXT_KOSPI_STOCK", "SCENE_RESOLVE_NXT_KOSDAQ_STOCK"],
            executor.Calls.Select(call => call.TableName));
        Assert.All(executor.Calls, call =>
        {
            Assert.Equal(DataSourceKind.MariaDb, call.Source);
            var parameter = Assert.Single(call.Spec.Parameters);
            var value = Assert.IsType<string>(parameter.Value);
            Assert.DoesNotContain("(NXT)", value, StringComparison.Ordinal);
            Assert.DoesNotContain(value, call.Spec.Sql, StringComparison.Ordinal);
        });
    }

    [Fact]
    public async Task Current_stock_pair_uses_original_master_table_precedence_and_session_boundary()
    {
        var executor = new RecordingExecutor(
            Count(1), Count(0),
            Count(1), Count(1));
        var resolver = new LegacyParameterizedSceneRequestResolver(
            executor,
            new FixedTimeProvider(new DateTimeOffset(2026, 7, 10, 15, 30, 1, TimeSpan.Zero)));

        var result = await resolver.ResolveTwoColumnRequestAsync(Entry(
            "8018", "종목", "첫째,둘째", "현재가"));

        var routed = Assert.IsType<LegacyS8018LoadRequest>(result);
        var request = Assert.IsType<S8018CurrentStockPairLoadRequest>(routed.Request);
        Assert.Equal(S8018StockMarket.Kospi, request.First.Market);
        Assert.Equal(S8018StockMarket.Kosdaq, request.Second.Market);
        Assert.Equal(S8018SessionPhase.AfterKrxClose, request.Session);
        Assert.Equal(
            [
                "SCENE_RESOLVE_KOSPI_STOCK",
                "SCENE_RESOLVE_KOSDAQ_STOCK",
                "SCENE_RESOLVE_KOSPI_STOCK",
                "SCENE_RESOLVE_KOSDAQ_STOCK"
            ],
            executor.Calls.Select(call => call.TableName));
        Assert.All(executor.Calls, call =>
        {
            var parameter = Assert.Single(call.Spec.Parameters);
            var selector = Assert.IsType<string>(parameter.Value);
            Assert.DoesNotContain(
                selector,
                call.Spec.Sql,
                StringComparison.Ordinal);
        });
    }

    [Fact]
    public async Task Expected_nxt_pair_fails_before_any_database_lookup()
    {
        var executor = new RecordingExecutor();
        var resolver = new LegacyParameterizedSceneRequestResolver(executor);

        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            resolver.ResolveTwoColumnRequestAsync(Entry(
                "8018", "종목", "A(NXT),B", "예상체결")));

        Assert.Empty(executor.Calls);
    }

    [Fact]
    public async Task Expected_mixed_nxt_pair_fails_before_any_database_lookup()
    {
        var executor = new RecordingExecutor();
        var resolver = new LegacyParameterizedSceneRequestResolver(executor);

        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            resolver.ResolveTwoColumnRequestAsync(Entry(
                "8018", "지수", "코스피 지수,A(NXT)", "예상체결")));

        Assert.Empty(executor.Calls);
    }

    [Fact]
    public async Task Current_world_stock_identity_uses_the_operator_input_name_consistently()
    {
        var executor = new RecordingExecutor(
            Count(0), Count(0), Count(1),
            Count(1), Count(0));
        var resolver = new LegacyParameterizedSceneRequestResolver(executor);

        var result = await resolver.ResolveTwoColumnRequestAsync(Entry(
            "8018", "종목", "NVIDIA,삼성전자", "현재가"));

        var routed = Assert.IsType<LegacyS8018LoadRequest>(result);
        var request = Assert.IsType<S8018CurrentStockPairLoadRequest>(routed.Request);
        Assert.Equal(S8018StockMarket.World, request.First.Market);
        Assert.Equal("NVIDIA", request.First.StockName);
        Assert.Equal(S8018StockMarket.Kospi, request.Second.Market);

        var worldCall = Assert.Single(
            executor.Calls,
            call => call.TableName == "SCENE_RESOLVE_WORLD_STOCK");
        Assert.Equal(DataSourceKind.Oracle, worldCall.Source);
        Assert.Contains("F_INPUT_NAME = :stockName", worldCall.Spec.Sql, StringComparison.OrdinalIgnoreCase);
        Assert.Contains("F_FDTC = '1'", worldCall.Spec.Sql, StringComparison.OrdinalIgnoreCase);
        Assert.Contains(
            "F_NATC IN ('US', 'TW')",
            worldCall.Spec.Sql,
            StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain("F_KNAM", worldCall.Spec.Sql, StringComparison.OrdinalIgnoreCase);
        Assert.Equal("NVIDIA", Assert.Single(worldCall.Spec.Parameters).Value);
    }

    [Fact]
    public async Task Mixed_market_and_world_stock_remains_closed_without_a_generalized_mixed_dto()
    {
        var executor = new RecordingExecutor(Count(0), Count(0));
        var resolver = new LegacyParameterizedSceneRequestResolver(executor);

        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            resolver.ResolveTwoColumnRequestAsync(Entry(
                "8018", "지수", "코스피 지수,NVIDIA", "현재가")));

        Assert.Equal(
            ["SCENE_RESOLVE_KOSPI_STOCK", "SCENE_RESOLVE_KOSDAQ_STOCK"],
            executor.Calls.Select(call => call.TableName));
        Assert.DoesNotContain(
            executor.Calls,
            call => call.TableName == "SCENE_RESOLVE_WORLD_STOCK");
    }

    [Fact]
    public async Task Futures_and_domestic_stock_remains_closed_before_database_lookup()
    {
        var executor = new RecordingExecutor();
        var resolver = new LegacyParameterizedSceneRequestResolver(executor);

        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            resolver.ResolveTwoColumnRequestAsync(Entry(
                "5032", "종목", "선물,삼성전자", "현재가")));

        Assert.Empty(executor.Calls);
    }

    [Fact]
    public void S8001_accepts_only_the_two_original_markets()
    {
        var resolver = new LegacyParameterizedSceneRequestResolver(new RecordingExecutor());

        Assert.Equal(
            S8001Market.Kospi,
            resolver.CreateS8001Request(Entry("8001", "전체", "코스피", "")).Market);
        Assert.Throws<LegacySceneDataException>(() =>
            resolver.CreateS8001Request(Entry("8001", "전체", "나스닥", "")));
    }

    private static LegacyPlaylistEntry Entry(
        string cutCode,
        string groupCode,
        string subject,
        string subtype) => new(
        "entry",
        cutCode,
        Selection: new LegacySceneSelection(
            groupCode,
            subject,
            "",
            subtype,
            ""));

    private static DataTable Count(int value)
    {
        var table = new DataTable();
        table.Columns.Add("MATCH_COUNT", typeof(int));
        table.Rows.Add(value);
        return table;
    }

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

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

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

        public DataTable Execute(DataSourceKind source, string tableName, string query) =>
            throw new InvalidOperationException("String SQL fallback is forbidden.");

        public Task<DataTable> ExecuteAsync(
            DataSourceKind source,
            string tableName,
            string query,
            CancellationToken cancellationToken = default) =>
            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 RecordedCall(source, tableName, query));
            if (_results.Count == 0)
            {
                throw new InvalidOperationException("No fake query result remains.");
            }

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

    private sealed class FixedTimeProvider(DateTimeOffset utcNow) : TimeProvider
    {
        public override DateTimeOffset GetUtcNow() => utcNow;

        public override TimeZoneInfo LocalTimeZone => TimeZoneInfo.Utc;
    }
}
