using MMoneyCoderSharp.Data;

namespace MBN_STOCK_WEBVIEW.LegacyApplication.Tests;

public sealed class LegacyExpertWorkflowTests
{
    private static readonly ExpertSelectionIdentity Alpha = new("0001", "강전문");
    private static readonly ExpertSelectionIdentity Beta = new("0002", "김전문");

    [Fact]
    public async Task SearchSelectPreview_MaterializesClosedS5074DraftFromActionIdOnly()
    {
        var service = new DelegateExpertService
        {
            Search = (query, _, _) => Task.FromResult(SearchResult(query, Alpha, Beta)),
            Preview = (identity, _, _) => Task.FromResult(
                PreviewResult(identity, Recommendations(6)))
        };
        var controller = new LegacyExpertWorkflowController(service);

        var search = await controller.SearchAsync("  전문  ");
        var preview = await controller.SelectExpertAsync(search.Search.Results[1].SelectionId);
        var draft = controller.MaterializeSelectedAction("five-row-current");

        Assert.Equal("전문", service.SearchQuery);
        Assert.Equal(500, service.SearchMaximum);
        Assert.Equal(100, service.PreviewMaximum);
        Assert.Equal("김전문", preview.SelectedExpert?.ExpertName);
        Assert.Equal(6, preview.Preview.Items.Count);
        Assert.Equal(2, preview.Preview.PageCount);
        Assert.True(Assert.Single(preview.Actions).IsAvailable);
        Assert.Equal("s5074", draft.BuilderKey);
        Assert.Equal("5074", draft.CutCode);
        Assert.Equal(new[] { "5074" }, draft.Aliases);
        Assert.Equal("PAGED_EXPERT", draft.Selection.GroupCode);
        Assert.Equal("김전문", draft.Selection.Subject);
        Assert.Equal("INPUT_ORDER", draft.Selection.GraphicType);
        Assert.Equal("CURRENT", draft.Selection.Subtype);
        Assert.Equal("0002", draft.Selection.DataCode);
        Assert.Equal(5, draft.PageSize);
        Assert.Equal(2, draft.PageCount);
        Assert.Equal(20, draft.MaximumPageCount);
        Assert.Equal("1/2", draft.PageText);
        Assert.Equal(draft.DraftId, draft.ToLegacyPlaylistEntry().EntryId);
        Assert.Equal("legacy-expert-workflow", draft.Source);

        var method = typeof(LegacyExpertWorkflowController).GetMethod(
            nameof(LegacyExpertWorkflowController.MaterializeSelectedAction));
        Assert.NotNull(method);
        Assert.Collection(
            method.GetParameters(),
            parameter => Assert.Equal("actionId", parameter.Name));
        Assert.Throws<ArgumentOutOfRangeException>(
            () => controller.MaterializeSelectedAction("5074-caller-override"));
    }

    [Fact]
    public async Task Search_AcceptsConservativeTruncationAfterInitialFiltering()
    {
        var service = new DelegateExpertService
        {
            Search = (query, _, _) => Task.FromResult(
                SearchResult(query, Alpha) with { IsTruncated = true })
        };
        var controller = new LegacyExpertWorkflowController(service);

        var searched = await controller.SearchAsync("ㄱㅈㅁ");

        Assert.Equal(Alpha.ExpertName, Assert.Single(searched.Search.Results).ExpertName);
        Assert.True(searched.Search.IsTruncated);
    }

    [Fact]
    public async Task Search_StillRejectsImpossiblePartialTruncationForOrdinaryText()
    {
        var service = new DelegateExpertService
        {
            Search = (query, _, _) => Task.FromResult(
                SearchResult(query, Alpha) with { IsTruncated = true })
        };
        var controller = new LegacyExpertWorkflowController(service);

        await Assert.ThrowsAsync<ExpertSelectionDataException>(
            () => controller.SearchAsync("Expert"));
    }

    [Theory]
    [InlineData(1, false, 1)]
    [InlineData(5, false, 1)]
    [InlineData(6, false, 2)]
    [InlineData(100, true, 20)]
    public async Task PageN_UsesFiveRowsAndNeverExceedsTwentyPages(
        int itemCount,
        bool truncated,
        int expectedPageCount)
    {
        var service = new DelegateExpertService
        {
            Search = (query, _, _) => Task.FromResult(SearchResult(query, Alpha)),
            Preview = (identity, _, _) => Task.FromResult(
                PreviewResult(identity, Recommendations(itemCount), truncated))
        };
        var controller = new LegacyExpertWorkflowController(service);
        var search = await controller.SearchAsync(string.Empty);
        var selected = await controller.SelectExpertAsync(
            search.Search.Results[0].SelectionId);

        var draft = controller.MaterializeSelectedAction(
            LegacyExpertWorkflowController.ActionId);

        Assert.Equal(expectedPageCount, selected.Preview.PageCount);
        Assert.Equal(expectedPageCount, draft.PageCount);
        Assert.InRange(draft.PageCount, 1, 20);
        Assert.Equal(truncated, draft.PreviewTruncated);
    }

    [Fact]
    public async Task EmptyRecommendationPreview_PreservesOriginalOneOverZeroCut()
    {
        var service = new DelegateExpertService
        {
            Search = (query, _, _) => Task.FromResult(SearchResult(query, Alpha)),
            Preview = (identity, _, _) => Task.FromResult(
                PreviewResult(identity, []))
        };
        var controller = new LegacyExpertWorkflowController(service);
        var search = await controller.SearchAsync(string.Empty);

        var selected = await controller.SelectExpertAsync(
            search.Search.Results[0].SelectionId);

        Assert.Equal(LegacyExpertRequestStatus.Ready, selected.Preview.Status);
        Assert.Empty(selected.Preview.Items);
        Assert.Equal(0, selected.Preview.PageCount);
        Assert.True(Assert.Single(selected.Actions).IsAvailable);
        var draft = controller.MaterializeSelectedAction(
            LegacyExpertWorkflowController.ActionId);
        Assert.Equal(0, draft.ItemCount);
        Assert.Equal(0, draft.PageCount);
        Assert.Equal("1/0", draft.PageText);
    }

    [Fact]
    public async Task NullBuyAmount_RemainsVisibleAndCanBeComposedForFreshPrepareValidation()
    {
        var service = new DelegateExpertService
        {
            Search = (query, _, _) => Task.FromResult(SearchResult(query, Alpha)),
            Preview = (identity, _, _) => Task.FromResult(PreviewResult(identity,
            [
                new ExpertRecommendationPreview(0, "005930", "Samsung", null),
                new ExpertRecommendationPreview(1, "035720", "Kakao", 42_000m)
            ]))
        };
        var controller = new LegacyExpertWorkflowController(service);
        var search = await controller.SearchAsync(string.Empty);

        var selected = await controller.SelectExpertAsync(search.Search.Results[0].SelectionId);

        Assert.Equal(LegacyExpertRequestStatus.Ready, selected.Preview.Status);
        Assert.Equal(2, selected.Preview.Items.Count);
        Assert.Null(selected.Preview.Items[0].BuyAmount);
        Assert.True(Assert.Single(selected.Actions).IsAvailable);

        var draft = controller.MaterializeSelectedAction(
            LegacyExpertWorkflowController.ActionId);

        Assert.Equal(2, draft.ItemCount);
        Assert.Equal(1, draft.PageCount);
        Assert.Equal("1/1", draft.PageText);
        Assert.Equal("0001", draft.ExpertCode);
        Assert.Equal(Alpha.ExpertName, draft.ExpertName);
    }

    [Fact]
    public async Task LateSearchResponse_CannotReplaceNewerExpertResult()
    {
        var first = new TaskCompletionSource<ExpertSearchResult>(
            TaskCreationOptions.RunContinuationsAsynchronously);
        var second = new TaskCompletionSource<ExpertSearchResult>(
            TaskCreationOptions.RunContinuationsAsynchronously);
        var service = new DelegateExpertService
        {
            Search = (query, _, _) => query == "첫째" ? first.Task : second.Task
        };
        var controller = new LegacyExpertWorkflowController(service);

        var firstRequest = controller.SearchAsync("첫째");
        var secondRequest = controller.SearchAsync("둘째");
        second.SetResult(SearchResult("둘째", Beta));
        var newest = await secondRequest;
        first.SetResult(SearchResult("첫째", Alpha));
        var staleCompletion = await firstRequest;

        Assert.Equal("둘째", newest.Search.Query);
        Assert.Equal("김전문", Assert.Single(newest.Search.Results).ExpertName);
        Assert.Equal("둘째", staleCompletion.Search.Query);
        Assert.Equal("김전문", Assert.Single(staleCompletion.Search.Results).ExpertName);
        Assert.Equal("둘째", controller.Current.Search.Query);
    }

    [Fact]
    public async Task LatePreviewResponse_CannotReplaceNewerExpertSelection()
    {
        var alphaPreview = new TaskCompletionSource<ExpertPreviewResult>(
            TaskCreationOptions.RunContinuationsAsynchronously);
        var betaPreview = new TaskCompletionSource<ExpertPreviewResult>(
            TaskCreationOptions.RunContinuationsAsynchronously);
        var service = new DelegateExpertService
        {
            Search = (query, _, _) => Task.FromResult(SearchResult(query, Alpha, Beta)),
            Preview = (identity, _, _) => identity == Alpha
                ? alphaPreview.Task
                : betaPreview.Task
        };
        var controller = new LegacyExpertWorkflowController(service);
        var search = await controller.SearchAsync(string.Empty);

        var firstSelection = controller.SelectExpertAsync(search.Search.Results[0].SelectionId);
        var secondSelection = controller.SelectExpertAsync(search.Search.Results[1].SelectionId);
        betaPreview.SetResult(PreviewResult(Beta, Recommendations(2, "B")));
        var newest = await secondSelection;
        alphaPreview.SetResult(PreviewResult(Alpha, Recommendations(1, "A")));
        var staleCompletion = await firstSelection;

        Assert.Equal("김전문", newest.SelectedExpert?.ExpertName);
        Assert.Equal(2, newest.Preview.Items.Count);
        Assert.Equal("김전문", staleCompletion.SelectedExpert?.ExpertName);
        Assert.Equal(2, staleCompletion.Preview.Items.Count);
        Assert.All(staleCompletion.Preview.Items, item =>
            Assert.StartsWith("B", item.StockCode, StringComparison.Ordinal));
    }

    [Fact]
    public async Task StrictExpertIdentityOrderAndPreviewIdentity_AreRevalidated()
    {
        var unsorted = new DelegateExpertService
        {
            Search = (query, _, _) => Task.FromResult(SearchResult(query, Beta, Alpha))
        };
        var unsortedController = new LegacyExpertWorkflowController(unsorted);
        await Assert.ThrowsAsync<ExpertSelectionDataException>(
            () => unsortedController.SearchAsync(string.Empty));
        Assert.Equal(
            LegacyExpertRequestStatus.Error,
            unsortedController.Current.Search.Status);

        var wrongPreview = new DelegateExpertService
        {
            Search = (query, _, _) => Task.FromResult(SearchResult(query, Alpha)),
            Preview = (_, _, _) => Task.FromResult(
                PreviewResult(Beta, Recommendations(1)))
        };
        var wrongController = new LegacyExpertWorkflowController(wrongPreview);
        var search = await wrongController.SearchAsync(string.Empty);
        await Assert.ThrowsAsync<ExpertSelectionDataException>(
            () => wrongController.SelectExpertAsync(search.Search.Results[0].SelectionId));
        Assert.Equal(
            LegacyExpertRequestStatus.Error,
            wrongController.Current.Preview.Status);
    }

    [Fact]
    public async Task ExpertSnapshots_AreImmutableAcrossLaterQueries()
    {
        var service = new DelegateExpertService
        {
            Search = (query, _, _) => Task.FromResult(
                query == "A" ? SearchResult(query, Alpha) : SearchResult(query, Beta))
        };
        var controller = new LegacyExpertWorkflowController(service);
        var before = await controller.SearchAsync("A");

        var after = await controller.SearchAsync("B");

        Assert.Equal("강전문", Assert.Single(before.Search.Results).ExpertName);
        Assert.Equal("김전문", Assert.Single(after.Search.Results).ExpertName);
        var rows = Assert.IsAssignableFrom<ICollection<LegacyExpertResultView>>(
            before.Search.Results);
        Assert.Throws<NotSupportedException>(() => rows.Clear());
        var actions = Assert.IsAssignableFrom<ICollection<LegacyExpertActionView>>(
            before.Actions);
        Assert.Throws<NotSupportedException>(() => actions.Add(before.Actions[0]));
    }

    private static ExpertSearchResult SearchResult(
        string query,
        params ExpertSelectionIdentity[] identities) =>
        new(
            query,
            DateTimeOffset.Parse("2026-07-15T00:00:00+09:00"),
            identities.Select(identity =>
                new ExpertSelectorItem(identity, DataSourceKind.Oracle)).ToArray(),
            false);

    private static ExpertPreviewResult PreviewResult(
        ExpertSelectionIdentity identity,
        IReadOnlyList<ExpertRecommendationPreview> items,
        bool truncated = false) =>
        new(
            identity,
            DateTimeOffset.Parse("2026-07-15T00:00:00+09:00"),
            items,
            truncated);

    private static IReadOnlyList<ExpertRecommendationPreview> Recommendations(
        int count,
        string prefix = "S") =>
        Enumerable.Range(0, count)
            .Select(index => new ExpertRecommendationPreview(
                index,
                $"{prefix}{index:D4}",
                $"추천종목{prefix}{index:D4}",
                10_000m + index))
            .ToArray();

    private sealed class DelegateExpertService : IExpertSelectionService
    {
        public Func<string, int, CancellationToken, Task<ExpertSearchResult>> Search { get; init; } =
            (query, _, _) => Task.FromResult(SearchResult(query));

        public Func<ExpertSelectionIdentity, int, CancellationToken, Task<ExpertPreviewResult>>
            Preview
        { get; init; } =
            (identity, _, _) => Task.FromResult(PreviewResult(identity, []));

        public string? SearchQuery { get; private set; }

        public int SearchMaximum { get; private set; }

        public int PreviewMaximum { get; private set; }

        public Task<ExpertSearchResult> SearchAsync(
            string query = "",
            int maximumResults = LegacyExpertSelectionService.DefaultMaximumResults,
            CancellationToken cancellationToken = default)
        {
            SearchQuery = query;
            SearchMaximum = maximumResults;
            return Search(query, maximumResults, cancellationToken);
        }

        public Task<ExpertPreviewResult> PreviewAsync(
            ExpertSelectionIdentity identity,
            int maximumItems = LegacyExpertSelectionService.DefaultMaximumPreviewItems,
            CancellationToken cancellationToken = default)
        {
            PreviewMaximum = maximumItems;
            return Preview(identity, maximumItems, cancellationToken);
        }
    }
}
