using System.Data;
using MMoneyCoderSharp.Data;

namespace MBN_STOCK_WEBVIEW.Core.Tests;

public sealed class LegacyOverseasStockSearchServiceTests
{
    [Fact]
    public async Task BlankQuery_UsesOriginalUsTwInitialScopeAndOrder()
    {
        var executor = new RecordingExecutor(WorldStockTable(("Apple", "NAS@AAPL", "US")));
        var result = await new LegacyOverseasStockSearchService(executor).SearchAsync("  ");

        Assert.Equal(string.Empty, result.Query);
        Assert.Equal(new WorldStockSearchItem("Apple", "NAS@AAPL", "US"), Assert.Single(result.Items));
        var call = Assert.Single(executor.Calls);
        Assert.Equal("OVERSEAS_STOCK_SEARCH", call.Name);
        Assert.Contains("F_NATC IN ('US', 'TW')", call.Spec.Sql, StringComparison.Ordinal);
        Assert.Contains("ORDER BY F_INPUT_NAME", call.Spec.Sql, StringComparison.Ordinal);
        Assert.DoesNotContain("ORDER BY F_INPUT_NAME, F_SYMB", call.Spec.Sql, StringComparison.Ordinal);
        Assert.Single(call.Spec.Parameters);
        Assert.Equal("row_limit", call.Spec.Parameters[0].Name);
    }

    [Fact]
    public async Task NonblankQuery_UsesOriginalAllNationScopeWithBoundEscapedTerm()
    {
        var executor = new RecordingExecutor(WorldStockTable(("London Stock", "LNS@LON", "GB")));
        var result = await new LegacyOverseasStockSearchService(executor).SearchAsync(" lo_% ");

        Assert.Equal("lo_%", result.Query);
        Assert.Equal("GB", Assert.Single(result.Items).NationCode);
        var call = Assert.Single(executor.Calls);
        Assert.DoesNotContain("F_NATC IN", call.Spec.Sql, StringComparison.Ordinal);
        Assert.Contains("LIKE '%' || :search_term || '%' ESCAPE '!'", call.Spec.Sql, StringComparison.Ordinal);
        Assert.Equal("LO!_!%", call.Spec.Parameters[0].Value);
        Assert.Equal("row_limit", call.Spec.Parameters[1].Name);
    }

    [Fact]
    public async Task InitialConsonantQuery_UsesAllNationSearchAndFiltersTheFullCandidateWindow()
    {
        var executor = new RecordingExecutor(WorldStockTable(
            ("삼성전자", "LNS@SAMSUNG", "GB"),
            ("Apple", "NAS@AAPL", "US"),
            ("신성전자", "TSE@SHINSEONG", "JP")));
        var service = new LegacyOverseasStockSearchService(executor);

        var result = await service.SearchAsync(" ㅅㅅㅈㅈ ", maximumResults: 1);

        Assert.Equal("ㅅㅅㅈㅈ", result.Query);
        Assert.True(result.IsTruncated);
        Assert.Equal(
            new WorldStockSearchItem("삼성전자", "LNS@SAMSUNG", "GB"),
            Assert.Single(result.Items));
        var call = Assert.Single(executor.Calls);
        Assert.DoesNotContain("F_NATC IN", call.Spec.Sql, StringComparison.Ordinal);
        Assert.Contains(
            "LIKE '%' || :search_term || '%' ESCAPE '!'",
            call.Spec.Sql,
            StringComparison.Ordinal);
        Assert.Equal(string.Empty, call.Spec.Parameters[0].Value);
        Assert.Equal(
            LegacyOverseasStockSearchService.MaximumResults +
                LegacyOverseasStockSearchService.MaximumIsolatedRows + 1,
            call.Spec.Parameters[1].Value);
    }

    [Fact]
    public async Task AllowsDuplicateInputNamesWhenTheExactIdentityDiffersButRejectsDuplicateIdentity()
    {
        var ambiguous = new RecordingExecutor(WorldStockTable(
            ("Alpha", "AAA", "US"),
            ("Alpha", "AAA", "GB")));
        var result = await new LegacyOverseasStockSearchService(ambiguous).SearchAsync("alpha");
        Assert.Equal(2, result.Items.Count);
        Assert.Equal(["US", "GB"], result.Items.Select(item => item.NationCode));

        var duplicate = new RecordingExecutor(WorldStockTable(
            ("Alpha", "AAA", "US"),
            ("Alpha", "AAA", "US")));
        await Assert.ThrowsAsync<WorldStockSearchDataException>(
            () => new LegacyOverseasStockSearchService(duplicate).SearchAsync("alpha"));
    }

    [Fact]
    public async Task NormalizesOuterAsciiPaddingButRejectsAResultingIdentityCollision()
    {
        var padded = new RecordingExecutor(WorldStockTable(
            (" Alpha ", " AAA ", " US ")));
        var result = await new LegacyOverseasStockSearchService(padded).SearchAsync("alpha");

        Assert.Equal(
            new WorldStockSearchItem("Alpha", "AAA", "US"),
            Assert.Single(result.Items));

        var collision = new RecordingExecutor(WorldStockTable(
            ("Alpha", "AAA", "US"),
            (" Alpha ", " AAA ", " US ")));
        await Assert.ThrowsAsync<WorldStockSearchDataException>(
            () => new LegacyOverseasStockSearchService(collision).SearchAsync("alpha"));
    }

    [Fact]
    public void CurrentProviderPunctuationIsCanonicalButSerializationDelimitersRemainBlocked()
    {
        Assert.True(LegacyNamedForeignStockRestoreService.IsCanonicalBoundSymbol("IDX@A&B"));
        Assert.True(LegacyNamedForeignStockRestoreService.IsCanonicalBoundSymbol("IDX@(A)"));
        Assert.False(LegacyNamedForeignStockRestoreService.IsCanonicalBoundSymbol("IDX|A"));
        Assert.False(LegacyNamedForeignStockRestoreService.IsCanonicalBoundSymbol("IDX^A"));
        Assert.False(LegacyNamedForeignStockRestoreService.IsCanonicalBoundSymbol("IDX/A"));
    }

    [Fact]
    public async Task DefaultQueryUsesTheFullBoundAndReportsWhenOriginalUnboundedResultsAreTruncated()
    {
        var rows = Enumerable.Range(0, LegacyOverseasStockSearchService.MaximumResults + 1)
            .Select(index => ($"Stock {index:D3}", $"LNS@{index:D4}", "GB"))
            .ToArray();
        var executor = new RecordingExecutor(WorldStockTable(rows));

        var result = await new LegacyOverseasStockSearchService(executor).SearchAsync("stock");

        Assert.Equal(LegacyOverseasStockSearchService.MaximumResults, result.Items.Count);
        Assert.True(result.IsTruncated);
        var rowLimit = Assert.Single(Assert.Single(executor.Calls).Spec.Parameters,
            parameter => parameter.Name == "row_limit");
        Assert.Equal(
            LegacyOverseasStockSearchService.MaximumResults +
                LegacyOverseasStockSearchService.MaximumIsolatedRows + 1,
            rowLimit.Value);
    }

    [Fact]
    public async Task CurrentUsTwPopulation_IsolatesBlankNamesWithoutLosingValidRows()
    {
        var valid = Enumerable.Range(0, 1_474)
            .Select(index => ($"Stock {index:D4}", $"NAS@{index:D4}", "US"))
            .ToArray();
        var table = WorldStockTable(valid);
        for (var index = 0; index < 16; index++)
        {
            table.Rows.Add(DBNull.Value, $"NAS@BLANK{index:D2}", "US");
        }

        var result = await new LegacyOverseasStockSearchService(
            new RecordingExecutor(table)).SearchAsync(string.Empty);

        Assert.Equal(1_474, result.Items.Count);
        Assert.Equal(16, result.IsolatedInvalidRowCount);
        Assert.True(result.IsTruncated);
    }

    private static DataTable WorldStockTable(params (string Name, string Symbol, string Nation)[] rows)
    {
        var table = new DataTable();
        table.Columns.Add("F_INPUT_NAME", typeof(string));
        table.Columns.Add("F_SYMB", typeof(string));
        table.Columns.Add("F_NATC", typeof(string));
        foreach (var row in rows) table.Rows.Add(row.Name, row.Symbol, row.Nation);
        return table;
    }

    private sealed record Call(string Name, DataQuerySpec Spec);

    private sealed class RecordingExecutor(DataTable result) : IDataQueryExecutor
    {
        public List<Call> Calls { get; } = [];

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

        public Task<DataTable> ExecuteAsync(
            DataSourceKind source,
            string queryName,
            string query,
            CancellationToken cancellationToken = default) =>
            throw new InvalidOperationException("String SQL fallback is forbidden.");

        public Task<DataTable> ExecuteAsync(
            DataSourceKind source,
            string queryName,
            DataQuerySpec spec,
            CancellationToken cancellationToken = default)
        {
            Assert.Equal(DataSourceKind.Oracle, source);
            cancellationToken.ThrowIfCancellationRequested();
            Calls.Add(new Call(queryName, spec));
            return Task.FromResult(result.Copy());
        }
    }
}
