using KArtSell.Modules.SignalEngine.Domain;
using KArtSell.Modules.SignalEngine.Domain.Policies;

namespace KArtSell.SignalEngine.UnitTests;

public sealed class SellPolicyChainTests
{
    private static SellPolicyChain CreateSut() => new(new ISellPolicy[]
    {
        new OpportunityCostPolicy(),
        new ConcentrationLiquidityPolicy(),
        new TwoCloseFloorBreachPolicy(),
        new GapFloorBreachPolicy(),
        new PortfolioSurvivalPolicy(),
        new HardImpairmentPolicy()
    });

    [Fact]
    public void Hard_impairment_cannot_be_overridden_by_lower_priority_policy()
    {
        var input = BaseInput() with
        {
            HardImpairmentApproved = true,
            CapitalFloorBreached = true,
            GapBelowFloorAtr = 2.0m,
            ConsecutiveCloseBreaches = 3,
            OpportunityEdgeLowerBound = 0.10m,
            OpportunitySellRatioOfLot = 0.25m
        };

        var result = CreateSut().Evaluate(input);

        Assert.Equal("ALG-SELL-001", result.PolicyId);
        Assert.Equal(SellAction.FullSell, result.Action);
        Assert.Equal(1m, result.SellRatioOfLot);
        Assert.False(result.ReentryEligible);
        Assert.Single(result.PolicyTrace);
        Assert.Equal(PolicyDisposition.Applied, result.PolicyTrace[0].Disposition);
    }

    [Fact]
    public void Portfolio_survival_outranks_profit_floor_and_may_cross_strategic_core()
    {
        var input = BaseInput() with
        {
            CurrentSecurityPortfolioWeight = 0.40m,
            CurrentLotPortfolioWeight = 0.20m,
            StrategicCoreFloorWeight = 0.30m,
            CapitalFloorBreached = true,
            SurvivalSellRatioOfLot = 0.75m,
            GapBelowFloorAtr = 2.0m
        };

        var result = CreateSut().Evaluate(input);

        Assert.Equal("ALG-SELL-PORT-001", result.PolicyId);
        Assert.Equal(0.75m, result.SellRatioOfLot);
        Assert.Equal(0.25m, result.TargetSecurityPortfolioWeightAfter);
    }

    [Fact]
    public void Lot_relative_ratio_uses_lot_weight_not_whole_security_weight()
    {
        var input = BaseInput() with
        {
            CurrentSecurityPortfolioWeight = 0.60m,
            CurrentLotPortfolioWeight = 0.20m,
            StrategicCoreFloorWeight = 0.50m,
            GapBelowFloorAtr = 2.0m
        };

        var result = CreateSut().Evaluate(input);

        Assert.Equal("ALG-SELL-002", result.PolicyId);
        Assert.Equal(0.40m, result.SellRatioOfLot);
        Assert.Equal(0.52m, result.TargetSecurityPortfolioWeightAfter);
        Assert.False(result.PolicyTrace.Single(x => x.PolicyId == "ALG-SELL-002").StrategicCoreClampApplied);
    }

    [Fact]
    public void Strategic_core_clamps_lot_ratio_when_only_part_of_lot_is_sellable()
    {
        var input = BaseInput() with
        {
            CurrentSecurityPortfolioWeight = 0.60m,
            CurrentLotPortfolioWeight = 0.40m,
            StrategicCoreFloorWeight = 0.50m,
            GapBelowFloorAtr = 2.0m
        };

        var result = CreateSut().Evaluate(input);

        Assert.Equal(0.25m, result.SellRatioOfLot);
        Assert.Equal(0.50m, result.TargetSecurityPortfolioWeightAfter);
        Assert.True(result.PolicyTrace.Single(x => x.PolicyId == "ALG-SELL-002").StrategicCoreClampApplied);
    }

    [Fact]
    public void Opportunity_sell_requires_positive_lower_confidence_edge()
    {
        var blocked = CreateSut().Evaluate(BaseInput() with
        {
            OpportunityEdgeLowerBound = 0m,
            OpportunitySellRatioOfLot = 0.20m
        });

        var allowed = CreateSut().Evaluate(BaseInput() with
        {
            OpportunityEdgeLowerBound = 0.01m,
            OpportunitySellRatioOfLot = 0.20m
        });

        Assert.Equal(SellAction.Hold, blocked.Action);
        Assert.Equal("ALG-SELL-005", allowed.PolicyId);
        Assert.Equal(0.20m, allowed.SellRatioOfLot);
    }

    [Fact]
    public void Positive_opportunity_edge_with_zero_requested_ratio_cannot_create_a_sell()
    {
        var result = CreateSut().Evaluate(BaseInput() with
        {
            OpportunityEdgeLowerBound = 0.02m,
            OpportunitySellRatioOfLot = 0m
        });

        Assert.Equal(SellAction.Hold, result.Action);
        var trace = Assert.Single(result.PolicyTrace, x => x.PolicyId == SellPolicyContract.OpportunityCostPolicyId);
        Assert.Equal(PolicyDisposition.Blocked, trace.Disposition);
        Assert.Equal("OPPORTUNITY_RATIO_NOT_POSITIVE", trace.ReasonCode);
    }

    [Fact]
    public void Future_published_evidence_is_rejected()
    {
        var input = BaseInput() with
        {
            PublishedAtCutoff = DateTimeOffset.Parse("2026-08-01T08:00:00Z"),
            AsOf = DateTimeOffset.Parse("2026-08-01T07:00:00Z")
        };

        Assert.Throws<InvalidOperationException>(() => CreateSut().Evaluate(input));
    }

    private static SellDecisionInput BaseInput() => new(
        Guid.Parse("00000000-0000-0000-0000-000000000001"),
        Guid.Parse("00000000-0000-0000-0000-000000000002"),
        "evidence-1",
        "dataset-1",
        "model-1",
        "config-1",
        "code-sha-1",
        DateTimeOffset.Parse("2026-08-01T07:00:00Z"),
        DateTimeOffset.Parse("2026-08-01T06:00:00Z"),
        0.60m,
        0.20m,
        0.30m,
        false,
        false,
        0m,
        0m,
        0,
        true,
        0m,
        0m,
        0m);
}
