using Theriapolis.Core;
using Theriapolis.Core.Data;
using Theriapolis.Core.Rules.Reputation;
using Theriapolis.Core.World;
using Xunit;

namespace Theriapolis.Tests.Reputation;

/// <summary>
/// Phase 6 M5 — propagation correctness: distance-band decay, opposition
/// cascade, frontier coin-flip determinism, NEMESIS/CHAMPION bypass.
/// </summary>
public sealed class RepPropagationTests
{
    private static IReadOnlyDictionary<string, FactionDef> Factions()
        => new ContentResolver(new ContentLoader(TestHelpers.DataDirectory)).Factions;

    private static Settlement Sett(int id, int x, int y) => new()
    {
        Id    = id,
        Name  = $"S{id}",
        Tier  = 3,
        TileX = x,
        TileY = y,
    };

    [Fact]
    public void BandFor_MapsTilesToBands()
    {
        Assert.Equal(RepPropagation.DistanceBand.Origin,      RepPropagation.BandFor(0));
        Assert.Equal(RepPropagation.DistanceBand.Adjacent,    RepPropagation.BandFor(1));
        Assert.Equal(RepPropagation.DistanceBand.Adjacent,    RepPropagation.BandFor(C.REP_ADJACENT_DIST_TILES));
        Assert.Equal(RepPropagation.DistanceBand.Regional,    RepPropagation.BandFor(C.REP_ADJACENT_DIST_TILES + 1));
        Assert.Equal(RepPropagation.DistanceBand.Regional,    RepPropagation.BandFor(C.REP_REGIONAL_DIST_TILES));
        Assert.Equal(RepPropagation.DistanceBand.Continental, RepPropagation.BandFor(C.REP_REGIONAL_DIST_TILES + 1));
        Assert.Equal(RepPropagation.DistanceBand.Continental, RepPropagation.BandFor(C.REP_CONTINENTAL_DIST_TILES));
        Assert.Equal(RepPropagation.DistanceBand.Frontier,    RepPropagation.BandFor(C.REP_CONTINENTAL_DIST_TILES + 1));
    }

    [Fact]
    public void DecayPctFor_HasMonotonicallyDecreasingValues()
    {
        Assert.True(RepPropagation.DecayPctFor(RepPropagation.DistanceBand.Origin) >
                    RepPropagation.DecayPctFor(RepPropagation.DistanceBand.Adjacent));
        Assert.True(RepPropagation.DecayPctFor(RepPropagation.DistanceBand.Adjacent) >
                    RepPropagation.DecayPctFor(RepPropagation.DistanceBand.Regional));
        Assert.True(RepPropagation.DecayPctFor(RepPropagation.DistanceBand.Regional) >
                    RepPropagation.DecayPctFor(RepPropagation.DistanceBand.Continental));
        Assert.True(RepPropagation.DecayPctFor(RepPropagation.DistanceBand.Continental) >
                    RepPropagation.DecayPctFor(RepPropagation.DistanceBand.Frontier));
    }

    [Fact]
    public void LocalStanding_AtOrigin_FullMagnitude()
    {
        var ledger = new RepLedger();
        ledger.Append(new RepEvent
        {
            FactionId = "inheritors",
            Magnitude = 20,
            OriginTileX = 100, OriginTileY = 100,
        });
        var s = Sett(1, 100, 100);
        Assert.Equal(20, RepPropagation.LocalStandingFor("inheritors", s, 0xCAFEUL, ledger, Factions()));
    }

    [Fact]
    public void LocalStanding_DecaysWithDistance()
    {
        var f = Factions();
        var ledger = new RepLedger();
        ledger.Append(new RepEvent
        {
            FactionId = "inheritors",
            Magnitude = 50,                  // (just under the bypass threshold)
            OriginTileX = 100, OriginTileY = 100,
        });
        // Adjacent: 80% of 50 = 40
        // Wait — 50 is exactly at threshold; let's use 49 to test decay.
        ledger = new RepLedger();
        ledger.Append(new RepEvent
        {
            FactionId = "inheritors",
            Magnitude = 49,
            OriginTileX = 100, OriginTileY = 100,
        });
        int origin     = RepPropagation.LocalStandingFor("inheritors", Sett(1, 100, 100), 0xCAFEUL, ledger, f);
        int adjacent   = RepPropagation.LocalStandingFor("inheritors", Sett(2, 110, 100), 0xCAFEUL, ledger, f);   // 10 tiles
        int regional   = RepPropagation.LocalStandingFor("inheritors", Sett(3, 150, 100), 0xCAFEUL, ledger, f);   // 50 tiles
        int continental= RepPropagation.LocalStandingFor("inheritors", Sett(4, 250, 100), 0xCAFEUL, ledger, f);   // 150 tiles

        Assert.Equal(49, origin);
        Assert.Equal((int)System.Math.Round(49 * 0.80f), adjacent);
        Assert.Equal((int)System.Math.Round(49 * 0.60f), regional);
        Assert.Equal((int)System.Math.Round(49 * 0.40f), continental);
    }

    [Fact]
    public void Cascade_AppliesOppositionMatrix()
    {
        // Player gains +20 with Inheritors at (100, 100). The Enforcers
        // hate this (mult -0.5) → -10 should cascade to Enforcer standing
        // even at origin.
        var f = Factions();
        var ledger = new RepLedger();
        ledger.Append(new RepEvent
        {
            FactionId = "inheritors",
            Magnitude = 20,
            OriginTileX = 100, OriginTileY = 100,
        });
        int enforcerLocal = RepPropagation.LocalStandingFor("covenant_enforcers",
            Sett(1, 100, 100), 0xCAFEUL, ledger, f);
        Assert.Equal(-10, enforcerLocal);
    }

    [Fact]
    public void ExtremeMagnitude_BypassesDecay()
    {
        var f = Factions();
        var ledger = new RepLedger();
        ledger.Append(new RepEvent
        {
            FactionId = "inheritors",
            Magnitude = 60,    // ≥ REP_EXTREME_BYPASS_MAGNITUDE
            OriginTileX = 0, OriginTileY = 0,
        });
        // Settlement on the frontier (>200 tiles away).
        int far = RepPropagation.LocalStandingFor("inheritors",
            Sett(99, 250, 250), 0xCAFEUL, ledger, f);
        Assert.Equal(60, far);
    }

    [Fact]
    public void FrontierDelivered_IsDeterministic()
    {
        // Same seed, same event id, same settlement id → same answer.
        bool a = RepPropagation.FrontierDelivered(0xCAFEUL, 1, 5);
        bool b = RepPropagation.FrontierDelivered(0xCAFEUL, 1, 5);
        Assert.Equal(a, b);

        // Vary one input → may differ; just confirm we don't always hit one branch.
        int trues = 0, falses = 0;
        for (int i = 1; i <= 100; i++)
        {
            if (RepPropagation.FrontierDelivered(0xCAFEUL, i, 5)) trues++;
            else falses++;
        }
        Assert.True(trues > 20 && falses > 20,
            $"Expected roughly 50/50 distribution; got trues={trues}, falses={falses}");
    }

    [Fact]
    public void FrontierEvent_OnlyAppliesWhenDelivered()
    {
        var f = Factions();
        var ledger = new RepLedger();
        ledger.Append(new RepEvent
        {
            SequenceId = 0,    // assigned by Append → 1
            FactionId = "inheritors",
            Magnitude = 20,
            OriginTileX = 0, OriginTileY = 0,
        });
        // Frontier settlement.
        var s = Sett(1, 250, 250);
        int got = RepPropagation.LocalStandingFor("inheritors", s, 0xCAFEUL, ledger, f);
        // Either 0 (not delivered) or +4 (20 × 20%).
        Assert.True(got == 0 || got == (int)System.Math.Round(20 * 0.20f),
            $"frontier delivery should be 0 or {System.Math.Round(20 * 0.20f)}, got {got}");
    }

    [Fact]
    public void ExplainLocalStanding_ReturnsRecentEvents()
    {
        var f = Factions();
        var ledger = new RepLedger();
        ledger.Append(new RepEvent
        {
            FactionId = "inheritors", Magnitude = 10, Note = "first",
            OriginTileX = 100, OriginTileY = 100,
        });
        ledger.Append(new RepEvent
        {
            FactionId = "inheritors", Magnitude = -5, Note = "second",
            OriginTileX = 100, OriginTileY = 100,
        });
        var s = Sett(1, 100, 100);
        var explained = RepPropagation.ExplainLocalStanding("inheritors", s, 0xCAFEUL, ledger, f, max: 8).ToList();
        Assert.Equal(2, explained.Count);
        // Most-recent-first.
        Assert.Equal("second", explained[0].Event.Note);
        Assert.Equal("first",  explained[1].Event.Note);
    }
}
