using System.Globalization;
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;
using MMoneyCoderSharp.Data;

namespace MBN_STOCK_WEBVIEW.LegacyApplication.Tests;

public sealed class LegacyNamedPlaylistWorkflowTests
{
    [Fact]
    public async Task RefreshSelectAndLoad_ExposeOpaqueIdentityAndTypedRowsOnly()
    {
        var persistence = CreateSeededPersistence();
        var controller = new LegacyNamedPlaylistWorkflowController(persistence);

        var listed = await controller.RefreshAsync();
        var selected = controller.SelectDefinition(listed.Definitions.Single(
            definition => definition.Title == "마감").DefinitionId);
        var loaded = await controller.LoadSelectedAsync();

        Assert.Equal(LegacyNamedPlaylistReadStatus.Ready, listed.ReadStatus);
        Assert.Equal(LegacyNamedPlaylistFreshness.Fresh, listed.ListFreshness);
        Assert.All(listed.Definitions, definition =>
            Assert.DoesNotContain("000000", definition.DefinitionId, StringComparison.Ordinal));
        Assert.Equal("마감", selected.SelectedTitle);
        Assert.Equal("00000002", persistence.LastLoadedProgramCode);
        Assert.Collection(
            loaded.Rows,
            row =>
            {
                Assert.Equal(0, row.ItemIndex);
                Assert.True(row.IsEnabled);
                Assert.Equal("KOSDAQ", row.Selection.GroupCode);
                Assert.Equal("다온테크", row.Selection.Subject);
                Assert.Equal("1열판기본", row.Selection.GraphicType);
                Assert.Equal("현재가", row.Selection.Subtype);
                Assert.Equal("035720", row.Selection.DataCode);
                Assert.Equal("1/1", row.Page?.PageText);
            });
        Assert.DoesNotContain(
            typeof(LegacyNamedPlaylistDefinitionView).GetProperties(),
            property => property.Name.Contains("ProgramCode", StringComparison.Ordinal));
        Assert.DoesNotContain(
            typeof(LegacyNamedPlaylistWorkflowSnapshot).GetProperties(),
            property => property.Name.Contains("ProgramCode", StringComparison.Ordinal));
        Assert.DoesNotContain(
            typeof(LegacyNamedPlaylistRowView).GetProperties(),
            property => property.Name.Contains("ListText", StringComparison.Ordinal));
    }

    [Fact]
    public async Task SaveCurrent_SendsOneAtomicOrderedSevenFieldBatch_ThenReadsFresh()
    {
        var persistence = CreateSeededPersistence();
        var controller = await LoadOpeningAsync(persistence);
        var drafts = new[]
        {
            controller.CreateRowDraft(
                Entry("entry-a", true, "KOSPI", "삼성전자", "1열판기본", "현재가", "005930"),
                new NamedPlaylistPageState(1, 1)),
            controller.CreateRowDraft(
                Entry("entry-b", false, "PAGED_THEME", "반도체", "INPUT_ORDER", "CURRENT", "00000012"),
                new NamedPlaylistPageState(2, 3))
        };

        var result = await controller.SaveCurrentAsync(drafts);

        Assert.Equal(LegacyNamedPlaylistMutationOutcome.CommittedFresh, result.Outcome);
        Assert.True(result.IsCommitted);
        Assert.False(result.AutomaticRetryAllowed);
        Assert.False(result.Snapshot.IsWriteInProgress);
        Assert.Equal(1, persistence.ReplaceCalls);
        Assert.Collection(
            persistence.LastReplaceItems!,
            item =>
            {
                Assert.Equal(0, item.ItemIndex);
                Assert.Equal(
                    "1^KOSPI^삼성전자^1열판기본^현재가^1/1^005930",
                    SevenFields(item));
            },
            item =>
            {
                Assert.Equal(1, item.ItemIndex);
                Assert.Equal(
                    "0^PAGED_THEME^반도체^INPUT_ORDER^CURRENT^2/3^00000012",
                    SevenFields(item));
            });
        Assert.Equal(LegacyNamedPlaylistFreshness.Fresh, result.Snapshot.DocumentFreshness);
        Assert.Equal(["삼성전자", "반도체"], result.Snapshot.Rows.Select(row => row.Selection.Subject));
    }

    [Fact]
    public async Task CreateAndDelete_UseInternalSuggestedIdentityAndFreshListReadBack()
    {
        var persistence = CreateSeededPersistence();
        var controller = new LegacyNamedPlaylistWorkflowController(persistence);
        await controller.RefreshAsync();
        var draft = controller.CreateProgramDraft("새 프로그램");

        var created = await controller.CreateProgramAsync(draft);

        Assert.Equal(LegacyNamedPlaylistMutationOutcome.CommittedFresh, created.Outcome);
        Assert.Equal("00000003", persistence.LastCreatedProgramCode);
        Assert.Equal(3, created.Snapshot.Definitions.Count);
        Assert.Equal(
            created.Snapshot.Definitions[0].DefinitionId,
            created.Snapshot.SelectedDefinitionId);
        var firstTitleAfterCreate = created.Snapshot.Definitions[0].Title;
        Assert.Empty(created.Snapshot.Rows);
        Assert.DoesNotContain(
            typeof(LegacyNamedPlaylistProgramDraft).GetProperties(),
            property => property.Name.Contains("Code", StringComparison.Ordinal));
        Assert.DoesNotContain("00000003", draft.DraftId, StringComparison.Ordinal);

        var deleted = await controller.DeleteSelectedAsync();

        Assert.Equal(LegacyNamedPlaylistMutationOutcome.CommittedFresh, deleted.Outcome);
        Assert.Equal(1, persistence.DeleteCalls);
        Assert.NotEqual("00000003", persistence.LastDeletedProgramCode);
        Assert.Equal(
            deleted.Snapshot.Definitions[0].DefinitionId,
            deleted.Snapshot.SelectedDefinitionId);
        Assert.DoesNotContain(
            deleted.Snapshot.Definitions,
            definition => string.Equals(
                definition.Title,
                firstTitleAfterCreate,
                StringComparison.Ordinal));
        Assert.Contains(
            deleted.Snapshot.Definitions,
            definition => string.Equals(
                definition.Title,
                "새 프로그램",
                StringComparison.Ordinal));
        Assert.Equal(LegacyNamedPlaylistFreshness.Fresh, deleted.Snapshot.ListFreshness);
    }

    [Fact]
    public async Task EveryNonEmptyListRebind_SelectsFirstDefinitionLikeWinFormsPList()
    {
        var persistence = CreateSeededPersistence();
        var controller = new LegacyNamedPlaylistWorkflowController(persistence);

        var opened = await controller.RefreshAsync();

        Assert.Equal(opened.Definitions[0].DefinitionId, opened.SelectedDefinitionId);
        var second = opened.Definitions[1];
        controller.SelectDefinition(second.DefinitionId);

        var refreshed = await controller.RefreshAsync();

        Assert.Equal(refreshed.Definitions[0].DefinitionId, refreshed.SelectedDefinitionId);
        Assert.NotEqual(second.Title, refreshed.SelectedTitle);
        Assert.True(refreshed.CanLoad);
        Assert.True(refreshed.CanDelete);
    }

    [Fact]
    public async Task KnownRollback_PreservesLoadedRowsAndNeverRetries()
    {
        var persistence = CreateSeededPersistence();
        var controller = await LoadOpeningAsync(persistence);
        var before = controller.Current;
        var draft = controller.CreateRowDraft(
            Entry("replacement", true, "KOSPI", "교체", "1열판기본", "현재가", "123456"),
            new NamedPlaylistPageState(1, 1));
        persistence.ReplaceFailure = new NamedPlaylistMutationException(
            "The transaction rolled back.",
            outcomeUnknown: false);

        var result = await controller.SaveCurrentAsync([draft]);

        Assert.Equal(LegacyNamedPlaylistMutationOutcome.RolledBack, result.Outcome);
        Assert.False(result.AutomaticRetryAllowed);
        Assert.False(result.Snapshot.IsWriteQuarantined);
        Assert.True(result.Snapshot.CanMutate);
        Assert.Equal(1, persistence.ReplaceCalls);
        Assert.Equal(
            before.Rows.Select(row => row.Selection.Subject),
            result.Snapshot.Rows.Select(row => row.Selection.Subject));
        Assert.Equal("삼성전자", persistence.StoredSubjects("00000001").Single());
    }

    [Fact]
    public async Task OutcomeUnknown_LatchesQuarantineAndBlocksEveryLaterWriteWithoutRetry()
    {
        var persistence = CreateSeededPersistence();
        var controller = await LoadOpeningAsync(persistence);
        var draft = controller.CreateRowDraft(
            Entry("replacement", true, "KOSPI", "교체", "1열판기본", "현재가", "123456"),
            new NamedPlaylistPageState(1, 1));
        persistence.ReplaceFailure = new NamedPlaylistMutationException(
            "Commit response was lost.",
            outcomeUnknown: true);

        var unknown = await controller.SaveCurrentAsync([draft]);
        persistence.ReplaceFailure = null;
        var blocked = await controller.SaveCurrentAsync([draft]);

        Assert.Equal(LegacyNamedPlaylistMutationOutcome.OutcomeUnknown, unknown.Outcome);
        Assert.True(unknown.Snapshot.IsWriteQuarantined);
        Assert.False(unknown.Snapshot.CanMutate);
        Assert.False(unknown.AutomaticRetryAllowed);
        Assert.Equal(LegacyNamedPlaylistMutationOutcome.Quarantined, blocked.Outcome);
        Assert.False(blocked.AutomaticRetryAllowed);
        Assert.Equal(1, persistence.ReplaceCalls);
        Assert.Equal("삼성전자", persistence.StoredSubjects("00000001").Single());
    }

    [Fact]
    public async Task WriteGate_ReturnsBusyWhileFirstAtomicSaveIsPending()
    {
        var persistence = CreateSeededPersistence();
        var entered = new TaskCompletionSource(
            TaskCreationOptions.RunContinuationsAsynchronously);
        var release = new TaskCompletionSource(
            TaskCreationOptions.RunContinuationsAsynchronously);
        persistence.ReplaceEntered = entered;
        persistence.ReleaseReplace = release;
        var controller = await LoadOpeningAsync(persistence);
        var draft = controller.CreateRowDraft(
            Entry("replacement", true, "KOSPI", "교체", "1열판기본", "현재가", "123456"),
            new NamedPlaylistPageState(1, 1));

        var first = controller.SaveCurrentAsync([draft]);
        await entered.Task.WaitAsync(TimeSpan.FromSeconds(5));
        var concurrent = await controller.SaveCurrentAsync([draft]);
        release.SetResult();
        var completed = await first;

        Assert.Equal(LegacyNamedPlaylistMutationOutcome.Busy, concurrent.Outcome);
        Assert.False(concurrent.AutomaticRetryAllowed);
        Assert.Equal(LegacyNamedPlaylistMutationOutcome.CommittedFresh, completed.Outcome);
        Assert.Equal(1, persistence.ReplaceCalls);
    }

    [Fact]
    public async Task FailedFreshReadBack_KeepsOptimisticRowsUntilExplicitFreshLoad()
    {
        var persistence = CreateSeededPersistence();
        var controller = await LoadOpeningAsync(persistence);
        var draft = controller.CreateRowDraft(
            Entry("replacement", true, "KOSPI", "낙관행", "1열판기본", "현재가", "654321"),
            new NamedPlaylistPageState(1, 1));
        persistence.LoadFailures.Enqueue(
            new NamedPlaylistDataException("Simulated read-back failure."));

        var optimistic = await controller.SaveCurrentAsync([draft]);

        Assert.Equal(
            LegacyNamedPlaylistMutationOutcome.CommittedOptimistic,
            optimistic.Outcome);
        Assert.Equal(
            LegacyNamedPlaylistFreshness.Optimistic,
            optimistic.Snapshot.DocumentFreshness);
        Assert.Equal("낙관행", Assert.Single(optimistic.Snapshot.Rows).Selection.Subject);
        Assert.False(optimistic.Snapshot.IsWriteQuarantined);
        Assert.Equal(1, persistence.ReplaceCalls);

        var fresh = await controller.LoadSelectedAsync();

        Assert.Equal(LegacyNamedPlaylistFreshness.Fresh, fresh.DocumentFreshness);
        Assert.Equal("낙관행", Assert.Single(fresh.Rows).Selection.Subject);
        Assert.Equal(1, persistence.ReplaceCalls);
    }

    [Fact]
    public async Task ReadOnlyPersistence_RejectsMutationBeforeServiceCall()
    {
        var persistence = CreateSeededPersistence();
        persistence.CanMutate = false;
        var controller = await LoadOpeningAsync(persistence);
        var draft = controller.CreateRowDraft(
            Entry("replacement", true, "KOSPI", "교체", "1열판기본", "현재가", "123456"),
            new NamedPlaylistPageState(1, 1));

        var result = await controller.SaveCurrentAsync([draft]);

        Assert.Equal(LegacyNamedPlaylistMutationOutcome.ReadOnly, result.Outcome);
        Assert.False(result.Snapshot.CanMutate);
        Assert.Equal(0, persistence.ReplaceCalls);
    }

    [Fact]
    public async Task SnapshotsAreImmutable_AndStaleOpaqueIdsAreRejectedAfterRefresh()
    {
        var persistence = CreateSeededPersistence();
        var controller = new LegacyNamedPlaylistWorkflowController(persistence);
        var before = await controller.RefreshAsync();
        var staleId = before.Definitions[0].DefinitionId;
        controller.SelectDefinition(staleId);
        var loaded = await controller.LoadSelectedAsync();

        var after = await controller.RefreshAsync();

        var definitions = Assert.IsAssignableFrom<
            ICollection<LegacyNamedPlaylistDefinitionView>>(before.Definitions);
        Assert.Throws<NotSupportedException>(() => definitions.Clear());
        var rows = Assert.IsAssignableFrom<ICollection<LegacyNamedPlaylistRowView>>(loaded.Rows);
        Assert.Throws<NotSupportedException>(() => rows.Clear());
        Assert.NotEqual(staleId, after.SelectedDefinitionId);
        Assert.Throws<KeyNotFoundException>(() => controller.SelectDefinition(staleId));
        Assert.Equal("마감", before.Definitions[0].Title);
    }

    [Fact]
    public async Task DraftsAreBoundToTheirNativeAuthorityInstance()
    {
        var firstPersistence = CreateSeededPersistence();
        var secondPersistence = CreateSeededPersistence();
        var first = await LoadOpeningAsync(firstPersistence);
        var second = await LoadOpeningAsync(secondPersistence);
        var foreignDraft = first.CreateRowDraft(
            Entry("foreign", true, "KOSPI", "외부", "1열판기본", "현재가", "111111"),
            new NamedPlaylistPageState(1, 1));

        await Assert.ThrowsAsync<ArgumentException>(
            () => second.SaveCurrentAsync([foreignDraft]));

        Assert.Equal(0, secondPersistence.ReplaceCalls);
    }

    private static FakeNamedPlaylistPersistence CreateSeededPersistence()
    {
        var persistence = new FakeNamedPlaylistPersistence();
        persistence.Seed(
            "00000001",
            "오전",
            StoredItem(0, true, "KOSPI", "삼성전자", "1열판기본", "현재가", "005930", 1, 1));
        persistence.Seed(
            "00000002",
            "마감",
            StoredItem(0, true, "KOSDAQ", "다온테크", "1열판기본", "현재가", "035720", 1, 1));
        return persistence;
    }

    private static async Task<LegacyNamedPlaylistWorkflowController> LoadOpeningAsync(
        FakeNamedPlaylistPersistence persistence)
    {
        var controller = new LegacyNamedPlaylistWorkflowController(persistence);
        var listed = await controller.RefreshAsync();
        controller.SelectDefinition(listed.Definitions.Single(
            definition => definition.Title == "오전").DefinitionId);
        await controller.LoadSelectedAsync();
        return controller;
    }

    private static LegacyPlaylistEntry Entry(
        string id,
        bool enabled,
        string groupCode,
        string subject,
        string graphicType,
        string subtype,
        string dataCode) =>
        new(
            id,
            "5001",
            enabled,
            6,
            new LegacySceneSelection(
                groupCode,
                subject,
                graphicType,
                subtype,
                dataCode));

    private static NamedPlaylistStoredItem StoredItem(
        int index,
        bool enabled,
        string groupCode,
        string subject,
        string graphicType,
        string subtype,
        string dataCode,
        int currentPage,
        int totalPages) =>
        new(
            index,
            enabled,
            new LegacySceneSelection(
                groupCode,
                subject,
                graphicType,
                subtype,
                dataCode),
            new NamedPlaylistPageState(currentPage, totalPages));

    private static string SevenFields(NamedPlaylistStoredItem item) =>
        string.Join(
            '^',
            item.IsEnabled ? "1" : "0",
            item.Selection.GroupCode,
            item.Selection.Subject,
            item.Selection.GraphicType,
            item.Selection.Subtype,
            item.Page?.ToString() ?? string.Empty,
            item.Selection.DataCode);

    private sealed class FakeNamedPlaylistPersistence : INamedPlaylistPersistenceService
    {
        private readonly Dictionary<string, StoredProgram> _programs =
            new(StringComparer.Ordinal);

        public bool CanMutate { get; set; } = true;

        public int CreateCalls { get; private set; }

        public int ReplaceCalls { get; private set; }

        public int DeleteCalls { get; private set; }

        public int LoadCalls { get; private set; }

        public string? LastCreatedProgramCode { get; private set; }

        public string? LastLoadedProgramCode { get; private set; }

        public string? LastDeletedProgramCode { get; private set; }

        public IReadOnlyList<NamedPlaylistStoredItem>? LastReplaceItems { get; private set; }

        public Exception? CreateFailure { get; set; }

        public Exception? ReplaceFailure { get; set; }

        public Exception? DeleteFailure { get; set; }

        public Queue<Exception> LoadFailures { get; } = new();

        public TaskCompletionSource? ReplaceEntered { get; set; }

        public TaskCompletionSource? ReleaseReplace { get; set; }

        public void Seed(
            string programCode,
            string title,
            params NamedPlaylistStoredItem[] items) =>
            _programs.Add(
                programCode,
                new StoredProgram(title, items.Select(Clone).ToList()));

        public IEnumerable<string> StoredSubjects(string programCode) =>
            _programs[programCode].Items.Select(item => item.Selection.Subject);

        public Task<NamedPlaylistListResult> ListAsync(
            int maximumResults = LegacyNamedPlaylistPersistenceService.DefaultMaximumDefinitions,
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            var ordered = _programs
                .Select(pair => new NamedPlaylistSummary(pair.Key, pair.Value.Title))
                .OrderBy(summary => summary.Title, StringComparer.Ordinal)
                .ThenBy(summary => summary.ProgramCode, StringComparer.Ordinal)
                .ToArray();
            return Task.FromResult(new NamedPlaylistListResult(
                DateTimeOffset.Parse("2026-07-15T00:00:00+09:00"),
                ordered.Take(maximumResults).ToArray(),
                ordered.Length > maximumResults));
        }

        public Task<string> SuggestNextProgramCodeAsync(
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            var next = _programs.Count == 0
                ? 1
                : _programs.Keys.Max(code => int.Parse(
                    code,
                    NumberStyles.None,
                    CultureInfo.InvariantCulture)) + 1;
            return Task.FromResult(next.ToString("D8", CultureInfo.InvariantCulture));
        }

        public Task<NamedPlaylistDocument> LoadAsync(
            string programCode,
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            ++LoadCalls;
            LastLoadedProgramCode = programCode;
            if (LoadFailures.TryDequeue(out var failure))
            {
                return Task.FromException<NamedPlaylistDocument>(failure);
            }

            if (!_programs.TryGetValue(programCode, out var stored))
            {
                return Task.FromException<NamedPlaylistDocument>(
                    new NamedPlaylistNotFoundException(programCode));
            }

            return Task.FromResult(new NamedPlaylistDocument(
                new NamedPlaylistSummary(programCode, stored.Title),
                DateTimeOffset.Parse("2026-07-15T00:00:00+09:00"),
                stored.Items.Select(Clone).ToArray()));
        }

        public Task CreateDefinitionAsync(
            string programCode,
            string title,
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            ++CreateCalls;
            LastCreatedProgramCode = programCode;
            if (CreateFailure is not null)
            {
                return Task.FromException(CreateFailure);
            }

            if (!_programs.TryAdd(programCode, new StoredProgram(title, [])))
            {
                return Task.FromException(new NamedPlaylistMutationException(
                    "Duplicate program; rolled back.",
                    outcomeUnknown: false));
            }

            return Task.CompletedTask;
        }

        public async Task ReplaceItemsAsync(
            string programCode,
            IReadOnlyList<NamedPlaylistStoredItem> items,
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            ++ReplaceCalls;
            LastReplaceItems = Array.AsReadOnly(items.Select(Clone).ToArray());
            ReplaceEntered?.TrySetResult();
            if (ReleaseReplace is not null)
            {
                await ReleaseReplace.Task.WaitAsync(cancellationToken);
            }

            if (ReplaceFailure is not null)
            {
                throw ReplaceFailure;
            }

            if (!_programs.TryGetValue(programCode, out var stored))
            {
                throw new NamedPlaylistNotFoundException(programCode);
            }

            // One assignment models the executor's transaction boundary: no partial row
            // becomes visible when an exception is raised before this point.
            _programs[programCode] = stored with
            {
                Items = items.Select(Clone).ToList()
            };
        }

        public Task DeleteAsync(
            string programCode,
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            ++DeleteCalls;
            LastDeletedProgramCode = programCode;
            if (DeleteFailure is not null)
            {
                return Task.FromException(DeleteFailure);
            }

            if (!_programs.Remove(programCode))
            {
                return Task.FromException(new NamedPlaylistNotFoundException(programCode));
            }

            return Task.CompletedTask;
        }

        private static NamedPlaylistStoredItem Clone(NamedPlaylistStoredItem item) =>
            new(
                item.ItemIndex,
                item.IsEnabled,
                item.Selection with { },
                item.Page is null
                    ? null
                    : new NamedPlaylistPageState(
                        item.Page.CurrentPage,
                        item.Page.TotalPages));

        private sealed record StoredProgram(
            string Title,
            List<NamedPlaylistStoredItem> Items);
    }
}
