using System.Data;
using System.IO;
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 LegacyGridMarketSceneRequestResolverTests
{
    public static TheoryData<string, string, string, string, LegacyDomesticMarket> TrendCases => new()
    {
        { "5023", "코스피", "코스피 매매동향", "표그래프", LegacyDomesticMarket.Kospi },
        { "5023", "KOSDAQ", "KOSDAQ_TRADING_TREND", "TABLE_GRAPH", LegacyDomesticMarket.Kosdaq },
        { "5024", "코스피", "코스피 매매동향", "막대그래프", LegacyDomesticMarket.Kospi },
        { "5024", "KOSDAQ", "KOSDAQ_TRADING_TREND", "BAR_GRAPH", LegacyDomesticMarket.Kosdaq }
    };

    public static TheoryData<string, string, string, Type> FixedGridCases => new()
    {
        { "5085", "프로그램 매매", "표그래프", typeof(LegacyS5085SceneLoadRequest) },
        { "5085", "PROGRAM_TRADING", "TABLE_GRAPH", typeof(LegacyS5085SceneLoadRequest) },
        { "6067", "기관 순매수 현황", "표그래프", typeof(LegacyS6067SceneLoadRequest) },
        { "6067", "INSTITUTION_NET_BUY", "TABLE_GRAPH", typeof(LegacyS6067SceneLoadRequest) }
    };

    [Theory]
    [MemberData(nameof(TrendCases))]
    public async Task Trend_cuts_map_MainForm_graphic_and_subtype_fields_without_database_calls(
        string cutCode,
        string groupCode,
        string graphicType,
        string subtype,
        LegacyDomesticMarket expectedMarket)
    {
        var executor = new RecordingExecutor();
        var resolver = new LegacyGridMarketSceneRequestResolver(executor);

        var plan = await resolver.ResolveAsync(
            Entry(cutCode, groupCode, "지수", graphicType, subtype));

        if (cutCode == "5023")
        {
            Assert.Equal(
                expectedMarket,
                Assert.IsType<LegacyS5023SceneLoadRequest>(plan).Request.Market);
        }
        else
        {
            Assert.Equal(
                expectedMarket,
                Assert.IsType<LegacyS5024SceneLoadRequest>(plan).Request.Market);
        }

        Assert.Empty(executor.Calls);
        Assert.Equal(0, executor.StringCalls);
    }

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

        await Assert.ThrowsAsync<LegacySceneDataException>(() => resolver.ResolveAsync(
            Entry("5023", "코스닥", "지수", "코스피 매매동향", "표그래프")));
        await Assert.ThrowsAsync<LegacySceneDataException>(() => resolver.ResolveAsync(
            Entry("5023", "코스피", "지수", "코스피 매매동향", "막대그래프")));
        await Assert.ThrowsAsync<LegacySceneDataException>(() => resolver.ResolveAsync(
            Entry("5024", "코스피", "지수", "코스피 매매동향", "막대그래프", "unexpected")));

        Assert.Empty(executor.Calls);
    }

    [Theory]
    [InlineData("5037", "코스피", LegacyDomesticMarket.Kospi)]
    [InlineData("5037", "KOSDAQ", LegacyDomesticMarket.Kosdaq)]
    public async Task S5037_maps_code_and_trimmed_jongmok_to_a_typed_request(
        string cutCode,
        string groupCode,
        LegacyDomesticMarket expectedMarket)
    {
        var executor = new RecordingExecutor();
        var resolver = new LegacyGridMarketSceneRequestResolver(executor);

        var plan = Assert.IsType<LegacyS5037SceneLoadRequest>(await resolver.ResolveAsync(
            Entry(cutCode, groupCode, "  삼성전자  ", "거래원", "표그래프", "A005930")));

        Assert.Equal(expectedMarket, plan.Request.Market);
        Assert.Equal("삼성전자", plan.Request.StockName);
        Assert.Empty(executor.Calls);
    }

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

        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            new LegacyGridMarketSceneRequestResolver(executor).ResolveAsync(
                Entry("5037", "전체", "삼성전자", "거래원", "표그래프")));

        Assert.Empty(executor.Calls);
    }

    [Theory]
    [MemberData(nameof(FixedGridCases))]
    public async Task Fixed_grid_cuts_validate_selection_identity_and_return_no_parameter_markers(
        string cutCode,
        string graphicType,
        string subtype,
        Type expectedType)
    {
        var executor = new RecordingExecutor();
        var plan = await new LegacyGridMarketSceneRequestResolver(executor).ResolveAsync(
            Entry(cutCode, "전체", "지수", graphicType, subtype));

        Assert.IsType(expectedType, plan);
        Assert.Empty(executor.Calls);
    }

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

        await Assert.ThrowsAsync<LegacySceneDataException>(() => resolver.ResolveAsync(
            Entry("5085", "코스피", "지수", "프로그램 매매", "표그래프")));
        await Assert.ThrowsAsync<LegacySceneDataException>(() => resolver.ResolveAsync(
            Entry("6067", "전체", "지수", "기관 순매수 현황", "막대그래프")));
        await Assert.ThrowsAsync<LegacySceneDataException>(() => resolver.ResolveAsync(
            Entry("6067", "전체", "지수", "기관 순매수 현황", "표그래프", "path.dat")));

        Assert.Empty(executor.Calls);
    }

    [Theory]
    [InlineData("개인 순매도 상위(수동)", S5025ManualAudience.Individual)]
    [InlineData("FOREIGN", S5025ManualAudience.Foreign)]
    [InlineData("기관 순매도 상위(수동)", S5025ManualAudience.Institution)]
    public async Task S5025_maps_only_closed_audiences_when_a_native_trusted_source_is_configured(
        string groupCode,
        S5025ManualAudience expectedAudience)
    {
        var executor = new RecordingExecutor();
        var source = new RecordingManualSource(ManualRows());
        var resolver = new LegacyGridMarketSceneRequestResolver(executor, source);

        var plan = Assert.IsType<LegacyS5025SceneLoadRequest>(await resolver.ResolveAsync(
            Entry("5025", groupCode, "지수", "순매도 상위", "순매도 상위")));

        Assert.Equal(expectedAudience, plan.Request.Audience);
        Assert.Empty(source.Calls);
        Assert.Empty(executor.Calls);
    }

    [Fact]
    public async Task S5025_safe_runtime_loads_and_validates_trusted_rows_before_database_or_mutations()
    {
        var executor = new RecordingExecutor();
        var source = new RecordingManualSource(ManualRows());
        var resolver = new LegacyGridMarketSceneRequestResolver(executor, source);
        var entry = Entry(
            "5025",
            "외국인 순매도 상위(수동)",
            "지수",
            "순매도 상위",
            "순매도 상위");

        var data = await resolver.LoadTrustedS5025Async(entry, CancellationToken.None);
        var mutations = new S5025SceneMutationBuilder().Build(data);

        Assert.Equal(S5025ManualAudience.Foreign, Assert.Single(source.Calls));
        Assert.Equal("외국인", data.Title);
        Assert.Equal(5, data.Rows.Count);
        Assert.Contains(mutations, mutation =>
            mutation is PlayoutSetValue { ObjectName: "종목10", Value: "R5" });
        Assert.Empty(executor.Calls);
        Assert.Equal(0, executor.StringCalls);
    }

    [Fact]
    public async Task S5025_missing_configuration_and_path_injection_fail_before_source_database_or_com()
    {
        var entry = Entry(
            "5025",
            "개인 순매도 상위(수동)",
            "지수",
            "순매도 상위",
            "순매도 상위");
        var executor = new RecordingExecutor();

        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            new LegacyGridMarketSceneRequestResolver(executor).ResolveAsync(entry));

        var source = new RecordingManualSource(ManualRows());
        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            new LegacyGridMarketSceneRequestResolver(executor, source).ResolveAsync(
                entry with
                {
                    Selection = entry.Selection! with { DataCode = "C:\\outside\\개인.dat" }
                }));

        Assert.Empty(source.Calls);
        Assert.Empty(executor.Calls);
    }

    [Fact]
    public async Task S5025_missing_trusted_file_is_normalized_without_leaking_the_native_path()
    {
        const string secretPath = "C:\\trusted\\secret\\개인.dat";
        var executor = new RecordingExecutor();
        var source = new ThrowingManualSource(new FileNotFoundException("Missing " + secretPath));
        var resolver = new LegacyGridMarketSceneRequestResolver(executor, source);

        var exception = await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            resolver.LoadTrustedS5025Async(Entry(
                "5025",
                "개인 순매도 상위(수동)",
                "지수",
                "순매도 상위",
                "순매도 상위")));

        Assert.DoesNotContain(secretPath, exception.Message, StringComparison.OrdinalIgnoreCase);
        Assert.Empty(executor.Calls);
    }

    [Fact]
    public async Task S8003_resolves_Kospi_name_to_code_with_two_parameterized_master_lookups()
    {
        const string stockName = "삼성전자";
        const string stockCode = "005930";
        var executor = new RecordingExecutor(Stock(stockCode), Stock());
        var resolver = new LegacyGridMarketSceneRequestResolver(executor);

        var plan = Assert.IsType<LegacyS8003SceneLoadRequest>(await resolver.ResolveAsync(
            Entry("8003", "코스피", stockName, "호가창", "표그래프", stockCode)));

        Assert.Equal(LegacyDomesticMarket.Kospi, plan.Request.Market);
        Assert.Equal(stockCode, plan.Request.StockCode);
        Assert.Equal(
            ["SCENE_RESOLVE_8003_KOSPI", "SCENE_RESOLVE_8003_KOSDAQ"],
            executor.Calls.Select(call => call.TableName));
        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.Contains("FROM T_STOCK", executor.Calls[0].Spec.Sql, StringComparison.OrdinalIgnoreCase);
        Assert.Contains("FROM T_KOSDAQ_STOCK", executor.Calls[1].Spec.Sql, StringComparison.OrdinalIgnoreCase);
        Assert.Equal(0, executor.StringCalls);
    }

    [Fact]
    public async Task S8003_preserves_MainForm_Kosdaq_later_table_precedence()
    {
        var executor = new RecordingExecutor(Stock("111111"), Stock("222222"));

        var plan = Assert.IsType<LegacyS8003SceneLoadRequest>(await
            new LegacyGridMarketSceneRequestResolver(executor).ResolveAsync(
                Entry("8003", "코스닥", "동명이인", "호가창", "표그래프", "222222")));

        Assert.Equal(LegacyDomesticMarket.Kosdaq, plan.Request.Market);
        Assert.Equal("222222", plan.Request.StockCode);
    }

    [Fact]
    public async Task S8003_fails_closed_for_missing_nonunique_mismatched_or_malformed_master_results()
    {
        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            Resolve8003Async(new RecordingExecutor(Stock(), Stock()), "코스피", ""));

        var duplicate = Stock("005930");
        duplicate.Rows.Add("005930");
        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            Resolve8003Async(new RecordingExecutor(duplicate, Stock()), "코스피", "005930"));

        var schema = Stock("005930");
        schema.Columns[0].ColumnName = "CODE";
        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            Resolve8003Async(new RecordingExecutor(schema, Stock()), "코스피", "005930"));

        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            Resolve8003Async(
                new RecordingExecutor(Stock("005930"), Stock()),
                "코스닥",
                "005930"));
        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            Resolve8003Async(
                new RecordingExecutor(Stock("005930"), Stock()),
                "코스피",
                "999999"));
    }

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

        await Assert.ThrowsAsync<LegacySceneDataException>(() => resolver.ResolveAsync(
            new LegacyPlaylistEntry("entry", "5023")));
        await Assert.ThrowsAsync<LegacySceneDataException>(() => resolver.ResolveAsync(
            Entry("9999", "전체", "지수", "프로그램 매매", "표그래프")));

        Assert.Empty(executor.Calls);
    }

    private static Task<LegacyGridMarketSceneLoadRequest> Resolve8003Async(
        RecordingExecutor executor,
        string market,
        string dataCode) =>
        new LegacyGridMarketSceneRequestResolver(executor).ResolveAsync(
            Entry("8003", market, "삼성전자", "호가창", "표그래프", dataCode));

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

    private static DataTable Stock(params string[] codes)
    {
        var table = new DataTable();
        table.Columns.Add("STOCK_CODE", typeof(object));
        foreach (var code in codes)
        {
            table.Rows.Add(code);
        }

        return table;
    }

    private static IReadOnlyList<S5025TrustedManualRow> ManualRows() =>
    [
        new("L1", "1", "R1", "2"),
        new("L2", "3", "R2", "4"),
        new("L3", "5", "R3", "6"),
        new("L4", "7", "R4", "8"),
        new("L5", "9", "R5", "10")
    ];

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

    private sealed class RecordingManualSource(IReadOnlyList<S5025TrustedManualRow> rows)
        : IS5025TrustedManualDataSource
    {
        public List<S5025ManualAudience> Calls { get; } = [];

        public Task<IReadOnlyList<S5025TrustedManualRow>> ReadAsync(
            S5025ManualAudience audience,
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            Calls.Add(audience);
            return Task.FromResult(rows);
        }
    }

    private sealed class ThrowingManualSource(Exception exception) : IS5025TrustedManualDataSource
    {
        public Task<IReadOnlyList<S5025TrustedManualRow>> ReadAsync(
            S5025ManualAudience audience,
            CancellationToken cancellationToken = default) =>
            Task.FromException<IReadOnlyList<S5025TrustedManualRow>>(exception);
    }
}
