#nullable enable

using System.Data;
using MMoneyCoderSharp.Data;

namespace MBN_STOCK_WEBVIEW.Core.Tests;

public sealed class LegacyIndustrySelectionServiceTests
{
    [Theory]
    [InlineData(IndustryMarket.Kospi, "OPERATOR_KOSPI_INDUSTRIES", "FROM T_PART")]
    [InlineData(IndustryMarket.Kosdaq, "OPERATOR_KOSDAQ_INDUSTRIES", "FROM T_KOSDAQ_PART")]
    public async Task GetAsync_uses_one_closed_parameterized_query_and_normalizes_rows(
        IndustryMarket market,
        string expectedTableName,
        string expectedSqlFragment)
    {
        var executor = new RecordingExecutor(Table((" 반도체 ", "013"), ("운송", "021")));
        var service = new LegacyIndustrySelectionService(executor);

        var result = await service.GetAsync(market);

        Assert.Equal(2, result.Count);
        Assert.All(result, value => Assert.Equal(market, value.Market));
        Assert.Equal(new IndustrySelection(market, "반도체", "013"), result[0]);
        var call = Assert.Single(executor.Calls);
        Assert.Equal(DataSourceKind.Oracle, call.Source);
        Assert.Equal(expectedTableName, call.TableName);
        Assert.Contains(expectedSqlFragment, call.Spec.Sql, StringComparison.OrdinalIgnoreCase);
        Assert.Empty(call.Spec.Parameters);
        Assert.Equal(0, executor.StringCalls);
    }

    [Fact]
    public async Task GetAsync_rejects_wrong_schema_unsafe_code_and_control_text()
    {
        var wrongSchema = Table(("반도체", "013"));
        wrongSchema.Columns[0].ColumnName = "NAME";
        await Assert.ThrowsAsync<IndustrySelectionDataException>(() =>
            new LegacyIndustrySelectionService(new RecordingExecutor(wrongSchema))
                .GetAsync(IndustryMarket.Kospi));

        await Assert.ThrowsAsync<IndustrySelectionDataException>(() =>
            new LegacyIndustrySelectionService(new RecordingExecutor(Table(("반도체", "01-3"))))
                .GetAsync(IndustryMarket.Kospi));

        await Assert.ThrowsAsync<IndustrySelectionDataException>(() =>
            new LegacyIndustrySelectionService(new RecordingExecutor(Table(("반\n도체", "013"))))
                .GetAsync(IndustryMarket.Kospi));
    }

    [Fact]
    public async Task GetAsync_preserves_duplicate_name_and_code_rows_in_original_order()
    {
        var result = await new LegacyIndustrySelectionService(new RecordingExecutor(
            Table(
                ("반도체", "013"),
                ("반도체", "014"),
                ("운송", "013"),
                ("반도체", "013"))))
            .GetAsync(IndustryMarket.Kospi);

        Assert.Equal(
        [
            new IndustrySelection(IndustryMarket.Kospi, "반도체", "013"),
            new IndustrySelection(IndustryMarket.Kospi, "반도체", "014"),
            new IndustrySelection(IndustryMarket.Kospi, "운송", "013"),
            new IndustrySelection(IndustryMarket.Kospi, "반도체", "013")
        ],
            result);
    }

    [Fact]
    public async Task GetAsync_rejects_unbounded_results_and_unknown_market_before_string_SQL()
    {
        var oversized = Table(Enumerable.Range(0, LegacyIndustrySelectionService.MaximumResults + 1)
            .Select(index => ($"업종{index}", index.ToString("D3", System.Globalization.CultureInfo.InvariantCulture)))
            .ToArray());
        await Assert.ThrowsAsync<IndustrySelectionDataException>(() =>
            new LegacyIndustrySelectionService(new RecordingExecutor(oversized))
                .GetAsync(IndustryMarket.Kosdaq));

        var executor = new RecordingExecutor(Table());
        await Assert.ThrowsAsync<ArgumentOutOfRangeException>(() =>
            new LegacyIndustrySelectionService(executor).GetAsync((IndustryMarket)999));
        Assert.Empty(executor.Calls);
        Assert.Equal(0, executor.StringCalls);
    }

    private static DataTable Table(params (string Name, string Code)[] rows)
    {
        var table = new DataTable();
        table.Columns.Add("INDUSTRY_NAME", typeof(string));
        table.Columns.Add("INDUSTRY_CODE", typeof(string));
        foreach (var row in rows) table.Rows.Add(row.Name, row.Code);
        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 is forbidden.");
        }

        public Task<DataTable> ExecuteAsync(
            DataSourceKind source,
            string tableName,
            string query,
            CancellationToken cancellationToken = default)
        {
            StringCalls++;
            throw new InvalidOperationException("String SQL 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 remains.");
            return Task.FromResult(_results.Dequeue().Copy());
        }
    }
}
