using System;
using System.Linq;
using System.Threading.Tasks;
using PolyTrader.Tests.Fakes;
using PolyTraderSharp;
using PolyTraderSharp.Models;
using PolyTraderSharp.Services;
using Xunit;

namespace PolyTrader.Tests
{
    /// <summary>
    /// Integrationstests der SELL-Eskalationsleiter über einen gemockten CLOB-Client
    /// (Slice 1 der Fable-Fixes): atomarer Leiter-Claim (H1) und Floor-Robustheit (K1).
    /// Diese Fehler entstehen zwischen Services und sind durch reine Unit-Tests nicht fangbar.
    /// </summary>
    public class SellLadderServiceTests
    {
        private const string Tok = "tok-1";

        private static (SellLadderService svc, CopyTradingState copy, TradingState state, FakeClobClient clob, FakePositionRepository repo)
            Build(FakeClobClient? clob = null)
        {
            var copy = new CopyTradingState();
            var state = new TradingState();
            clob ??= new FakeClobClient();
            var logger = new TerminalLogger();
            var notify = new FakeNotificationSink();
            var repo = new FakePositionRepository();
            var svc = new SellLadderService(copy, state, clob, logger, notify, repo, new FakeOrderEventLog());
            return (svc, copy, state, clob, repo);
        }

        private static (AccountState account, Position pos) LiveAccountWithPosition(TradingState state, decimal size = 100m)
        {
            var account = new AccountState { AccountId = 1, Name = "Live-Test", IsDemo = false };
            var pos = new Position
            {
                TokenId = Tok,
                MarketQuestion = "Wird X passieren?",
                SourceTraderId = 7,
                Size = size,
                EntryPrice = 0.40m,
                CurrentPrice = 0.50m,
                AmountUsd = 40m
            };
            account.OpenPositions[pos.TokenId] = pos;
            state.Accounts[account.AccountId] = account;
            return (account, pos);
        }

        // ---------- H1: Atomarer Claim ----------

        [Fact]
        public async Task StartLadder_places_one_order_and_registers_ladder()
        {
            var (svc, copy, state, clob, _) = Build();
            var (account, pos) = LiveAccountWithPosition(state);

            bool ok = await svc.StartLadderAsync(account, pos, 0.50m, 7, false, 5m, 15m, false, "Master SELL");

            Assert.True(ok);
            Assert.True(copy.ExitLadders.ContainsKey("1_" + Tok));
            Assert.True(pos.ExitPending);
            Assert.Single(clob.Placed);
            Assert.Equal("SELL", clob.Placed[0].Side);
        }

        [Fact]
        public async Task Second_StartLadder_for_same_position_is_idempotent_no_second_order()
        {
            var (svc, copy, state, clob, _) = Build();
            var (account, pos) = LiveAccountWithPosition(state);

            bool first = await svc.StartLadderAsync(account, pos, 0.50m, 7, false, 5m, 15m, false, "Master SELL");
            bool second = await svc.StartLadderAsync(account, pos, 0.50m, 7, false, 5m, 15m, false, "Profit Target");

            Assert.True(first);
            Assert.False(second);           // zweiter Aufruf verliert den Claim
            Assert.Single(clob.Placed);     // KEINE zweite GTC-Order
        }

        [Fact]
        public async Task Concurrent_StartLadder_starts_exactly_one_ladder()
        {
            var (svc, copy, state, clob, _) = Build();
            var (account, pos) = LiveAccountWithPosition(state);

            var results = await Task.WhenAll(
                Enumerable.Range(0, 8).Select(_ =>
                    svc.StartLadderAsync(account, pos, 0.50m, 7, false, 5m, 15m, false, "race")));

            Assert.Equal(1, results.Count(r => r));   // genau ein Gewinner
            Assert.Single(clob.Placed);               // genau eine Order
            Assert.True(copy.ExitLadders.ContainsKey("1_" + Tok));
        }

        [Fact]
        public async Task StartLadder_when_ladder_already_exists_readds_position_as_exitpending()
        {
            // H1-Waisen-Schutz: Engine entfernt die Position vor StartLadderAsync. Verliert dieser Aufruf
            // den Claim (andere Leiter läuft schon), muss die Position trotzdem als ExitPending präsent bleiben.
            var (svc, copy, state, clob, _) = Build();
            var (account, pos) = LiveAccountWithPosition(state);

            copy.ExitLadders["1_" + Tok] = new ExitLadderState { AccountId = 1, TokenId = Tok, Floor = 0.40m, CurrentLimit = 0.45m };
            account.OpenPositions.TryRemove(Tok, out _);   // Engine hat entfernt
            pos.ExitPending = false;

            bool ok = await svc.StartLadderAsync(account, pos, 0.50m, 7, false, 5m, 15m, false, "Master SELL");

            Assert.False(ok);
            Assert.True(account.OpenPositions.ContainsKey(Tok));   // wieder da
            Assert.True(pos.ExitPending);
            Assert.Empty(clob.Placed);                            // keine neue Order
        }

        [Fact]
        public async Task StartLadder_releases_claim_when_place_fails()
        {
            var clob = new FakeClobClient { PlaceResult = "ERROR: insufficient balance" };
            var (svc, copy, state, _, _) = Build(clob);
            var (account, pos) = LiveAccountWithPosition(state);

            bool ok = await svc.StartLadderAsync(account, pos, 0.50m, 7, false, 5m, 15m, false, "Master SELL");

            Assert.False(ok);
            Assert.False(copy.ExitLadders.ContainsKey("1_" + Tok));   // Claim zurückgegeben
            Assert.False(pos.ExitPending);
            Assert.True(account.OpenPositions.ContainsKey(Tok));      // Position bleibt handelbar
        }

        // ---------- K1: Floor-Robustheit ----------

        private static ExitLadderState FloorLadder() => new()
        {
            AccountId = 1,
            TokenId = Tok,
            SourceTraderId = 7,
            MarketQuestion = "Wird X passieren?",
            ReferencePrice = 0.50m,
            CurrentLimit = 0.40m,
            Floor = 0.40m,          // CurrentLimit == Floor -> IsAtFloor
            IsHf = false,
            Attempt = 5,
            LastActionAt = DateTime.UtcNow.AddMinutes(-10)   // Intervall (120s) längst vorbei
        };

        [Fact]
        public async Task Floor_replaces_order_when_none_resting()
        {
            var (svc, copy, state, clob, _) = Build();
            var (account, pos) = LiveAccountWithPosition(state);
            var ladder = FloorLadder();
            copy.ExitLadders["1_" + Tok] = ladder;
            // FakeClob liefert für den Token keine offenen Orders -> Order ist weg (z.B. Cleanup/extern).

            await svc.ProcessLadderAsync("1_" + Tok, ladder);

            Assert.Single(clob.Placed);
            Assert.Equal("SELL", clob.Placed[0].Side);
            Assert.Equal(0.40m, clob.Placed[0].Price);                 // am Floor neu platziert
            Assert.True(copy.PendingOrderTimestamps.ContainsKey("1_" + Tok));
        }

        [Fact]
        public async Task Floor_holds_when_order_still_resting()
        {
            var (svc, copy, state, clob, _) = Build();
            var (account, pos) = LiveAccountWithPosition(state);
            var ladder = FloorLadder();
            ladder.FloorNotified = true;   // Floor-Benachrichtigung überspringen
            copy.ExitLadders["1_" + Tok] = ladder;
            clob.OpenOrdersByAsset[Tok] = new() { ("oid-1", "SELL", 0.40m) };

            await svc.ProcessLadderAsync("1_" + Tok, ladder);

            Assert.Empty(clob.Placed);   // Order ruht -> nicht neu platzieren
        }

        [Fact]
        public async Task Ladder_completes_when_position_gone()
        {
            var (svc, copy, state, clob, _) = Build();
            var (account, pos) = LiveAccountWithPosition(state);
            var ladder = FloorLadder();
            copy.ExitLadders["1_" + Tok] = ladder;
            account.OpenPositions.TryRemove(Tok, out _);   // Sync hat den Fill erkannt

            await svc.ProcessLadderAsync("1_" + Tok, ladder);

            Assert.False(copy.ExitLadders.ContainsKey("1_" + Tok));   // Leiter beendet
            Assert.Empty(clob.Placed);
        }

        [Fact]
        public async Task Ladder_aborts_on_dust_remainder_below_minimum()
        {
            // H4: Dust-Rest unter dem Minimum -> Leiter beenden statt endloser Sub-Minimum-Rejects.
            var (svc, copy, state, clob, _) = Build();
            var (account, pos) = LiveAccountWithPosition(state, size: 3m);   // < MinShares (5.5)
            pos.ExitPending = true;
            var ladder = FloorLadder();
            copy.ExitLadders["1_" + Tok] = ladder;

            await svc.ProcessLadderAsync("1_" + Tok, ladder);

            Assert.False(copy.ExitLadders.ContainsKey("1_" + Tok));   // Leiter beendet
            Assert.False(pos.ExitPending);                            // Rest handelbar/gehalten
            Assert.Empty(clob.Placed);                               // keine Sub-Minimum-Order
        }
    }
}
