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

namespace Theriapolis.Tests.Reputation;

/// <summary>
/// Phase 6.5 M7 — betrayal cascade: tier mapping, faction propagation
/// through the opposition matrix, permanent memory tag, sticky aggro
/// flag on guard-style NPCs in the betrayed faction.
/// </summary>
public sealed class BetrayalCascadeTests
{
    private readonly ContentResolver _content = new(new ContentLoader(TestHelpers.DataDirectory));

    // ── Tier resolution ───────────────────────────────────────────────────

    [Theory]
    [InlineData(0,      0)]    // not a betrayal
    [InlineData(-5,     0)]    // sub-minor → no faction cascade
    [InlineData(-10,   -5)]    // minor
    [InlineData(-24,   -5)]    // still minor
    [InlineData(-25,  -15)]    // moderate
    [InlineData(-49,  -15)]    // still moderate
    [InlineData(-50,  -30)]    // major
    [InlineData(-74,  -30)]    // still major
    [InlineData(-75,  -50)]    // critical
    [InlineData(-100, -50)]    // floor
    public void ResolveFactionDelta_TiersByMagnitude(int magnitude, int expected)
    {
        Assert.Equal(expected, BetrayalCascade.ResolveFactionDelta(magnitude));
    }

    // ── Apply: cascade outcomes ───────────────────────────────────────────

    [Fact]
    public void Apply_NonBetrayalEvent_ReturnsEmpty()
    {
        var rep = new PlayerReputation();
        var ev  = MakeBetrayalEvent("test.role", magnitude: 0);   // magnitude 0 = not a betrayal
        var result = BetrayalCascade.Apply(ev, rep, betrayedNpc: null,
            npcs: System.Linq.Enumerable.Empty<NpcActor>(), factions: _content.Factions);
        Assert.True(result.IsEmpty);
        Assert.Empty(rep.Ledger.Entries);
    }

    [Fact]
    public void Apply_PositiveMagnitude_NoCascade()
    {
        var rep = new PlayerReputation();
        var ev  = MakeBetrayalEvent("test.role", magnitude: 10);  // positive — not a betrayal
        var result = BetrayalCascade.Apply(ev, rep, betrayedNpc: null,
            npcs: System.Linq.Enumerable.Empty<NpcActor>(), factions: _content.Factions);
        Assert.True(result.IsEmpty);
    }

    [Fact]
    public void Apply_WritesBetrayedMeMemoryFlag()
    {
        var rep = new PlayerReputation();
        var ev  = MakeBetrayalEvent("millhaven.asha", magnitude: -25);
        BetrayalCascade.Apply(ev, rep, betrayedNpc: null,
            npcs: System.Linq.Enumerable.Empty<NpcActor>(), factions: _content.Factions);
        Assert.Contains("betrayed_me", rep.PersonalFor("millhaven.asha").Memory);
    }

    [Fact]
    public void Apply_AppliesFactionDeltaFromBetrayedNpc()
    {
        var rep = new PlayerReputation();
        var asha = MakeNpc(faction: "hybrid_underground", behavior: "resident");
        var ev   = MakeBetrayalEvent("millhaven.asha", magnitude: -25);

        var result = BetrayalCascade.Apply(ev, rep, asha,
            npcs: System.Linq.Enumerable.Empty<NpcActor>(), factions: _content.Factions);

        Assert.Equal("hybrid_underground", result.factionId);
        // Hybrid_underground: -15 (moderate tier).
        Assert.Equal(-15, rep.Factions.Get("hybrid_underground"));
        // Opposition cascade: hybrid_underground hates inheritors (-0.5),
        // so -15 with hybrid_underground → +7 with inheritors (negative ×
        // negative). Sign check.
        int inheritors = rep.Factions.Get("inheritors");
        Assert.True(inheritors > 0, $"expected positive inheritor delta from cascade, got {inheritors}");
    }

    [Fact]
    public void Apply_LedgerRecordsCascadeAsFactionTaggedEvent()
    {
        var rep = new PlayerReputation();
        var asha = MakeNpc(faction: "hybrid_underground", behavior: "resident");
        var ev   = MakeBetrayalEvent("millhaven.asha", magnitude: -50);

        BetrayalCascade.Apply(ev, rep, asha,
            npcs: System.Linq.Enumerable.Empty<NpcActor>(), factions: _content.Factions);

        Assert.Contains(rep.Ledger.Entries,
            e => e.Kind == RepEventKind.Betrayal
              && e.FactionId == "hybrid_underground"
              && e.RoleTag == "millhaven.asha");
    }

    [Fact]
    public void Apply_NoFactionCascadeWhenNpcAndEventBothLackFaction()
    {
        var rep = new PlayerReputation();
        var npc = MakeNpc(faction: "", behavior: "brigand");
        var ev  = MakeBetrayalEvent("test.role", magnitude: -25);

        var result = BetrayalCascade.Apply(ev, rep, npc,
            npcs: new[] { npc }, factions: _content.Factions);

        // Personal flag still set, but no faction propagation.
        Assert.Contains("betrayed_me", rep.PersonalFor("test.role").Memory);
        Assert.Equal("", result.factionId);
        Assert.Empty(result.factionDeltas);
    }

    // ── Apply: permanent aggro flip ───────────────────────────────────────

    [Fact]
    public void Apply_FlipsPermanentAggroOnSameFactionGuards()
    {
        var rep = new PlayerReputation();
        var betrayed = MakeNpc(faction: "covenant_enforcers", behavior: "patrol");
        var otherGuard = MakeNpc(faction: "covenant_enforcers", behavior: "patrol");
        var unrelated  = MakeNpc(faction: "merchant_guilds",    behavior: "patrol");

        var ev = MakeBetrayalEvent("guard.captain", magnitude: -50);
        var result = BetrayalCascade.Apply(ev, rep, betrayed,
            npcs: new[] { betrayed, otherGuard, unrelated }, factions: _content.Factions);

        Assert.True(otherGuard.PermanentAggroAfterBetrayal);
        Assert.False(unrelated.PermanentAggroAfterBetrayal);  // wrong faction
        Assert.True(result.permanentAggroFlipped >= 1);
    }

    [Fact]
    public void Apply_DoesNotFlipCivilianResidents()
    {
        var rep = new PlayerReputation();
        var betrayed = MakeNpc(faction: "merchant_guilds", behavior: "resident");
        var civilianTrader = MakeNpc(faction: "merchant_guilds", behavior: "resident");

        var ev = MakeBetrayalEvent("guild.master", magnitude: -50);
        BetrayalCascade.Apply(ev, rep, betrayed,
            npcs: new[] { betrayed, civilianTrader }, factions: _content.Factions);

        // Civilians stay non-aggro even on betrayal — only guard-style behaviors flip.
        Assert.False(civilianTrader.PermanentAggroAfterBetrayal);
    }

    [Theory]
    [InlineData("brigand")]
    [InlineData("patrol")]
    [InlineData("poi_guard")]
    [InlineData("wild_animal")]
    public void Apply_FlipsAggroForCombatBehaviors(string behavior)
    {
        var rep = new PlayerReputation();
        var betrayed = MakeNpc(faction: "inheritors", behavior: behavior);
        var same     = MakeNpc(faction: "inheritors", behavior: behavior);

        var ev = MakeBetrayalEvent("inheritor.captain", magnitude: -50);
        BetrayalCascade.Apply(ev, rep, betrayed,
            npcs: new[] { betrayed, same }, factions: _content.Factions);

        Assert.True(same.PermanentAggroAfterBetrayal);
    }

    [Fact]
    public void Apply_DoesNotReFlipAlreadyAggroedNpc()
    {
        var rep = new PlayerReputation();
        var betrayed = MakeNpc(faction: "inheritors", behavior: "patrol");
        var preFlipped = MakeNpc(faction: "inheritors", behavior: "patrol");
        preFlipped.PermanentAggroAfterBetrayal = true;

        var ev = MakeBetrayalEvent("inheritor.scout", magnitude: -25);
        var result = BetrayalCascade.Apply(ev, rep, betrayed,
            npcs: new[] { betrayed, preFlipped }, factions: _content.Factions);

        // betrayed gets flipped (it's the same-faction guard), but
        // preFlipped is already-aggro and shouldn't double-count.
        Assert.True(betrayed.PermanentAggroAfterBetrayal);
        Assert.True(preFlipped.PermanentAggroAfterBetrayal);
        // Only `betrayed` flipped fresh in this call.
        Assert.Equal(1, result.permanentAggroFlipped);
    }

    // ── FactionAggression integration ─────────────────────────────────────

    [Fact]
    public void FactionAggression_FlipsAllegiance_OnPermanentAggroFlag()
    {
        // Simulate: a friendly resident with the betrayal aggro flag set
        // (no need for a hostile faction standing). FactionAggression
        // should still flip them to Hostile.
        var npc = MakeNpc(faction: "hybrid_underground", behavior: "resident");
        npc.Allegiance = Theriapolis.Core.Rules.Character.Allegiance.Friendly;
        npc.PermanentAggroAfterBetrayal = true;

        // FactionAggression.UpdateAllegiances takes an ActorManager + content
        // + world. Build minimal fixtures.
        var actors = new ActorManager();
        actors.SpawnNpc(npc);
        // No PC needed for the early-return null check; pass a stub character.
        var pc = MakeStubPc();

        // Empty WorldState — UpdateAllegiances handles missing settlements gracefully.
        var world = new Theriapolis.Core.World.WorldState();
        var rep   = new PlayerReputation();

        int flipped = FactionAggression.UpdateAllegiances(actors, pc, rep, _content, world, 0xCAFEUL);
        Assert.True(flipped >= 1);
        Assert.Equal(Theriapolis.Core.Rules.Character.Allegiance.Hostile, npc.Allegiance);
    }

    // ── Helpers ───────────────────────────────────────────────────────────

    private static RepEvent MakeBetrayalEvent(string roleTag, int magnitude) => new()
    {
        Kind             = RepEventKind.Betrayal,
        RoleTag          = roleTag,
        Magnitude        = magnitude,
        Note             = "test betrayal",
        TimestampSeconds = 1000,
    };

    // Monotone counter so every NPC gets a positive Id — needed because
    // ActorManager.SpawnNpc clones (and resets initialization-time flags)
    // when Id ≤ 0.
    private static int _nextNpcId = 1000;

    private static NpcActor MakeNpc(string faction, string behavior)
    {
        // Use NpcTemplateDef so we can set an arbitrary Behavior id.
        var template = new NpcTemplateDef
        {
            Id      = "test_" + behavior,
            Name    = "Test NPC",
            Hp      = 20,
            Behavior = behavior,
            Faction = faction,
            DefaultAllegiance = "neutral",
        };
        return new NpcActor(template) { Id = ++_nextNpcId };
    }

    private Theriapolis.Core.Rules.Character.Character MakeStubPc()
    {
        var b = new Theriapolis.Core.Rules.Character.CharacterBuilder
        {
            Clade        = _content.Clades["canidae"],
            Species      = _content.Species["wolf"],
            ClassDef     = _content.Classes["fangsworn"],
            Background   = _content.Backgrounds["pack_raised"],
            BaseAbilities = new Theriapolis.Core.Rules.Stats.AbilityScores(15, 14, 13, 12, 10, 8),
        };
        int n = b.ClassDef.SkillsChoose;
        foreach (var raw in b.ClassDef.SkillOptions)
        {
            if (b.ChosenClassSkills.Count >= n) break;
            try { b.ChosenClassSkills.Add(Theriapolis.Core.Rules.Stats.SkillIdExtensions.FromJson(raw)); } catch { }
        }
        return b.Build(_content.Items);
    }
}
