#nullable enable

using System.Data;
using MMoneyCoderSharp.Data;

namespace MBN_STOCK_WEBVIEW.Core.Tests;

public sealed class LegacyNamedComparisonRestoreServiceTests
{
    [Fact]
    public async Task ResolveAsync_RestoresFixedPairsWithoutGuessingOrDatabaseAccess()
    {
        var executor = new RecordingExecutor(_ => throw new InvalidOperationException());
        var service = new LegacyNamedComparisonRestoreService(executor);

        var result = await service.ResolveAsync(
        [
            Row(groupCode: "지수", subject: "코스피 지수,코스닥 지수", graphicType: "2열판"),
            Row(
                itemIndex: 1,
                groupCode: "지수",
                subject: "코스피200 지수,KRX100지수",
                graphicType: "2열판",
                subtype: "예상체결")
        ]);

        Assert.Collection(
            result.Rows,
            row =>
            {
                Assert.True(row.IsResolved);
                Assert.Equal("two-column-current", row.ActionId);
                Assert.Equal("Kospi", row.First!.MarketTarget);
                Assert.Equal("Kosdaq", row.Second!.MarketTarget);
            },
            row =>
            {
                Assert.True(row.IsResolved);
                Assert.Equal("two-column-expected", row.ActionId);
                Assert.Equal("Kospi200", row.First!.MarketTarget);
                Assert.Equal("Krx100", row.Second!.MarketTarget);
            });
        Assert.Empty(executor.Calls);
    }

    [Theory]
    [InlineData("삼성전자,SK하이닉스")]
    [InlineData("삼성전자(NXT),기아(NXT)")]
    [InlineData("엔비디아,애플")]
    public async Task ResolveAsync_RejectsTwoColumnStocksBecauseLegacyRowsLostEachMarket(
        string subject)
    {
        var executor = new RecordingExecutor(_ => throw new InvalidOperationException());
        var service = new LegacyNamedComparisonRestoreService(executor);

        var result = await service.ResolveAsync(
        [
            Row(groupCode: "종목", subject: subject, graphicType: "2열판")
        ]);

        var outcome = Assert.Single(result.Rows);
        Assert.False(outcome.IsResolved);
        Assert.Equal(
            LegacyNamedComparisonRestoreFailure.MissingMarketIdentity,
            outcome.Failure);
        Assert.Empty(executor.Calls);
    }

    [Fact]
    public async Task ResolveAsync_ReportsUnsupportedFixedExpectedPairAsOneFailedRow()
    {
        var executor = new RecordingExecutor(_ => throw new InvalidOperationException());
        var service = new LegacyNamedComparisonRestoreService(executor);

        var result = await service.ResolveAsync(
        [
            Row(
                groupCode: "지수",
                subject: "해외지수-다우,해외지수-나스닥",
                graphicType: "2열판",
                subtype: "예상체결")
        ]);

        Assert.Equal(
            LegacyNamedComparisonRestoreFailure.UnsupportedAction,
            Assert.Single(result.Rows).Failure);
        Assert.Empty(executor.Calls);
    }

    [Fact]
    public async Task ResolveAsync_BlocksAllSixteenStaleMarketLostRowsWithoutSearchingByName()
    {
        var executor = new RecordingExecutor(_ => throw new InvalidOperationException());
        var service = new LegacyNamedComparisonRestoreService(executor);
        var rows = Enumerable.Range(0, 16)
            .Select(index => Row(
                itemIndex: index,
                groupCode: "종목",
                subject: $"과거종목{index * 2},과거종목{index * 2 + 1}",
                graphicType: "2열판"))
            .ToArray();

        var result = await service.ResolveAsync(rows);

        Assert.Equal(16, result.Rows.Count);
        Assert.All(result.Rows, outcome => Assert.Equal(
            LegacyNamedComparisonRestoreFailure.MissingMarketIdentity,
            outcome.Failure));
        Assert.Empty(executor.Calls);
    }

    [Theory]
    [InlineData("종목별 비교분석_캔들 그래프", "", "comparison-candle")]
    [InlineData("종목별 비교분석_라인 그래프", "", "comparison-line")]
    [InlineData("종목별 수익률 비교", "5일", "return-5d")]
    [InlineData("종목별 수익률 비교", "1개월", "return-1m")]
    [InlineData("종목별 수익률 비교", "3개월", "return-3m")]
    [InlineData("종목별 수익률 비교", "6개월", "return-6m")]
    [InlineData("종목별 수익률 비교", "12개월", "return-12m")]
    public async Task ResolveAsync_RestoresEveryGraphSubtypeWithExactPerTargetMarkets(
        string graphicType,
        string subtype,
        string expectedAction)
    {
        var executor = new RecordingExecutor(call =>
        {
            var name = Assert.IsType<string>(Assert.Single(call.Spec.Parameters).Value);
            return name switch
            {
                "삼성전자" => StockTable("삼성전자", "005930"),
                "에이비엘바이오" => StockTable("에이비엘바이오", "298380"),
                _ => throw new InvalidOperationException()
            };
        });
        var service = new LegacyNamedComparisonRestoreService(executor);

        var result = await service.ResolveAsync(
        [
            Row(
                groupCode: "코스피,코스닥",
                subject: "삼성전자,에이비엘바이오",
                graphicType: graphicType,
                subtype: subtype)
        ]);

        var outcome = Assert.Single(result.Rows);
        Assert.True(outcome.IsResolved);
        Assert.Equal(expectedAction, outcome.ActionId);
        Assert.Equal("kospi", outcome.First!.Market);
        Assert.Equal("005930", outcome.First.StockCode);
        Assert.Equal("kosdaq", outcome.Second!.Market);
        Assert.Equal("298380", outcome.Second.StockCode);
        Assert.Equal(2, executor.Calls.Count);
        Assert.All(executor.Calls, call =>
        {
            call.Spec.ValidateFor(call.Source);
            Assert.DoesNotContain(
                Assert.IsType<string>(call.Spec.Parameters[0].Value),
                call.Spec.Sql,
                StringComparison.Ordinal);
        });
        Assert.Equal(0, executor.StringSqlCallCount);
    }

    [Fact]
    public async Task ResolveAsync_CachesRepeatedIdentityAcrossRows()
    {
        var executor = new RecordingExecutor(call =>
            StockTable(
                Assert.IsType<string>(call.Spec.Parameters[0].Value),
                call.TableName.EndsWith("KOSPI", StringComparison.Ordinal)
                    ? "005930"
                    : "298380"));
        var service = new LegacyNamedComparisonRestoreService(executor);

        var result = await service.ResolveAsync(
        [
            Row(
                groupCode: "코스피,코스닥",
                subject: "삼성전자,에이비엘바이오",
                graphicType: "종목별 비교분석_캔들 그래프"),
            Row(
                itemIndex: 1,
                groupCode: "코스피,코스닥",
                subject: "삼성전자,에이비엘바이오",
                graphicType: "종목별 수익률 비교",
                subtype: "5일")
        ]);

        Assert.All(result.Rows, row => Assert.True(row.IsResolved));
        Assert.Equal(2, executor.Calls.Count);
    }

    [Fact]
    public async Task ResolveAsync_FailsEachMissingAmbiguousAndMalformedIdentityClosed()
    {
        var executor = new RecordingExecutor(call =>
        {
            var name = Assert.IsType<string>(call.Spec.Parameters[0].Value);
            return name switch
            {
                "없음" => StockTable(),
                "중복" => StockTable(("중복", "111111"), ("중복", "222222")),
                "오염" => WrongSchemaTable(),
                "정상" => StockTable("정상", "333333"),
                _ => throw new InvalidOperationException()
            };
        });
        var service = new LegacyNamedComparisonRestoreService(executor);

        var result = await service.ResolveAsync(
        [
            Row(
                groupCode: "코스피,코스닥",
                subject: "없음,정상",
                graphicType: "종목별 비교분석_캔들 그래프"),
            Row(
                itemIndex: 1,
                groupCode: "코스피,코스닥",
                subject: "중복,정상",
                graphicType: "종목별 비교분석_라인 그래프"),
            Row(
                itemIndex: 2,
                groupCode: "코스피,코스닥",
                subject: "오염,정상",
                graphicType: "종목별 수익률 비교",
                subtype: "1개월")
        ]);

        Assert.Equal(
            new LegacyNamedComparisonRestoreFailure?[]
            {
                LegacyNamedComparisonRestoreFailure.IdentityNotFound,
                LegacyNamedComparisonRestoreFailure.IdentityAmbiguous,
                LegacyNamedComparisonRestoreFailure.DatabaseDataInvalid
            },
            result.Rows.Select(row => row.Failure));
    }

    [Fact]
    public async Task ResolveAsync_RejectsNxtGraphsBeforeTheyCanBecomePlayable()
    {
        var executor = new RecordingExecutor(call =>
            StockTable(
                Assert.IsType<string>(call.Spec.Parameters[0].Value),
                "005930"));
        var service = new LegacyNamedComparisonRestoreService(executor);

        var result = await service.ResolveAsync(
        [
            Row(
                groupCode: "코스피_NXT,코스피_NXT",
                subject: "삼성전자(NXT),기아(NXT)",
                graphicType: "종목별 비교분석_캔들 그래프")
        ]);

        var outcome = Assert.Single(result.Rows);
        Assert.Equal(
            LegacyNamedComparisonRestoreFailure.UnsupportedAction,
            outcome.Failure);
        Assert.Empty(executor.Calls);
    }

    [Theory]
    [InlineData("종목별 수익률 비교", "20일")]
    [InlineData("종목별 비교분석_캔들 그래프", "5일")]
    [InlineData("2열판", "CURRENT")]
    public async Task ResolveAsync_RejectsUnknownOrCanonicalizedSubtypeGrammar(
        string graphicType,
        string subtype)
    {
        var executor = new RecordingExecutor(_ => throw new InvalidOperationException());
        var service = new LegacyNamedComparisonRestoreService(executor);

        var result = await service.ResolveAsync(
        [
            Row(
                groupCode: graphicType == "2열판" ? "지수" : "코스피,코스피",
                subject: graphicType == "2열판"
                    ? "코스피 지수,코스닥 지수"
                    : "삼성전자,기아",
                graphicType: graphicType,
                subtype: subtype)
        ]);

        Assert.Equal(
            LegacyNamedComparisonRestoreFailure.InvalidRow,
            Assert.Single(result.Rows).Failure);
        Assert.Empty(executor.Calls);
    }

    [Fact]
    public async Task ResolveAsync_HonorsCancellationAndRejectsDuplicateIndexes()
    {
        var executor = new RecordingExecutor(_ => throw new InvalidOperationException());
        var service = new LegacyNamedComparisonRestoreService(executor);
        using var cancellation = new CancellationTokenSource();
        cancellation.Cancel();

        await Assert.ThrowsAnyAsync<OperationCanceledException>(
            () => service.ResolveAsync(
                [Row(groupCode: "지수", subject: "코스피 지수,코스닥 지수", graphicType: "2열판")],
                cancellation.Token));
        await Assert.ThrowsAsync<ArgumentException>(() => service.ResolveAsync(
        [
            Row(groupCode: "지수", subject: "코스피 지수,코스닥 지수", graphicType: "2열판"),
            Row(groupCode: "지수", subject: "코스피 지수,코스닥 지수", graphicType: "2열판")
        ]));
        Assert.Empty(executor.Calls);
    }

    private static LegacyNamedComparisonRestoreRow Row(
        int itemIndex = 0,
        string groupCode = "코스피,코스피",
        string subject = "삼성전자,기아",
        string graphicType = "종목별 비교분석_캔들 그래프",
        string subtype = "",
        string pageText = "1/1",
        string dataCode = "") =>
        new(
            itemIndex,
            true,
            groupCode,
            subject,
            graphicType,
            subtype,
            pageText,
            dataCode);

    private static DataTable StockTable(params (string Name, string Code)[] rows)
    {
        var table = new DataTable();
        table.Columns.Add("STOCK_NAME", typeof(string));
        table.Columns.Add("STOCK_CODE", typeof(string));
        foreach (var row in rows)
        {
            table.Rows.Add(row.Name, row.Code);
        }

        return table;
    }

    private static DataTable StockTable(string name, string code) =>
        StockTable((name, code));

    private static DataTable WrongSchemaTable()
    {
        var table = new DataTable();
        table.Columns.Add("stock_name", typeof(string));
        table.Columns.Add("STOCK_CODE", typeof(string));
        return table;
    }

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

    private sealed class RecordingExecutor(Func<RecordedCall, DataTable> handler)
        : IDataQueryExecutor
    {
        private int _stringSqlCallCount;

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

        public int StringSqlCallCount => Volatile.Read(ref _stringSqlCallCount);

        public DataTable Execute(DataSourceKind source, string tableName, string query)
        {
            Interlocked.Increment(ref _stringSqlCallCount);
            throw new InvalidOperationException();
        }

        public Task<DataTable> ExecuteAsync(
            DataSourceKind source,
            string tableName,
            string query,
            CancellationToken cancellationToken = default)
        {
            Interlocked.Increment(ref _stringSqlCallCount);
            throw new InvalidOperationException();
        }

        public Task<DataTable> ExecuteAsync(
            DataSourceKind source,
            string tableName,
            DataQuerySpec query,
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            query.ValidateFor(source);
            var call = new RecordedCall(source, tableName, query, cancellationToken);
            Calls.Add(call);
            return Task.FromResult(handler(call).Copy());
        }
    }
}
