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 ManualSceneDataLoadersTests
{
    [Fact]
    public async Task S5076_UsesAParameterizedOracleQueryAndMapsExplicitDto()
    {
        var table = Table(
            ["STOCK_NAME", "BASE_DATE", "GUSUNG_1", "GUSUNG_2", "GUSUNG_3", "GUSUNG_4", "GUSUNG_5"],
            ["A사", "2026.07", "국내_25.5", "해외_74.5", "", "", ""]);
        var executor = new RecordingExecutor(table);

        var data = await new S5076SceneDataLoader(executor)
            .LoadAsync("A사", CancellationToken.None);

        Assert.Equal(DataSourceKind.Oracle, executor.Source);
        Assert.Equal("INPUT_PIE", executor.TableName);
        Assert.NotNull(executor.Spec);
        Assert.Contains("WHERE STOCK_NAME = :stockName", executor.Spec.Sql, StringComparison.Ordinal);
        var parameter = Assert.Single(executor.Spec.Parameters);
        Assert.Equal("stockName", parameter.Name);
        Assert.Equal("A사", parameter.Value);
        Assert.Equal(DbType.String, parameter.DbType);
        Assert.Equal("A사", data.StockName);
        Assert.Equal("2026.07", data.Basis);
        Assert.Equal(new RevenueSliceData("국내", "25.5", 25.5), data.Slices[0]);
        Assert.Equal(new RevenueSliceData("해외", "74.5", 74.5), data.Slices[1]);
        Assert.Null(data.Slices[2]);
    }

    [Fact]
    public async Task S5076_TreatsLegacyUnderscoreSlotsAsAbsent()
    {
        var table = Table(
            ["STOCK_NAME", "BASE_DATE", "GUSUNG_1", "GUSUNG_2", "GUSUNG_3", "GUSUNG_4", "GUSUNG_5"],
            ["A", "2026.07", "DOMESTIC_100", "_", "_ignored", "", "_"]);

        var data = await new S5076SceneDataLoader(new RecordingExecutor(table))
            .LoadAsync("A", CancellationToken.None);

        Assert.Equal(new RevenueSliceData("DOMESTIC", "100", 100d), data.Slices[0]);
        Assert.All(data.Slices.Skip(1), Assert.Null);
    }

    [Fact]
    public async Task S5081_MapsSixTypedQuarterValuesWithoutStringSqlFallback()
    {
        var table = Table(
            ["STOCK_NAME", "GUSUNG_1", "GUSUNG_2", "GUSUNG_3", "GUSUNG_4", "GUSUNG_5", "GUSUNG_6"],
            ["A사", "1Q_100", "2Q_-50", "3Q_", "4Q_25", "5Q_1,000", "6Q_-2"]);
        var executor = new RecordingExecutor(table);

        var data = await new S5081SceneDataLoader(executor)
            .LoadAsync("A사", CancellationToken.None);

        Assert.Equal(
            new[] { 100, -50, 0, 25, 1000, -2 },
            data.Quarters.Select(item => item.Value));
        Assert.Equal(
            new[] { "1Q", "2Q", "3Q", "4Q", "5Q", "6Q" },
            data.Quarters.Select(item => item.Quarter));
        Assert.Equal(1, executor.SpecCalls);
        Assert.Equal(0, executor.StringCalls);
    }

    [Fact]
    public async Task S5081_KeepsLegacyEmptyQuarterLabels()
    {
        var table = Table(
            ["STOCK_NAME", "GUSUNG_1", "GUSUNG_2", "GUSUNG_3", "GUSUNG_4", "GUSUNG_5", "GUSUNG_6"],
            ["A", "_", "_20_legacy-extra", "3Q_30", "4Q_40", "5Q_50", "6Q_60"]);

        var data = await new S5081SceneDataLoader(new RecordingExecutor(table))
            .LoadAsync("A", CancellationToken.None);

        Assert.Equal(string.Empty, data.Quarters[0].Quarter);
        Assert.Equal(0, data.Quarters[0].Value);
        Assert.Equal(string.Empty, data.Quarters[1].Quarter);
        Assert.Equal(20, data.Quarters[1].Value);
        _ = new S5081SceneMutationBuilder().Build(data);
    }

    [Fact]
    public async Task S5081_NormalizesOnlyTrailingHorizontalTabsInQuarterLabels()
    {
        var accepted = Table(
            ["STOCK_NAME", "GUSUNG_1", "GUSUNG_2", "GUSUNG_3", "GUSUNG_4", "GUSUNG_5", "GUSUNG_6"],
            ["A", "1Q_10", "2023/12\t_159", "3Q_\t30\t", "4Q_40", "5Q_50", "6Q_60"]);

        var data = await new S5081SceneDataLoader(new RecordingExecutor(accepted))
            .LoadAsync("A", CancellationToken.None);
        Assert.Equal("2023/12", data.Quarters[1].Quarter);
        Assert.Equal(159, data.Quarters[1].Value);
        Assert.Equal(30, data.Quarters[2].Value);
        var setValues = new S5081SceneMutationBuilder().Build(data)
            .OfType<PlayoutSetValue>()
            .ToArray();
        Assert.Contains(setValues, mutation =>
            mutation.ObjectName == "value3_1" && mutation.Value == "30");
        Assert.DoesNotContain(setValues, mutation => mutation.Value.Contains('\t'));

        foreach (var invalid in new[] { "2023\t/12_159", "2023/12_\t", "2023/12_15\t9" })
        {
            var rejected = accepted.Copy();
            rejected.Rows[0]["GUSUNG_2"] = invalid;
            await Assert.ThrowsAsync<LegacySceneDataException>(() =>
                new S5081SceneDataLoader(new RecordingExecutor(rejected))
                    .LoadAsync("A", CancellationToken.None));
        }
    }

    [Fact]
    public async Task S5076_RejectsMissingRowsBeforeMutation()
    {
        var table = Table(
            ["STOCK_NAME", "BASE_DATE", "GUSUNG_1", "GUSUNG_2", "GUSUNG_3", "GUSUNG_4", "GUSUNG_5"]);

        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            new S5076SceneDataLoader(new RecordingExecutor(table))
                .LoadAsync("A사", CancellationToken.None));
    }

    [Fact]
    public async Task S5076_AcceptsEquivalentDuplicatesAndRejectsConflictingDuplicates()
    {
        var table = Table(
            ["STOCK_NAME", "BASE_DATE", "GUSUNG_1", "GUSUNG_2", "GUSUNG_3", "GUSUNG_4", "GUSUNG_5"]);
        table.Rows.Add("A", "2026", "A_100", "", "", "", "");
        table.Rows.Add("A", "2026", "A_100", "", "", "", "");

        var data = await new S5076SceneDataLoader(new RecordingExecutor(table))
            .LoadAsync("A", CancellationToken.None);
        Assert.Equal(new RevenueSliceData("A", "100", 100d), data.Slices[0]);

        table.Rows[1]["GUSUNG_1"] = "A_99";
        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            new S5076SceneDataLoader(new RecordingExecutor(table))
                .LoadAsync("A", CancellationToken.None));
    }

    [Fact]
    public async Task S5081_AcceptsEquivalentDuplicatesAndRejectsConflictingDuplicates()
    {
        var table = Table(
            ["STOCK_NAME", "GUSUNG_1", "GUSUNG_2", "GUSUNG_3", "GUSUNG_4", "GUSUNG_5", "GUSUNG_6"]);
        table.Rows.Add("A", "1Q_10", "2Q_20", "3Q_30", "4Q_40", "5Q_50", "6Q_60");
        table.Rows.Add("A", "1Q_10", "2Q_20", "3Q_30", "4Q_40", "5Q_50", "6Q_60");

        var data = await new S5081SceneDataLoader(new RecordingExecutor(table))
            .LoadAsync("A", CancellationToken.None);
        Assert.Equal(new[] { 10, 20, 30, 40, 50, 60 },
            data.Quarters.Select(item => item.Value));

        table.Rows[1]["GUSUNG_6"] = "6Q_61";
        await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            new S5081SceneDataLoader(new RecordingExecutor(table))
                .LoadAsync("A", CancellationToken.None));
    }

    [Fact]
    public async Task LoaderErrorsDoNotIncludeTheSelectedStockName()
    {
        const string sensitiveSelection = "SENSITIVE-SELECTION";
        var exception = await Assert.ThrowsAsync<LegacySceneDataException>(() =>
            new S5076SceneDataLoader(new RecordingExecutor(new DataTable()))
                .LoadAsync(sensitiveSelection, CancellationToken.None));

        Assert.DoesNotContain(sensitiveSelection, exception.Message, StringComparison.Ordinal);
    }

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

    private sealed class RecordingExecutor(DataTable result) : IDataQueryExecutor
    {
        public DataSourceKind Source { get; private set; }

        public string? TableName { get; private set; }

        public DataQuerySpec? Spec { get; private set; }

        public int SpecCalls { get; private set; }

        public int StringCalls { get; private set; }

        public DataTable Execute(DataSourceKind source, string tableName, string query)
        {
            StringCalls++;
            throw new InvalidOperationException("String query fallback is forbidden in this test.");
        }

        public Task<DataTable> ExecuteAsync(
            DataSourceKind source,
            string tableName,
            string query,
            CancellationToken cancellationToken = default)
        {
            StringCalls++;
            throw new InvalidOperationException("String query fallback is forbidden in this test.");
        }

        public Task<DataTable> ExecuteAsync(
            DataSourceKind source,
            string tableName,
            DataQuerySpec query,
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            Source = source;
            TableName = tableName;
            Spec = query;
            SpecCalls++;
            return Task.FromResult(result.Copy());
        }
    }
}
