using System;
using PolyTrader.Modules.CopyTrading.Logic;
using PolyTraderSharp.Models;
using Xunit;
using static PolyTrader.Modules.CopyTrading.Logic.CopyTradingRisk;

namespace PolyTrader.Tests
{
    /// <summary>
    /// Umfangreiches Sicherheitsnetz für die reine BUY-Risikologik der Engine
    /// (Limit-Preis, Zeitfenster, Markt-Budget). Diese Werte fließen direkt in CLOB-Orders –
    /// jede Regression hier ist teuer. Verhalten ist 1:1 aus CopyTradingEngine übernommen.
    /// </summary>
    public class CopyTradingRiskTests
    {
        // ---------------- CalculateBuyOrderPrice ----------------

        [Fact]
        public void OrderPrice_normal_trader_applies_percentage_markup()
        {
            // 0.50 + 2 % = 0.51
            Assert.Equal(0.51m, CalculateBuyOrderPrice(0.50m, isHfTrader: false, maxPriceDifferencePct: 2m, maxBuyPrice: 0.98m));
        }

        [Fact]
        public void OrderPrice_hf_trader_applies_fixed_half_cent_markup()
        {
            // 0.50 + 0.005 = 0.505 (Prozentwert wird ignoriert)
            Assert.Equal(0.505m, CalculateBuyOrderPrice(0.50m, isHfTrader: true, maxPriceDifferencePct: 99m, maxBuyPrice: 0.98m));
        }

        [Fact]
        public void OrderPrice_is_capped_by_max_buy_price()
        {
            // desired 0.50*2 = 1.00, MaxBuy 0.80 -> 0.80
            Assert.Equal(0.80m, CalculateBuyOrderPrice(0.50m, false, 100m, 0.80m));
        }

        [Fact]
        public void OrderPrice_never_exceeds_hard_cap_099()
        {
            // desired 0.98*1.05 = 1.029, MaxBuy 1.00 -> Min = 1.00 -> hart auf 0.99
            Assert.Equal(0.99m, CalculateBuyOrderPrice(0.98m, false, 5m, 1.00m));
        }

        [Fact]
        public void OrderPrice_hard_cap_beats_higher_max_buy_price()
        {
            Assert.Equal(0.99m, CalculateBuyOrderPrice(0.995m, true, 0m, 1.00m));
        }

        [Fact]
        public void OrderPrice_zero_markup_returns_signal_price()
        {
            Assert.Equal(0.42m, CalculateBuyOrderPrice(0.42m, false, 0m, 0.98m));
        }

        [Fact]
        public void OrderPrice_exactly_099_stays()
        {
            Assert.Equal(0.99m, CalculateBuyOrderPrice(0.99m, false, 0m, 0.99m));
        }

        // ---------------- ResolveTimeBucket ----------------

        [Theory]
        [InlineData(-1.0, TimeBucket.Under6h)]     // bereits abgelaufen
        [InlineData(0.0, TimeBucket.Under6h)]
        [InlineData(5.99, TimeBucket.Under6h)]
        [InlineData(6.0, TimeBucket.Under24h)]     // Grenze: < 6 ist false
        [InlineData(23.99, TimeBucket.Under24h)]
        [InlineData(24.0, TimeBucket.Under72h)]
        [InlineData(71.99, TimeBucket.Under72h)]
        [InlineData(72.0, TimeBucket.Over72h)]
        [InlineData(999999.0, TimeBucket.Over72h)]
        public void ResolveTimeBucket_maps_hours_to_bucket(double hoursLeft, TimeBucket expected)
        {
            Assert.Equal(expected, ResolveTimeBucket(hoursLeft));
        }

        // ---------------- TimeLimitPct / Label ----------------

        [Fact]
        public void TimeLimitPct_maps_each_bucket_to_its_setting()
        {
            var s = new CopyTradingAccountSettings
            {
                perMaxTime6h = 1m,
                perMaxTime24h = 2m,
                perMaxTime72h = 3m,
                perMaxTimeNone = 4m
            };
            Assert.Equal(1m, TimeLimitPct(TimeBucket.Under6h, s));
            Assert.Equal(2m, TimeLimitPct(TimeBucket.Under24h, s));
            Assert.Equal(3m, TimeLimitPct(TimeBucket.Under72h, s));
            Assert.Equal(4m, TimeLimitPct(TimeBucket.Over72h, s));
        }

        [Theory]
        [InlineData(TimeBucket.Under6h, "< 6h")]
        [InlineData(TimeBucket.Under24h, "< 24h")]
        [InlineData(TimeBucket.Under72h, "< 72h")]
        [InlineData(TimeBucket.Over72h, "> 72h")]
        public void TimeBucketLabel_matches_bucket(TimeBucket bucket, string expected)
        {
            Assert.Equal(expected, TimeBucketLabel(bucket));
        }

        // ---------------- IsPositionInBucket ----------------

        private static readonly DateTime Now = new(2026, 7, 7, 12, 0, 0, DateTimeKind.Utc);

        [Fact]
        public void PositionInBucket_null_expiry_counts_only_for_over72h()
        {
            Assert.True(IsPositionInBucket(null, TimeBucket.Over72h, Now));
            Assert.False(IsPositionInBucket(null, TimeBucket.Under6h, Now));
            Assert.False(IsPositionInBucket(null, TimeBucket.Under24h, Now));
            Assert.False(IsPositionInBucket(null, TimeBucket.Under72h, Now));
        }

        [Theory]
        [InlineData(3, TimeBucket.Under6h, true)]
        [InlineData(3, TimeBucket.Under24h, false)]
        [InlineData(12, TimeBucket.Under24h, true)]
        [InlineData(12, TimeBucket.Under6h, false)]
        [InlineData(48, TimeBucket.Under72h, true)]
        [InlineData(48, TimeBucket.Under24h, false)]
        [InlineData(100, TimeBucket.Over72h, true)]
        [InlineData(100, TimeBucket.Under72h, false)]
        public void PositionInBucket_classifies_by_hours_to_expiry(int hoursToExpiry, TimeBucket bucket, bool expected)
        {
            var expiry = Now.AddHours(hoursToExpiry);
            Assert.Equal(expected, IsPositionInBucket(expiry, bucket, Now));
        }

        [Fact]
        public void PositionInBucket_boundaries_are_consistent_with_engine()
        {
            // exakt 6h: nicht < 6h, aber in < 24h
            Assert.False(IsPositionInBucket(Now.AddHours(6), TimeBucket.Under6h, Now));
            Assert.True(IsPositionInBucket(Now.AddHours(6), TimeBucket.Under24h, Now));
            // exakt 72h: nicht < 72h, aber > 72h
            Assert.False(IsPositionInBucket(Now.AddHours(72), TimeBucket.Under72h, Now));
            Assert.True(IsPositionInBucket(Now.AddHours(72), TimeBucket.Over72h, Now));
        }

        [Fact]
        public void PositionInBucket_expired_position_counts_as_under6h()
        {
            Assert.True(IsPositionInBucket(Now.AddHours(-5), TimeBucket.Under6h, Now));
            Assert.False(IsPositionInBucket(Now.AddHours(-5), TimeBucket.Over72h, Now));
        }

        // ---------------- MaxPerMarket ----------------

        [Fact]
        public void MaxPerMarket_normal_balance_uses_percentage()
        {
            // 1000 * 5 % = 50
            Assert.Equal(50m, MaxPerMarket(totalBalance: 1000m, availableBalance: 800m, perMarketLimitPct: 5m));
        }

        [Fact]
        public void MaxPerMarket_tiny_balance_below_150_capped_at_1_20()
        {
            Assert.Equal(1.20m, MaxPerMarket(100m, 800m, 5m));
            // von verfügbarem Guthaben begrenzt
            Assert.Equal(0.5m, MaxPerMarket(100m, 0.5m, 5m));
        }

        [Fact]
        public void MaxPerMarket_below_500_capped_at_3()
        {
            Assert.Equal(3.0m, MaxPerMarket(300m, 200m, 5m));
            Assert.Equal(1m, MaxPerMarket(300m, 1m, 5m));
        }

        [Fact]
        public void MaxPerMarket_negative_available_clamps_to_zero()
        {
            Assert.Equal(0m, MaxPerMarket(100m, -20m, 5m));
        }

        [Theory]
        [InlineData(150, 3.0)]   // genau 150 -> Stufe < 500
        [InlineData(500, 25.0)]  // genau 500 -> normale Prozentregel (500*5%)
        public void MaxPerMarket_tier_boundaries(double balance, double expected)
        {
            // available großzügig, damit die Stufen-Caps nicht durch Verfügbarkeit greifen
            Assert.Equal((decimal)expected, MaxPerMarket((decimal)balance, 100000m, 5m));
        }

        // ---------------- IsBelowPolymarketMinimum ----------------

        [Theory]
        [InlineData(5.5, 1.0, false)]   // genau an der Share-Grenze -> ok
        [InlineData(5.4, 1.0, true)]    // zu wenige Shares
        [InlineData(6.0, 0.10, false)]  // genau an der USDC-Grenze -> ok
        [InlineData(6.0, 0.09, true)]   // zu wenig USDC
        [InlineData(100.0, 50.0, false)]
        public void IsBelowPolymarketMinimum_enforces_share_and_usdc_floor(double shares, double usdc, bool expected)
        {
            Assert.Equal(expected, IsBelowPolymarketMinimum((decimal)shares, (decimal)usdc));
        }

        // ---------------- IsAuthorizedSell (K3) ----------------

        [Theory]
        [InlineData(0, 7, true)]    // System-Signal schließt fremde Copy-Position (Demo-Resolution)
        [InlineData(0, 0, true)]    // System-Signal, keine Herkunft
        [InlineData(7, 7, true)]    // Master schließt eigene Position
        [InlineData(7, 9, false)]   // Master 7 darf Position von Master 9 nicht schließen
        [InlineData(9, 0, false)]   // echter Master darf herrenlose Position nicht kapern
        public void IsAuthorizedSell_allows_owner_and_system(int signalTraderId, int positionSourceTraderId, bool expected)
        {
            Assert.Equal(expected, IsAuthorizedSell(signalTraderId, positionSourceTraderId));
        }
    }
}
