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

namespace MBN_STOCK_WEBVIEW.LegacyApplication.Tests;

public sealed class LegacyManualFinancialOperatorControllerTests
{
    [Fact]
    public async Task GraphE_open_warns_when_invalid_legacy_rows_were_isolated()
    {
        var service = new FakeManualService(Sales("Alpha", 1))
        {
            RejectedItemCount = 2
        };

        var opened = await Controller(service).OpenManualFinancialAsync(
            ManualFinancialScreenKind.Sales);

        Assert.Single(opened.ManualFinancial!.Rows);
        Assert.Equal(2, opened.ManualFinancial.ListRejectedItemCount);
        Assert.Equal(LegacyOperatorStatusKind.Warning, opened.StatusKind);
        Assert.Contains("2행", opened.StatusMessage, StringComparison.Ordinal);
    }

    [Fact]
    public async Task Strict_ready_GraphE_load_clears_previous_raw_row_warning()
    {
        var service = new LegacyManualFinancialScreenService(
            new RawGraphEQueryExecutor());
        var controller = Controller(service);
        var opened = await controller.OpenManualFinancialAsync(
            ManualFinancialScreenKind.Sales);
        var rawRow = opened.ManualFinancial!.Rows.Single(row => !row.IsPlayoutReady);
        var readyRow = opened.ManualFinancial.Rows.Single(row => row.IsPlayoutReady);

        var rawLoaded = await controller.LoadManualFinancialAsync(rawRow.ResultId);

        Assert.False(rawLoaded.ManualFinancial!.IsPlayoutReady);
        Assert.Equal(LegacyOperatorStatusKind.Warning, rawLoaded.StatusKind);
        Assert.Contains("원문", rawLoaded.StatusMessage, StringComparison.Ordinal);

        var readyLoaded = await controller.LoadManualFinancialAsync(readyRow.ResultId);

        Assert.True(readyLoaded.ManualFinancial!.IsPlayoutReady);
        Assert.Equal(LegacyOperatorStatusKind.Information, readyLoaded.StatusKind);
        Assert.Contains("Alpha", readyLoaded.StatusMessage, StringComparison.Ordinal);
        Assert.DoesNotContain("원문", readyLoaded.StatusMessage, StringComparison.Ordinal);
    }

    [Fact]
    public async Task Unreadable_row_failure_clears_previous_detail_and_playout_authority()
    {
        var service = new FakeManualService(Sales("Alpha", 1))
        {
            RejectedItemCount = 1,
            UnreadableStockNames = ["Broken"],
            GetOverride = identity => identity.StockName == "Broken"
                ? throw new ManualFinancialDataException("Unreadable legacy row.")
                : Sales("Alpha", 1)
        };
        var controller = Controller(service);
        var opened = await controller.OpenManualFinancialAsync(
            ManualFinancialScreenKind.Sales);
        var valid = opened.ManualFinancial!.Rows.Single(row => row.IsDataReadable);
        var unreadable = opened.ManualFinancial.Rows.Single(row => !row.IsDataReadable);
        var loaded = await controller.LoadManualFinancialAsync(valid.ResultId);
        Assert.True(loaded.ManualFinancial!.CanMaterializePlaylist);

        var rejected = await controller.LoadManualFinancialAsync(unreadable.ResultId);

        var failed = rejected.ManualFinancial!;
        Assert.Equal(unreadable.ResultId, failed.SelectedRowId);
        Assert.Null(failed.SelectedRecord);
        Assert.False(failed.CanSave);
        Assert.False(failed.CanDelete);
        Assert.False(failed.CanMaterializePlaylist);
        Assert.NotNull(rejected.Dialog);

        var activated = await controller.ActivateManualFinancialResultAsync(
            unreadable.ResultId);
        Assert.Empty(activated.Playlist);
        Assert.False(activated.CommandResult?.Succeeded);
        Assert.Equal(unreadable.ResultId, activated.CommandResult?.TargetId);
    }

    [Fact]
    public async Task Stale_result_handle_clears_previous_detail_before_it_is_rejected()
    {
        var service = new FakeManualService(Sales("Alpha", 1));
        var controller = Controller(service);
        var opened = await controller.OpenManualFinancialAsync(
            ManualFinancialScreenKind.Sales);
        var valid = Assert.Single(opened.ManualFinancial!.Rows);
        var loaded = await controller.LoadManualFinancialAsync(valid.ResultId);
        Assert.True(loaded.ManualFinancial!.CanMaterializePlaylist);

        var rejected = await controller.LoadManualFinancialAsync("stale-result-id");

        Assert.Null(rejected.ManualFinancial!.SelectedRowId);
        Assert.Null(rejected.ManualFinancial.SelectedRecord);
        Assert.False(rejected.ManualFinancial.CanSave);
        Assert.False(rejected.ManualFinancial.CanDelete);
        Assert.False(rejected.ManualFinancial.CanMaterializePlaylist);
        Assert.NotNull(rejected.Dialog);
        Assert.Equal(1, service.GetCalls);
    }

    [Fact]
    public async Task GraphE_result_double_click_fresh_loads_appends_once_and_closes_on_success()
    {
        var service = new FakeManualService(Sales("Alpha", 1));
        var controller = Controller(service);
        var opened = await controller.OpenManualFinancialAsync(
            ManualFinancialScreenKind.Sales);
        var resultId = Assert.Single(opened.ManualFinancial!.Rows).ResultId;

        var activated = await controller.ActivateManualFinancialResultAsync(resultId);

        Assert.Null(activated.ManualFinancial);
        Assert.Equal(1, service.GetCalls);
        Assert.Equal(0, service.MutationCalls);
        Assert.Equal("5080", Assert.Single(activated.Playlist).ToPlayoutEntry().CutCode);
        Assert.Equal("activate-manual-financial-result", activated.CommandResult?.Command);
        Assert.Equal(resultId, activated.CommandResult?.TargetId);
        Assert.True(activated.CommandResult?.Succeeded);
    }

    [Fact]
    public async Task GraphE_result_double_click_rejects_identity_change_without_append_or_close()
    {
        var service = new FakeManualService(Sales("Alpha", 1))
        {
            GetOverride = _ => Sales("Beta", 2)
        };
        var controller = Controller(service);
        var opened = await controller.OpenManualFinancialAsync(
            ManualFinancialScreenKind.Sales);
        var resultId = Assert.Single(opened.ManualFinancial!.Rows).ResultId;

        var rejected = await controller.ActivateManualFinancialResultAsync(resultId);

        Assert.NotNull(rejected.ManualFinancial);
        Assert.Empty(rejected.Playlist);
        Assert.Equal(1, service.GetCalls);
        Assert.Equal(0, service.MutationCalls);
        Assert.NotNull(rejected.Dialog);
        Assert.False(rejected.CommandResult?.Succeeded);
    }

    [Fact]
    public async Task Existing_GraphE_row_loads_verifies_and_adds_CSharp_owned_playlist_row()
    {
        var service = new FakeManualService(Sales("Alpha", 1));
        var controller = Controller(service);

        var opened = await controller.OpenManualFinancialAsync(
            ManualFinancialScreenKind.Sales);
        var rowId = Assert.Single(opened.ManualFinancial!.Rows).ResultId;
        var loaded = await controller.LoadManualFinancialAsync(rowId);

        Assert.True(loaded.ManualFinancial!.CanMaterializePlaylist);
        Assert.Equal(0, service.MutationCalls);
        var added = controller.ActivateManualFinancialAction("sales");
        var playlist = Assert.Single(added.Playlist);
        var entry = playlist.ToPlayoutEntry();
        Assert.Equal("5080", entry.CutCode);
        var selection = Assert.IsType<LegacySceneSelection>(entry.Selection);
        Assert.Equal("Alpha", selection.Subject);
        Assert.Equal("SALES", selection.GraphicType);
        Assert.Equal("1/1", playlist.PageText);
    }

    [Fact]
    public async Task GraphE_find_loads_fresh_detail_and_stale_sort_generation_opens_modal()
    {
        var service = new FakeManualService(Sales("Alpha", 1));
        var controller = Controller(service);
        var opened = await controller.OpenManualFinancialAsync(
            ManualFinancialScreenKind.Sales);
        var generation = opened.ManualFinancial!.Revision;

        var found = await controller.FindManualFinancialAsync(
            "Alpha",
            LegacyManualFinancialFindDirection.Down,
            generation);

        Assert.Equal("Alpha", found.ManualFinancial!.SelectedRecord!.Identity.StockName);
        Assert.Equal(1, service.GetCalls);
        var sorted = controller.ToggleManualFinancialNameSort(
            found.ManualFinancial.Revision);
        var stale = controller.ToggleManualFinancialNameSort(
            found.ManualFinancial.Revision);
        Assert.NotNull(stale.Dialog);
        Assert.Equal(
            sorted.ManualFinancial!.NameSortDirection,
            stale.ManualFinancial!.NameSortDirection);
    }

    [Fact]
    public async Task Create_does_not_write_until_explicit_save_and_fresh_readback_completes()
    {
        var service = new FakeManualService();
        var controller = Controller(service);
        await controller.OpenManualFinancialAsync(ManualFinancialScreenKind.OperatingProfit);
        controller.BeginManualFinancialCreate();
        var searched = await controller.SearchManualFinancialStocksAsync("Alpha");
        controller.SelectManualFinancialStock(
            Assert.Single(searched.ManualFinancial!.StockCandidates).ResultId);

        Assert.Equal(0, service.MutationCalls);
        var saved = await controller.SaveManualFinancialAsync(Profit("Alpha", 10));

        Assert.Equal(1, service.MutationCalls);
        Assert.Equal(1, service.GetCalls);
        Assert.Equal(
            LegacyManualFinancialWorkflowStatus.WriteCommitted,
            saved.ManualFinancial!.Status);
        Assert.True(saved.ManualFinancial.CanMaterializePlaylist);
        Assert.Equal("저장되었습니다", saved.Dialog?.Message);
        Assert.Equal("MmoneyCoder", saved.Dialog?.Caption);
        Assert.False(saved.Dialog?.IsConfirmation);
        Assert.DoesNotContain("password", saved.StatusMessage, StringComparison.OrdinalIgnoreCase);

        var dismissed = controller.DismissDialog();
        Assert.Null(dismissed.Dialog);
        Assert.NotNull(dismissed.ManualFinancial);
    }

    [Fact]
    public async Task Empty_GraphE_stock_confirm_is_a_quiet_no_op()
    {
        var service = new FakeManualService();
        var controller = Controller(service);
        var opened = await controller.OpenManualFinancialAsync(
            ManualFinancialScreenKind.OperatingProfit);
        var creating = controller.BeginManualFinancialCreate();

        var unchanged = await controller.SearchManualFinancialStocksAsync("   ");

        Assert.Equal(creating.Revision, unchanged.Revision);
        Assert.Equal(creating.ManualFinancial!.Revision, unchanged.ManualFinancial!.Revision);
        Assert.Empty(unchanged.ManualFinancial.StockCandidates);
        Assert.Null(unchanged.Dialog);
        Assert.Equal(0, service.MutationCalls);
        Assert.NotEqual(opened.Revision, unchanged.Revision);

        var noSelection = controller.SelectManualFinancialStock(string.Empty);
        Assert.Equal(unchanged.Revision, noSelection.Revision);
        Assert.Null(noSelection.Dialog);
    }

    [Fact]
    public async Task Known_GraphE_save_rejection_uses_specific_sanitized_dialog_and_keeps_editor()
    {
        const string safeMessage =
            "The create precondition failed and the Oracle transaction was rolled back.";
        var service = new FakeManualService
        {
            MutationException = new ManualFinancialMutationRejectedException(
                ManualFinancialMutationRejectionReason.DuplicateIdentity,
                safeMessage)
        };
        var controller = Controller(service);
        await controller.OpenManualFinancialAsync(ManualFinancialScreenKind.OperatingProfit);
        controller.BeginManualFinancialCreate();
        var searched = await controller.SearchManualFinancialStocksAsync("Alpha");
        controller.SelectManualFinancialStock(
            Assert.Single(searched.ManualFinancial!.StockCandidates).ResultId);

        var rejected = await controller.SaveManualFinancialAsync(Profit("Alpha", 10));

        Assert.Equal(1, service.MutationCalls);
        Assert.Equal(
            LegacyManualFinancialWorkflowStatus.WriteRejected,
            rejected.ManualFinancial!.Status);
        Assert.Equal(safeMessage, rejected.StatusMessage);
        Assert.Equal(safeMessage, rejected.Dialog?.Message);
        Assert.Equal("MmoneyCoder", rejected.Dialog?.Caption);
        Assert.DoesNotContain("DB연결", rejected.Dialog!.Message, StringComparison.Ordinal);

        var dismissed = controller.DismissDialog();
        Assert.Null(dismissed.Dialog);
        Assert.NotNull(dismissed.ManualFinancial);
        Assert.True(dismissed.ManualFinancial.CanSave);
    }

    [Fact]
    public async Task Malformed_GraphE_editor_data_is_not_misreported_as_a_database_outage()
    {
        var service = new FakeManualService();
        var controller = Controller(service);
        await controller.OpenManualFinancialAsync(ManualFinancialScreenKind.OperatingProfit);
        controller.BeginManualFinancialCreate();
        var searched = await controller.SearchManualFinancialStocksAsync("Alpha");
        controller.SelectManualFinancialStock(
            Assert.Single(searched.ManualFinancial!.StockCandidates).ResultId);

        var rejected = await controller.SaveManualFinancialAsync(
            Assert.IsType<ManualSalesRecord>(Sales("Alpha", 1).Record));

        Assert.NotNull(rejected.ManualFinancial);
        Assert.NotNull(rejected.Dialog);
        Assert.DoesNotContain("DB연결", rejected.Dialog!.Message, StringComparison.Ordinal);
        Assert.Equal(0, service.MutationCalls);
    }

    [Fact]
    public async Task Delete_and_delete_all_require_MmoneyCoder_confirmation_before_writing()
    {
        var service = new FakeManualService(Sales("Alpha", 1));
        var controller = Controller(service);
        var opened = await controller.OpenManualFinancialAsync(ManualFinancialScreenKind.Sales);
        await controller.LoadManualFinancialAsync(
            Assert.Single(opened.ManualFinancial!.Rows).ResultId);

        Assert.Equal(0, service.MutationCalls);
        var deleteRequested = await controller.DeleteManualFinancialAsync(all: false);
        Assert.Equal("선택한 종목을 삭제하겠습니까?", deleteRequested.Dialog?.Message);
        Assert.Equal("MmoneyCoder", deleteRequested.Dialog?.Caption);
        Assert.True(deleteRequested.Dialog?.IsConfirmation);
        Assert.Equal(0, service.MutationCalls);

        var canceled = controller.CancelDialog();
        Assert.Null(canceled.Dialog);
        Assert.Equal(0, service.MutationCalls);
        Assert.Single(canceled.ManualFinancial!.Rows);

        await controller.DeleteManualFinancialAsync(all: false);
        var deleted = await controller.ConfirmDialogAsync();
        Assert.Equal(1, service.MutationCalls);
        Assert.Empty(deleted.ManualFinancial!.Rows);

        var allRequested = await controller.DeleteManualFinancialAsync(all: true);
        Assert.Equal("모든 종목을 삭제하겠습니까?", allRequested.Dialog?.Message);
        Assert.True(allRequested.Dialog?.IsConfirmation);
        Assert.Equal(1, service.MutationCalls);

        var all = await controller.ConfirmDialogAsync();
        Assert.Equal(2, service.MutationCalls);
        Assert.Empty(all.ManualFinancial!.Rows);
    }

    [Fact]
    public async Task Delete_without_a_GraphE_row_selection_remains_a_silent_no_op()
    {
        var service = new FakeManualService(Sales("Alpha", 1));
        var controller = Controller(service);
        await controller.OpenManualFinancialAsync(ManualFinancialScreenKind.Sales);

        var result = await controller.DeleteManualFinancialAsync(all: false);

        Assert.Null(result.Dialog);
        Assert.Equal(0, service.MutationCalls);
        Assert.Single(result.ManualFinancial!.Rows);
    }

    private static LegacyOperatorController Controller(IManualFinancialScreenService service)
    {
        var workflow = new LegacyManualFinancialWorkflow(
            service,
            new FakeStockService(),
            new LegacyManualFinancialWriteSafetyState());
        return new LegacyOperatorController(
            new EmptyStockLookup(),
            manualFinancialWorkflow: workflow);
    }

    private static ManualFinancialSnapshot Sales(string stockName, int version) =>
        new(
            new ManualSalesRecord(
                new ManualFinancialIdentity(ManualFinancialScreenKind.Sales, stockName),
                Enumerable.Range(1, 6)
                    .Select(index => new ManualQuarterValue($"{index}Q", index))
                    .ToArray()),
            Version(version));

    private static ManualOperatingProfitRecord Profit(string stockName, int offset) =>
        new(
            new ManualFinancialIdentity(
                ManualFinancialScreenKind.OperatingProfit,
                stockName),
            Enumerable.Range(1, 6)
                .Select(index => new ManualQuarterValue($"{index}Q", offset + index))
                .ToArray());

    private static string Version(int value) => value.ToString("X64");

    private sealed class EmptyStockLookup : ILegacyStockLookup
    {
        public Task<LegacyStockSearchData> SearchAsync(
            string query,
            CancellationToken cancellationToken = default) =>
            Task.FromResult(new LegacyStockSearchData(query, [], []));
    }

    private sealed class FakeStockService : IStockSearchService
    {
        public Task<StockSearchResult> SearchAsync(
            string query,
            int maximumResults = LegacyStockSearchService.DefaultMaximumResults,
            CancellationToken cancellationToken = default) =>
            Task.FromResult(new StockSearchResult(
                query,
                DateTimeOffset.UtcNow,
                [new StockSearchItem(
                    StockMarket.Kospi,
                    DataSourceKind.Oracle,
                    "Alpha",
                    "000001")],
                false));
    }

    private sealed class FakeManualService : IManualFinancialScreenService
    {
        private readonly Dictionary<(ManualFinancialScreenKind, string), ManualFinancialSnapshot>
            _rows = new();
        private int _version = 10;

        internal FakeManualService(params ManualFinancialSnapshot[] rows)
        {
            foreach (var row in rows)
            {
                _rows[(row.Record.Identity.Screen, row.Record.Identity.StockName)] = row;
            }
        }

        public bool CanMutate => true;

        internal int MutationCalls { get; private set; }

        internal int GetCalls { get; private set; }

        internal ManualFinancialMutationException? MutationException { get; init; }

        internal int RejectedItemCount { get; init; }

        internal IReadOnlyList<string> UnreadableStockNames { get; init; } =
            Array.Empty<string>();

        internal Func<ManualFinancialIdentity, ManualFinancialSnapshot>? GetOverride
        {
            get;
            init;
        }

        public Task<ManualFinancialSearchResult> SearchAsync(
            ManualFinancialScreenKind screen,
            string query = "",
            int maximumResults = LegacyManualFinancialScreenService.DefaultMaximumResults,
            CancellationToken cancellationToken = default)
        {
            var rows = _rows.Values
                .Where(row => row.Record.Identity.Screen == screen)
                .Where(row => query.Length == 0 ||
                    row.Record.Identity.StockName.Contains(
                        query,
                        StringComparison.OrdinalIgnoreCase))
                .Take(maximumResults)
                .ToArray();
            return Task.FromResult(new ManualFinancialSearchResult(
                screen,
                query,
                DateTimeOffset.UtcNow,
                rows,
                false)
            {
                RejectedItemCount = RejectedItemCount,
                UnreadableStockNames = UnreadableStockNames
            });
        }

        public Task<ManualFinancialSnapshot> GetAsync(
            ManualFinancialIdentity identity,
            CancellationToken cancellationToken = default)
        {
            GetCalls++;
            return Task.FromResult(
                GetOverride?.Invoke(identity) ??
                _rows[(identity.Screen, identity.StockName)]);
        }

        public Task<ManualFinancialMutationReceipt> CreateAsync(
            ManualFinancialRecord record,
            CancellationToken cancellationToken = default)
        {
            MutationCalls++;
            if (MutationException is not null)
            {
                throw MutationException;
            }

            _rows[(record.Identity.Screen, record.Identity.StockName)] =
                new ManualFinancialSnapshot(record, Version(++_version));
            return Task.FromResult(Receipt(
                ManualFinancialMutationKind.Create,
                record.Identity.Screen,
                record.Identity.StockName,
                1));
        }

        public Task<ManualFinancialMutationReceipt> UpdateAsync(
            ManualFinancialSnapshot expected,
            ManualFinancialRecord replacement,
            CancellationToken cancellationToken = default)
        {
            MutationCalls++;
            if (MutationException is not null)
            {
                throw MutationException;
            }

            _rows[(replacement.Identity.Screen, replacement.Identity.StockName)] =
                new ManualFinancialSnapshot(replacement, Version(++_version));
            return Task.FromResult(Receipt(
                ManualFinancialMutationKind.Update,
                replacement.Identity.Screen,
                replacement.Identity.StockName,
                1));
        }

        public Task<ManualFinancialMutationReceipt> DeleteAsync(
            ManualFinancialSnapshot expected,
            CancellationToken cancellationToken = default)
        {
            MutationCalls++;
            _rows.Remove((
                expected.Record.Identity.Screen,
                expected.Record.Identity.StockName));
            return Task.FromResult(Receipt(
                ManualFinancialMutationKind.DeleteOne,
                expected.Record.Identity.Screen,
                expected.Record.Identity.StockName,
                1));
        }

        public Task<ManualFinancialMutationReceipt> DeleteAllAsync(
            ManualFinancialScreenKind screen,
            CancellationToken cancellationToken = default)
        {
            MutationCalls++;
            var keys = _rows.Keys.Where(key => key.Item1 == screen).ToArray();
            foreach (var key in keys)
            {
                _rows.Remove(key);
            }

            return Task.FromResult(Receipt(
                ManualFinancialMutationKind.DeleteAll,
                screen,
                string.Empty,
                keys.Length));
        }

        private static ManualFinancialMutationReceipt Receipt(
            ManualFinancialMutationKind kind,
            ManualFinancialScreenKind screen,
            string stockName,
            int affectedRows) =>
            new(
                Guid.NewGuid(),
                kind,
                screen,
                stockName,
                DateTimeOffset.UtcNow,
                affectedRows);
    }

    private sealed class RawGraphEQueryExecutor : IDataQueryExecutor
    {
        private readonly DataTable _rows = CreateRows();

        public DataTable Execute(DataSourceKind source, string tableName, string query) =>
            throw new NotSupportedException();

        public Task<DataTable> ExecuteAsync(
            DataSourceKind source,
            string tableName,
            string query,
            CancellationToken cancellationToken = default) =>
            throw new NotSupportedException();

        public Task<DataTable> ExecuteAsync(
            DataSourceKind source,
            string tableName,
            DataQuerySpec query,
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (!string.Equals(
                    tableName,
                    "MANUAL_FINANCIAL_SELL_RAW_GET",
                    StringComparison.Ordinal))
            {
                return Task.FromResult(_rows.Copy());
            }

            var stockName = Assert.IsType<string>(query.Parameters.Single(
                parameter => string.Equals(
                    parameter.Name,
                    "stock_name",
                    StringComparison.OrdinalIgnoreCase)).Value);
            var result = _rows.Clone();
            var row = Assert.Single(_rows.AsEnumerable().Where(candidate =>
                string.Equals(
                    candidate.Field<string>("STOCK_NAME"),
                    stockName,
                    StringComparison.Ordinal)));
            result.ImportRow(row);
            return Task.FromResult(result);
        }

        private static DataTable CreateRows()
        {
            var table = new DataTable();
            foreach (var column in ManualFinancialCutContracts
                         .Get(ManualFinancialScreenKind.Sales)
                         .StorageColumns)
            {
                table.Columns.Add(column, typeof(string));
            }

            table.Columns.Add("ROW_VERSION", typeof(string));
            table.Columns.Add("ROW_HANDLE", typeof(string));
            table.Rows.Add(
                "Alpha",
                "1Q_10", "2Q_20", "3Q_30", "4Q_40", "5Q_50", "6Q_60",
                Version(1),
                "AAAAAAAAAAAAAAAA01");
            table.Rows.Add(
                "Broken",
                "1Q_not-a-number", "2Q_20", "3Q_30", "4Q_40", "5Q_50", "6Q_60",
                Version(2),
                "AAAAAAAAAAAAAAAA02");
            return table;
        }
    }
}
