using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using PolyTrader.Modules.Accounting.Logic;
using PolyTrader.Modules.Accounting.Models;
using PolyTrader.Modules.Accounting.Persistence;
using PolyTrader.Modules.Accounting.Services;
using PolyTrader.Tests.TestSupport;
using PolyTraderSharp;
using PolyTraderSharp.Models;
using PolyTraderSharp.Services;
using Xunit;

namespace PolyTrader.Tests
{
    /// <summary>
    /// Sicherheitsnetz für das Accounting-Fundament (A-1): pure Klassifikation (Typ/Vorzeichen/
    /// Idempotenz), Deposit/Withdrawal-Trennung intern↔extern, und die Ingest-Orchestrierung
    /// (idempotenter Upsert, Balance-Anker, Backfill vs. inkrementell).
    /// </summary>
    public class AccountingTests
    {
        // ---------------- Pure Klassifikation ----------------

        private static RawActivity Act(string type, string side, decimal usdc, decimal fee = 0m,
            string tx = "0xabc", int logIndex = 0, string token = "tok") => new()
        {
            Type = type, Side = side, Timestamp = new DateTime(2026, 7, 1, 12, 0, 0, DateTimeKind.Utc),
            TxHash = tx, LogIndex = logIndex, TokenId = token, UsdcAmount = usdc, Fee = fee, Size = 10m, Price = 0.5m
        };

        [Fact]
        public void Buy_is_cash_out_including_fee()
        {
            var e = AccountingClassifier.ClassifyActivity(1, Act("TRADE", "BUY", 100m, 1.5m), 7);
            Assert.Equal(LedgerEventType.TradeBuy, e.EventType);
            Assert.Equal(-101.5m, e.NetUsdc);        // −(brutto + fee)
            Assert.Equal(100m, e.GrossUsdc);
            Assert.Equal(1.5m, e.FeeUsdc);
            Assert.Equal(7, e.IngestBatchId);
        }

        [Fact]
        public void Sell_is_cash_in_minus_fee()
        {
            var e = AccountingClassifier.ClassifyActivity(1, Act("TRADE", "SELL", 100m, 1.5m), 7);
            Assert.Equal(LedgerEventType.TradeSell, e.EventType);
            Assert.Equal(98.5m, e.NetUsdc);          // brutto − fee
        }

        [Theory]
        [InlineData("REDEEM", 50.0, 50.0)]
        [InlineData("REWARD", 3.0, 3.0)]
        [InlineData("SPLIT", 20.0, 0.0)]     // geldneutral
        [InlineData("MERGE", 20.0, 0.0)]
        [InlineData("CONVERSION", 20.0, 0.0)]
        public void Other_types_have_expected_net(string type, double usdc, double expectedNet)
        {
            var e = AccountingClassifier.ClassifyActivity(1, Act(type, "", (decimal)usdc), 7);
            Assert.Equal((decimal)expectedNet, e.NetUsdc);
        }

        [Fact]
        public void Idempotency_key_is_stable_and_type_specific()
        {
            var buy = AccountingClassifier.ClassifyActivity(1, Act("TRADE", "BUY", 100m, tx: "0xAbC", logIndex: 2), 7);
            var buyAgain = AccountingClassifier.ClassifyActivity(1, Act("TRADE", "BUY", 100m, tx: "0xabc", logIndex: 2), 99);
            Assert.Equal(buy.IdempotencyKey, buyAgain.IdempotencyKey);   // gleicher Key trotz anderem Batch/Casing
            var sell = AccountingClassifier.ClassifyActivity(1, Act("TRADE", "SELL", 100m, tx: "0xabc", logIndex: 2), 7);
            Assert.NotEqual(buy.IdempotencyKey, sell.IdempotencyKey);    // Typ unterscheidet
        }

        [Fact]
        public void Transfer_external_is_deposit_or_withdrawal_internal_is_skipped()
        {
            var contracts = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "0xctf" };

            var deposit = AccountingClassifier.ClassifyTransfer(1, new RawTransfer
            {
                IsIncoming = true, FromAddress = "0xExternalWhale", ToAddress = "0xSafe",
                UsdcAmount = 500m, TxHash = "0xd", LogIndex = 1
            }, 7, contracts);
            Assert.NotNull(deposit);
            Assert.Equal(LedgerEventType.Deposit, deposit!.EventType);
            Assert.Equal(500m, deposit.NetUsdc);

            var withdrawal = AccountingClassifier.ClassifyTransfer(1, new RawTransfer
            {
                IsIncoming = false, FromAddress = "0xSafe", ToAddress = "0xExternalBank",
                UsdcAmount = 200m, TxHash = "0xw", LogIndex = 0
            }, 7, contracts);
            Assert.Equal(LedgerEventType.Withdrawal, withdrawal!.EventType);
            Assert.Equal(-200m, withdrawal.NetUsdc);

            // interne Bewegung (Gegenpart = System-Contract) → NICHT als Ein-/Auszahlung buchen
            var internalMove = AccountingClassifier.ClassifyTransfer(1, new RawTransfer
            {
                IsIncoming = false, FromAddress = "0xSafe", ToAddress = "0xCTF", UsdcAmount = 100m, TxHash = "0xi"
            }, 7, contracts);
            Assert.Null(internalMove);
        }

        // ---------------- Ingest-Orchestrierung ----------------

        private sealed class FakeActivitySource : IActivitySource
        {
            public List<RawActivity> Items { get; } = new();
            public int Calls { get; private set; }
            public Task<IReadOnlyList<RawActivity>> GetActivityAsync(string wallet, DateTime? since, CancellationToken ct)
            { Calls++; return Task.FromResult((IReadOnlyList<RawActivity>)Items.ToList()); }
        }
        private sealed class FakeBalance : IBalanceAnchorSource
        {
            public decimal? Value;
            public Task<decimal?> GetBalanceAsync(string wallet, CancellationToken ct) => Task.FromResult(Value);
        }

        private static (AccountingIngestService svc, EfLedgerRepository ledger, EfIngestRunRepository runs, FakeActivitySource act, FakeBalance bal)
            BuildIngest()
        {
            var factory = new InMemoryContextFactory<AccountingDbContext>(o => new AccountingDbContext(o));
            var ledger = new EfLedgerRepository(factory);
            var runs = new EfIngestRunRepository(factory);
            var raw = new EfRawSnapshotRepository(factory);
            var act = new FakeActivitySource();
            var bal = new FakeBalance();
            var state = new TradingState();
            var svc = new AccountingIngestService(state, ledger, runs, raw, act, new NullTransferSource(), bal,
                new AccountingSystemContracts(), new TerminalLogger());
            return (svc, ledger, runs, act, bal);
        }

        private static AccountState LiveAccount(int id) =>
            new() { AccountId = id, Name = "Live", IsDemo = false, WalletAddress = "0xSafe" };

        [Fact]
        public async Task Ingest_books_entries_and_is_idempotent_on_reingest()
        {
            var (svc, ledger, _, act, bal) = BuildIngest();
            act.Items.Add(Act("TRADE", "BUY", 100m, 1m, tx: "0x1", logIndex: 0));
            act.Items.Add(Act("TRADE", "SELL", 120m, 1m, tx: "0x2", logIndex: 0));
            bal.Value = 18m;   // Anker

            var run1 = await svc.IngestAccountAsync(LiveAccount(1), backfill: true, CancellationToken.None);
            Assert.True(run1.Success);
            Assert.Equal(2, run1.NewEntries);
            Assert.Equal(0, run1.DuplicateEntries);
            Assert.Equal(2, ledger.Count(1));

            // Zweiter Lauf mit denselben Ereignissen → alles Duplikate, keine Doppelbuchung.
            var run2 = await svc.IngestAccountAsync(LiveAccount(1), backfill: true, CancellationToken.None);
            Assert.Equal(0, run2.NewEntries);
            Assert.Equal(2, run2.DuplicateEntries);
            Assert.Equal(2, ledger.Count(1));
        }

        [Fact]
        public async Task Ingest_records_balance_anchor_delta()
        {
            var (svc, ledger, _, act, bal) = BuildIngest();
            act.Items.Add(Act("TRADE", "BUY", 100m, 0m, tx: "0x1"));   // Netto −100
            act.Items.Add(Act("REDEEM", "", 150m, tx: "0x2"));          // Netto +150 → Σ = 50
            bal.Value = 50m;                                            // Anker == Ledger-Netto → Δ 0

            var run = await svc.IngestAccountAsync(LiveAccount(1), backfill: true, CancellationToken.None);

            Assert.Equal(50m, run.LedgerNetUsdc);
            Assert.Equal(50m, run.BalanceAnchorUsdc);
            Assert.Equal(0m, run.BalanceDeltaUsdc);   // Vollständigkeit: Soll-Ist ≈ 0
        }

        [Fact]
        public async Task Incremental_run_queries_from_last_timestamp_with_lookback()
        {
            var (svc, _, _, act, _) = BuildIngest();
            act.Items.Add(Act("TRADE", "BUY", 100m, tx: "0x1"));
            await svc.IngestAccountAsync(LiveAccount(1), backfill: true, CancellationToken.None);

            var run = await svc.IngestAccountAsync(LiveAccount(1), backfill: false, CancellationToken.None);
            // Fenster beginnt beim jüngsten Ledger-Zeitpunkt minus Lookback-Überlappung.
            Assert.NotNull(run.FromTimestamp);
            var expected = new DateTime(2026, 7, 1, 12, 0, 0, DateTimeKind.Utc)
                .AddHours(-AccountingIngestService.IncrementalLookbackHours);
            Assert.Equal(expected, run.FromTimestamp);
        }
    }
}
