using System;
using System.Net.Http;
using System.Threading.Channels;
using System.Threading.Tasks;
using PolyTrader.Tests.Fakes;
using PolyTraderSharp;
using PolyTraderSharp.Models;
using PolyTraderSharp.Services;
using Xunit;

namespace PolyTrader.Tests
{
    /// <summary>
    /// Integrationstests des geldkritischen Engine-Entscheidungspfads (ProcessAccountOrderAsync) über
    /// einen gemockten CLOB-Client. Deckt die Fable-Guards ab, die zwischen Services entstehen und
    /// durch reine Unit-Tests nicht fangbar sind: H3 (BUY-Skip bei ExitPending), Doppel-SELL-Guard,
    /// K3 (System-Close TraderId==0) und H2 (Order-Cleanup schont aktive Leiter).
    /// </summary>
    public class CopyTradingEngineTests
    {
        private const string Tok = "tok-eng";

        private sealed class Harness
        {
            public CopyTradingEngine Engine = null!;
            public TradingState State = null!;
            public CopyTradingState Copy = null!;
            public FakeClobClient Clob = null!;
            public ChannelReader<ClosedTrade> ClosedReader = null!;
            public FakeDecisionJournal Journal = null!;
        }

        private static Harness Build()
        {
            var state = new TradingState { LiveTradingMode = TradingMode.Active, DemoTradingMode = TradingMode.Active };
            var copy = new CopyTradingState();
            var clob = new FakeClobClient();
            var logger = new TerminalLogger();
            var api = new PolymarketApiService(logger, new HttpClient());
            var posRepo = new FakePositionRepository();
            var marketRepo = new FakeMarketRepository();
            var accountRepo = new FakeAccountRepository();
            var notify = new FakeNotificationSink();
            var journal = new FakeDecisionJournal();
            var orderEvents = new FakeOrderEventLog();
            var ladder = new SellLadderService(copy, state, clob, logger, notify, posRepo, orderEvents);

            var signalCh = Channel.CreateUnbounded<CopySignal>();
            var closedCh = Channel.CreateUnbounded<ClosedTrade>();

            var engine = new CopyTradingEngine(state, copy, signalCh.Reader, closedCh.Writer, logger,
                clob, api, posRepo, marketRepo, accountRepo, ladder, journal, orderEvents);

            // MarketData cachen, damit der API-Pfad (Cache-Miss) nie läuft.
            state.MarketCache[Tok] = new MarketData { Slug = "slug", Question = "Frage?", Category = "Politics", TakerFeeBps = 0, NegRisk = false };

            return new Harness { Engine = engine, State = state, Copy = copy, Clob = clob, ClosedReader = closedCh.Reader, Journal = journal };
        }

        private static AccountState Account(bool demo) => new()
        {
            AccountId = 1, Name = demo ? "Demo" : "Live", IsDemo = demo,
            TotalBalance = 1000m, AvailableBalance = 1000m
        };

        private static Position Pos(bool exitPending = false, decimal size = 100m) => new()
        {
            TokenId = Tok, MarketQuestion = "Frage?", MarketSlug = "slug", SourceTraderId = 7,
            Size = size, EntryPrice = 0.40m, CurrentPrice = 0.50m, AmountUsd = 40m,
            OpenedAt = DateTime.UtcNow.AddHours(-1), ExitPending = exitPending
        };

        private static CopySignal Signal(string side, int traderId, decimal price) => new()
        {
            TraderId = traderId, TokenId = Tok, MarketSlug = "slug", MarketQuestion = "Frage?",
            Outcome = "Yes", Side = side, Price = price, Size = 100m, Timestamp = DateTime.UtcNow, Reason = "test"
        };

        private static TrackedTrader Trader() => new() { Id = 7, IsActive = true, Category = "" };

        // ---------- H3: BUY-Skip während ExitPending ----------

        [Fact]
        public async Task Buy_is_skipped_while_position_exit_pending()
        {
            var h = Build();
            var acc = Account(demo: false);
            acc.OpenPositions[Tok] = Pos(exitPending: true);

            await h.Engine.ProcessAccountOrderAsync(acc, Trader(), Signal("BUY", 7, 0.50m), false);

            Assert.Empty(h.Clob.Placed);          // kein Zukauf während des Ausstiegs
            Assert.True(acc.OpenPositions[Tok].ExitPending);
            // S-0: Entscheidung strukturiert im Journal (nicht nur Freitext-Log).
            Assert.Contains(h.Journal.Written, d =>
                d.Decision == TradeDecision.Skipped && d.Reason == DecisionReason.ExitPendingBuySkip && d.TokenId == Tok);
        }

        // ---------- Doppel-SELL-Guard ----------

        [Fact]
        public async Task Sell_is_ignored_while_ladder_already_running()
        {
            var h = Build();
            var acc = Account(demo: false);
            acc.OpenPositions[Tok] = Pos(exitPending: true);

            await h.Engine.ProcessAccountOrderAsync(acc, Trader(), Signal("SELL", 7, 0.45m), false);

            Assert.Empty(h.Clob.Placed);          // keine zweite Leiter/Order
            Assert.True(acc.OpenPositions.ContainsKey(Tok));
        }

        // ---------- K3: System-Close (TraderId == 0) ----------

        [Fact]
        public async Task System_close_resolves_demo_position_despite_foreign_owner()
        {
            var h = Build();
            var acc = Account(demo: true);
            acc.OpenPositions[Tok] = Pos();       // SourceTraderId = 7

            // System-SELL (TraderId 0) bei Marktauflösung – trader ist null.
            await h.Engine.ProcessAccountOrderAsync(acc, null, Signal("SELL", 0, 1.0m), false);

            Assert.False(acc.OpenPositions.ContainsKey(Tok));      // Demo-Position geschlossen
            Assert.True(h.ClosedReader.TryRead(out var ct));       // ClosedTrade geschrieben
            Assert.Equal(Tok, ct!.TokenId);
            Assert.False(string.IsNullOrEmpty(ct.SignalId));       // S-0: Korrelation gesetzt
            Assert.Contains(h.Journal.Written, d =>
                d.Decision == TradeDecision.Executed && d.Reason == DecisionReason.SystemResolutionClose);
        }

        [Fact]
        public async Task Foreign_trader_sell_is_still_rejected()
        {
            // Regression: der Ownership-Check bleibt für echte Master (TraderId != 0) scharf.
            var h = Build();
            var acc = Account(demo: true);
            acc.OpenPositions[Tok] = Pos();       // gehört Trader 7

            await h.Engine.ProcessAccountOrderAsync(acc, new TrackedTrader { Id = 9, IsActive = true }, Signal("SELL", 9, 1.0m), false);

            Assert.True(acc.OpenPositions.ContainsKey(Tok));       // NICHT geschlossen
            Assert.False(h.ClosedReader.TryRead(out _));
        }

        // ---------- H2: Order-Cleanup schont aktive Leiter ----------

        [Fact]
        public async Task Pre_signal_cleanup_is_skipped_when_ladder_active()
        {
            var h = Build();
            var acc = Account(demo: false);
            acc.HasOpenLimitOrders = true;
            acc.OpenPositions[Tok] = Pos(exitPending: true);
            h.Copy.ExitLadders["1_" + Tok] = new ExitLadderState { AccountId = 1, TokenId = Tok, Floor = 0.40m, CurrentLimit = 0.45m };

            await h.Engine.ProcessAccountOrderAsync(acc, Trader(), Signal("SELL", 7, 0.45m), false);

            Assert.Empty(h.Clob.ConflictCancels); // Leiter-Order NICHT weggeräumt
        }

        [Fact]
        public async Task Pre_signal_cleanup_runs_when_no_ladder_active()
        {
            // Kontrast: ohne aktive Leiter räumt der Cleanup konfligierende Orders auf.
            var h = Build();
            var acc = Account(demo: false);
            acc.HasOpenLimitOrders = true;
            acc.OpenPositions[Tok] = Pos();       // nicht ExitPending, keine Leiter

            await h.Engine.ProcessAccountOrderAsync(acc, Trader(), Signal("SELL", 7, 0.45m), false);

            Assert.Single(h.Clob.ConflictCancels);
        }
    }
}
