using Theriapolis.Core;
using Theriapolis.Core.Data;
using Theriapolis.Core.Rules.Reputation;
using Theriapolis.Core.World;
using Xunit;
namespace Theriapolis.Tests.Reputation;
///
/// Phase 6 M5 — propagation correctness: distance-band decay, opposition
/// cascade, frontier coin-flip determinism, NEMESIS/CHAMPION bypass.
///
public sealed class RepPropagationTests
{
private static IReadOnlyDictionary 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);
}
}