using MBN_STOCK_WEBVIEW.LegacyApplication;
using MMoneyCoderSharp.Data;

namespace MBN_STOCK_WEBVIEW.LegacyApplication.Tests;

public sealed class LegacyIndustrySelectionWorkflowTests
{
    [Fact]
    public async Task LoadMarketUsesServiceOrderAndCopiesRowsIntoImmutableSnapshot()
    {
        var source = KospiRows().ToList();
        var service = new QueueIndustryService(source);
        var workflow = new LegacyIndustrySelectionWorkflow(service);

        var state = await workflow.LoadMarketAsync(IndustryMarket.Kospi);
        source.Clear();

        Assert.Equal([IndustryMarket.Kospi], service.Markets);
        Assert.Equal(3, state.Industries.Count);
        Assert.Equal("전기전자", state.Industries[0].IndustryName);
        Assert.Equal(22, state.Actions.Count);
        Assert.Null(state.SelectedIndustry);
        Assert.Null(state.FirstIndustry);
        Assert.Null(state.SecondIndustry);
        Assert.Equal(string.Empty, state.FirstText);
        Assert.Equal(string.Empty, state.SecondText);

        var rows = Assert.IsAssignableFrom<IList<IndustrySelection>>(state.Industries);
        Assert.Throws<NotSupportedException>(() => rows.RemoveAt(0));
        var actions = Assert.IsAssignableFrom<IList<LegacyIndustryActionDefinition>>(state.Actions);
        Assert.Throws<NotSupportedException>(() => actions.RemoveAt(0));
    }

    [Fact]
    public async Task LoadMarketPreservesDuplicateRowsAndSelectionUsesTheClickedIndex()
    {
        var workflow = new LegacyIndustrySelectionWorkflow(new QueueIndustryService(
        [
            new IndustrySelection(IndustryMarket.Kospi, "동명이 업종", "013"),
            new IndustrySelection(IndustryMarket.Kospi, "동명이 업종", "014"),
            new IndustrySelection(IndustryMarket.Kospi, "다른 업종", "013")
        ]));

        var loaded = await workflow.LoadMarketAsync(IndustryMarket.Kospi);
        var selected = workflow.SelectIndustry(1);

        Assert.Equal(3, loaded.Industries.Count);
        Assert.Equal(["013", "014", "013"],
            loaded.Industries.Select(static row => row.IndustryCode));
        Assert.Equal(1, selected.SelectedIndex);
        Assert.Equal("014", selected.SelectedIndustry?.IndustryCode);
    }

    [Fact]
    public async Task ListDoubleClickRotatesFirstIntoSecondBeforeAssigningNewFirst()
    {
        var workflow = await KospiWorkflowAsync();

        var selectedOnly = workflow.SelectIndustry(1);
        Assert.Equal("운수장비", selectedOnly.SelectedIndustry?.IndustryName);
        Assert.Null(selectedOnly.FirstIndustry);

        var first = workflow.DoubleClickIndustry(0);
        Assert.Equal("전기전자", first.SelectedIndustry?.IndustryName);
        Assert.Equal("전기전자", first.FirstIndustry?.IndustryName);
        Assert.Null(first.SecondIndustry);

        var second = workflow.DoubleClickIndustry(2);
        Assert.Equal("화학", second.SelectedIndustry?.IndustryName);
        Assert.Equal("화학", second.FirstIndustry?.IndustryName);
        Assert.Equal("전기전자", second.SecondIndustry?.IndustryName);

        var third = workflow.DoubleClickIndustry(1);
        Assert.Equal("운수장비", third.FirstIndustry?.IndustryName);
        Assert.Equal("화학", third.SecondIndustry?.IndustryName);
    }

    [Fact]
    public async Task SwapMatchesUc2EvenWhenOnlyOneTextSlotHasAValue()
    {
        var workflow = await KospiWorkflowAsync();
        workflow.DoubleClickIndustry(0);

        var swapped = workflow.SwapIndustries();
        Assert.Null(swapped.FirstIndustry);
        Assert.Equal("전기전자", swapped.SecondIndustry?.IndustryName);
        Assert.Equal(string.Empty, swapped.FirstText);
        Assert.Equal("전기전자", swapped.SecondText);

        var restored = workflow.SwapIndustries();
        Assert.Equal("전기전자", restored.FirstIndustry?.IndustryName);
        Assert.Null(restored.SecondIndustry);
        Assert.Equal("전기전자", restored.FirstText);
        Assert.Equal(string.Empty, restored.SecondText);
    }

    [Fact]
    public async Task EditableComparisonTextRemainsCSharpAuthoritativeAndAffectsOnlyPairAction()
    {
        var workflow = await KospiWorkflowAsync();
        workflow.SelectIndustry(0);

        var first = workflow.SetComparisonText(
            LegacyIndustryComparisonSlot.First,
            "직접 입력 업종");
        var second = workflow.SetComparisonText(
            LegacyIndustryComparisonSlot.Second,
            "또 다른 업종");

        Assert.Null(first.FirstIndustry);
        Assert.Equal("직접 입력 업종", second.FirstText);
        Assert.Equal("또 다른 업종", second.SecondText);
        Assert.Equal("직접 입력 업종-또 다른 업종",
            workflow.MaterializePlaylistDraft("two-column").Selection.Subject);
        Assert.Equal("전기전자",
            workflow.MaterializePlaylistDraft("one-column").Selection.Subject);

        var swapped = workflow.SwapIndustries();
        Assert.Equal("또 다른 업종", swapped.FirstText);
        Assert.Equal("직접 입력 업종", swapped.SecondText);

        workflow.SetComparisonText(LegacyIndustryComparisonSlot.First, "수동 보존값");
        var rotated = workflow.DoubleClickIndustry(2);
        Assert.Equal("화학", rotated.FirstText);
        Assert.Equal("수동 보존값", rotated.SecondText);
        Assert.Equal("화학", rotated.FirstIndustry?.IndustryName);
        Assert.Null(rotated.SecondIndustry);
    }

    [Fact]
    public async Task EditableComparisonTextValidatesAtBridgeCompatibleBoundary()
    {
        var workflow = await KospiWorkflowAsync();
        workflow.SelectIndustry(0);

        var maximum = new string('가', LegacyIndustrySelectionWorkflow.MaximumComparisonTextLength);
        Assert.Equal(maximum, workflow.SetComparisonText(
            LegacyIndustryComparisonSlot.First,
            maximum).FirstText);
        Assert.Throws<LegacyIndustryWorkflowException>(() => workflow.SetComparisonText(
            LegacyIndustryComparisonSlot.First,
            maximum + "가"));
        Assert.Throws<LegacyIndustryWorkflowException>(() => workflow.SetComparisonText(
            LegacyIndustryComparisonSlot.First,
            "업종\n이름"));
        Assert.Throws<LegacyIndustryWorkflowException>(() => workflow.SetComparisonText(
            LegacyIndustryComparisonSlot.First,
            "업종\ud83d\ude00"));

        workflow.SetComparisonText(LegacyIndustryComparisonSlot.First, "공백 아님");
        workflow.SetComparisonText(LegacyIndustryComparisonSlot.Second, "   ");
        Assert.Throws<LegacyIndustryWorkflowException>(() =>
            workflow.MaterializePlaylistDraft("two-column"));
        workflow.SetComparisonText(LegacyIndustryComparisonSlot.Second, "하이-픈");
        Assert.Throws<LegacyIndustryWorkflowException>(() =>
            workflow.MaterializePlaylistDraft("two-column"));
    }

    [Fact]
    public async Task SameMarketRefreshReconcilesIdentityButMarketChangeClearsSelection()
    {
        var initial = KospiRows();
        var refreshed = new[] { initial[1], initial[0], initial[2] };
        var kosdaq = KosdaqRows();
        var service = new QueueIndustryService(initial, refreshed, kosdaq);
        var workflow = new LegacyIndustrySelectionWorkflow(service);

        await workflow.LoadMarketAsync(IndustryMarket.Kospi);
        workflow.DoubleClickIndustry(0);
        workflow.DoubleClickIndustry(1);

        var reconciled = await workflow.LoadMarketAsync(IndustryMarket.Kospi);
        Assert.Equal(0, reconciled.SelectedIndex);
        Assert.Equal("운수장비", reconciled.SelectedIndustry?.IndustryName);
        Assert.Equal("운수장비", reconciled.FirstIndustry?.IndustryName);
        Assert.Equal("전기전자", reconciled.SecondIndustry?.IndustryName);
        Assert.Equal("운수장비", reconciled.FirstText);
        Assert.Equal("전기전자", reconciled.SecondText);

        var changed = await workflow.LoadMarketAsync(IndustryMarket.Kosdaq);
        Assert.Equal(IndustryMarket.Kosdaq, changed.Market);
        Assert.Null(changed.SelectedIndex);
        Assert.Null(changed.SelectedIndustry);
        Assert.Null(changed.FirstIndustry);
        Assert.Null(changed.SecondIndustry);
        Assert.Equal(string.Empty, changed.FirstText);
        Assert.Equal(string.Empty, changed.SecondText);
    }

    [Fact]
    public async Task InvalidIndexAndEmptyClearAreNoOps()
    {
        var workflow = await KospiWorkflowAsync();
        var before = workflow.Current;

        Assert.Equal(before.Revision, workflow.SelectIndustry(-1).Revision);
        Assert.Equal(before.Revision, workflow.DoubleClickIndustry(3).Revision);
        Assert.Equal(before.Revision, workflow.ClearSelections().Revision);
        Assert.Equal(before.Revision, workflow.SwapIndustries().Revision);
    }

    [Fact]
    public async Task RepresentativeActionsMaterializeOnlyFromCatalogAndAuthoritativeState()
    {
        var workflow = await KospiWorkflowAsync(new FixedTimeProvider(
            new DateTimeOffset(2026, 7, 11, 10, 0, 0, TimeSpan.Zero)));
        workflow.DoubleClickIndustry(0);
        workflow.DoubleClickIndustry(1);

        var single = workflow.MaterializePlaylistDraft("one-column");
        Assert.Equal("s5001", single.BuilderKey);
        Assert.Equal("5001", single.SceneAlias);
        Assert.Equal("운수장비", single.Title);
        Assert.Equal(
            new LegacyIndustrySceneSelectionDraft(
                "INDUSTRY_KOSPI", "운수장비", "1열판", "CURRENT", string.Empty),
            single.Selection);
        Assert.True(single.IsEnabled);
        Assert.Equal(6, single.FadeDuration);

        var pair = workflow.MaterializePlaylistDraft("two-column");
        Assert.Equal("s8018", pair.BuilderKey);
        Assert.Equal("8018", pair.SceneAlias);
        Assert.Equal("운수장비-전기전자", pair.Selection.Subject);
        Assert.Equal("2열판", pair.Selection.GraphicType);

        var yield = workflow.MaterializePlaylistDraft("yield-120일");
        Assert.Equal("s5086", yield.BuilderKey);
        Assert.Equal("5086", yield.SceneAlias);
        Assert.Equal("OneHundredTwentyDays", yield.Selection.Subtype);

        var candle = workflow.MaterializePlaylistDraft("candle-volume-240일");
        Assert.Equal("s8010", candle.BuilderKey);
        Assert.Equal("8056", candle.SceneAlias);
        Assert.Equal("KOSPI_INDUSTRY", candle.Selection.GroupCode);
        Assert.Equal("VOLUME", candle.Selection.Subtype);

        var five = workflow.MaterializePlaylistDraft("five-row");
        Assert.Equal("s5074", five.BuilderKey);
        Assert.Equal("5074", five.SceneAlias);
        Assert.Equal(5, five.PageSize);
        Assert.Equal("PAGED_DOMESTIC_KOSPI", five.Selection.GroupCode);
        Assert.Equal("INDEX", five.Selection.Subject);
        Assert.Equal("INDUSTRY", five.Selection.Subtype);
        Assert.Equal("2026-07-11", five.Selection.DataCode);
        Assert.Equal(
            "F21DE58003315EAB41F9FE7B5A573C71653056203E6743D05AA96E7FF1B8BF52",
            five.RuntimeAssetSha256);

        var square = workflow.MaterializePlaylistDraft("square");
        Assert.Equal("8001", square.SceneAlias);
        Assert.Equal("KOSPI", square.Selection.Subject);
        Assert.Equal("네모그래프", square.Selection.GraphicType);

        var sector = workflow.MaterializePlaylistDraft("sector");
        Assert.Equal("5078", sector.SceneAlias);
        Assert.Equal("SECTOR_INDEX", sector.Selection.GraphicType);
    }

    [Fact]
    public async Task EveryKosdaqActionMaterializesWithItsExactAliasAndPageSize()
    {
        var service = new QueueIndustryService(KosdaqRows());
        var workflow = new LegacyIndustrySelectionWorkflow(
            service,
            new FixedTimeProvider(new DateTimeOffset(2026, 7, 12, 0, 0, 0, TimeSpan.Zero)));
        await workflow.LoadMarketAsync(IndustryMarket.Kosdaq);
        workflow.DoubleClickIndustry(0);
        workflow.DoubleClickIndustry(1);

        foreach (var action in LegacyIndustryActionCatalog.GetActions(IndustryMarket.Kosdaq))
        {
            var draft = workflow.MaterializePlaylistDraft(action.ActionId);
            Assert.Equal(action.ActionId, draft.ActionId);
            Assert.Equal(action.BuilderKey, draft.BuilderKey);
            Assert.Equal(action.SceneAlias, draft.SceneAlias);
            Assert.Equal(action.PageSize, draft.PageSize);
            Assert.Equal(IndustryMarket.Kosdaq, draft.Market);
        }

        Assert.Equal("8032", workflow.MaterializePlaylistDraft("two-column").SceneAlias);
        Assert.Equal("8002", workflow.MaterializePlaylistDraft("square").SceneAlias);
        Assert.Equal(
            "PAGED_DOMESTIC_KOSDAQ",
            workflow.MaterializePlaylistDraft("five-row").Selection.GroupCode);
    }

    [Fact]
    public async Task SelectionPrerequisitesUnsafePairAndUnknownActionFailClosed()
    {
        var workflow = await KospiWorkflowAsync();

        Assert.Throws<LegacyIndustryWorkflowException>(() =>
            workflow.MaterializePlaylistDraft("one-column"));
        Assert.Throws<LegacyIndustryWorkflowException>(() =>
            workflow.MaterializePlaylistDraft("two-column"));
        Assert.NotNull(workflow.MaterializePlaylistDraft("five-row"));
        Assert.Throws<LegacyIndustryWorkflowException>(() =>
            workflow.MaterializePlaylistDraft("../../5001"));
        Assert.Throws<ArgumentNullException>(() =>
            workflow.MaterializePlaylistDraft(null!));

        var unsafePair = new LegacyIndustrySelectionWorkflow(new QueueIndustryService(
        [
            new IndustrySelection(IndustryMarket.Kospi, "전기-전자", "013"),
            new IndustrySelection(IndustryMarket.Kospi, "운수장비", "015")
        ]));
        await unsafePair.LoadMarketAsync(IndustryMarket.Kospi);
        unsafePair.DoubleClickIndustry(0);
        unsafePair.DoubleClickIndustry(1);
        Assert.Throws<LegacyIndustryWorkflowException>(() =>
            unsafePair.MaterializePlaylistDraft("two-column"));
    }

    [Fact]
    public async Task InvalidServiceRowsFailAtomicallyBeforeStateChanges()
    {
        var service = new QueueIndustryService(
        [
            new IndustrySelection(IndustryMarket.Kosdaq, "반도체", "027")
        ]);
        var workflow = new LegacyIndustrySelectionWorkflow(service);

        await Assert.ThrowsAsync<LegacyIndustryWorkflowException>(() =>
            workflow.LoadMarketAsync(IndustryMarket.Kospi));

        Assert.Equal(0, workflow.Current.Revision);
        Assert.Null(workflow.Current.Market);
        Assert.Empty(workflow.Current.Industries);
    }

    [Fact]
    public async Task SlowerPreviousMarketResponseCannotReplaceOrFaultNewerMarketState()
    {
        var service = new ControlledIndustryService();
        var workflow = new LegacyIndustrySelectionWorkflow(service);

        var older = workflow.LoadMarketAsync(IndustryMarket.Kospi);
        var newer = workflow.LoadMarketAsync(IndustryMarket.Kosdaq);
        service.Kosdaq.SetResult(KosdaqRows());
        var newerState = await newer;

        // This row is invalid for the older KOSPI request. Because that request is
        // already stale, it must be ignored before it can disturb current state.
        service.Kospi.SetResult(KosdaqRows());
        var olderResult = await older;

        Assert.Equal(IndustryMarket.Kosdaq, newerState.Market);
        Assert.Equal(IndustryMarket.Kosdaq, olderResult.Market);
        Assert.Equal(IndustryMarket.Kosdaq, workflow.Current.Market);
        Assert.Equal("반도체", workflow.Current.Industries[0].IndustryName);
        Assert.Equal(1, workflow.Current.Revision);
    }

    [Fact]
    public async Task ClearSelectionsClearsHighlightedAndBothPairSlots()
    {
        var workflow = await KospiWorkflowAsync();
        workflow.DoubleClickIndustry(0);
        workflow.DoubleClickIndustry(1);

        var cleared = workflow.ClearSelections();

        Assert.Null(cleared.SelectedIndex);
        Assert.Null(cleared.SelectedIndustry);
        Assert.Null(cleared.FirstIndustry);
        Assert.Null(cleared.SecondIndustry);
    }

    private static async Task<LegacyIndustrySelectionWorkflow> KospiWorkflowAsync(
        TimeProvider? timeProvider = null)
    {
        var workflow = new LegacyIndustrySelectionWorkflow(
            new QueueIndustryService(KospiRows()),
            timeProvider);
        await workflow.LoadMarketAsync(IndustryMarket.Kospi);
        return workflow;
    }

    private static IndustrySelection[] KospiRows() =>
    [
        new(IndustryMarket.Kospi, "전기전자", "013"),
        new(IndustryMarket.Kospi, "운수장비", "015"),
        new(IndustryMarket.Kospi, "화학", "008")
    ];

    private static IndustrySelection[] KosdaqRows() =>
    [
        new(IndustryMarket.Kosdaq, "반도체", "027"),
        new(IndustryMarket.Kosdaq, "IT부품", "029")
    ];

    private sealed class QueueIndustryService(
        params IReadOnlyList<IndustrySelection>[] results) : IIndustrySelectionService
    {
        private readonly Queue<IReadOnlyList<IndustrySelection>> _results = new(results);

        public List<IndustryMarket> Markets { get; } = [];

        public Task<IReadOnlyList<IndustrySelection>> GetAsync(
            IndustryMarket market,
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            Markets.Add(market);
            if (_results.Count == 0)
            {
                throw new InvalidOperationException("No fake industry result remains.");
            }

            return Task.FromResult(_results.Dequeue());
        }
    }

    private sealed class FixedTimeProvider(DateTimeOffset utcNow) : TimeProvider
    {
        public override TimeZoneInfo LocalTimeZone => TimeZoneInfo.Utc;

        public override DateTimeOffset GetUtcNow() => utcNow;
    }

    private sealed class ControlledIndustryService : IIndustrySelectionService
    {
        public TaskCompletionSource<IReadOnlyList<IndustrySelection>> Kospi { get; } =
            new(TaskCreationOptions.RunContinuationsAsynchronously);

        public TaskCompletionSource<IReadOnlyList<IndustrySelection>> Kosdaq { get; } =
            new(TaskCreationOptions.RunContinuationsAsynchronously);

        public Task<IReadOnlyList<IndustrySelection>> GetAsync(
            IndustryMarket market,
            CancellationToken cancellationToken = default) => market switch
        {
            IndustryMarket.Kospi => Kospi.Task.WaitAsync(cancellationToken),
            IndustryMarket.Kosdaq => Kosdaq.Task.WaitAsync(cancellationToken),
            _ => throw new ArgumentOutOfRangeException(nameof(market), market, null)
        };
    }
}
