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

namespace Theriapolis.Tests.Rules;

/// <summary>
/// Phase 6.5 M5 — passing detection. Hybrid PCs with PassingActive get a
/// scent-detection roll on encountering scent-capable NPCs. The result is
/// permanent per-NPC (cached in Hybrid.NpcsWhoKnow + the NPC's
/// PersonalDisposition.Memory). Once detected, EffectiveDisposition layers
/// in the NPC's BiasProfile.HybridBias.
/// </summary>
public sealed class PassingDetectionTests
{
    private readonly ContentResolver _content = new(new ContentLoader(TestHelpers.DataDirectory));

    // ── Roll outcomes ─────────────────────────────────────────────────────

    [Fact]
    public void Roll_NotApplicable_ForPurebredPc()
    {
        var pc = MakePurebred();
        var npc = MakeCanidNpc();
        var result = PassingCheck.Roll(pc, npc, new HashSet<string>(), seed: 0xCAFE);
        Assert.Equal(DetectionResult.NotApplicable, result);
    }

    [Fact]
    public void Roll_PreviouslyDetected_NoFreshRoll()
    {
        var pc = MakeHybrid(passing: true);
        var npc = MakeCanidNpc();
        var memory = new HashSet<string> { "knows_hybrid" };
        var result = PassingCheck.Roll(pc, npc, memory, seed: 0xCAFE);
        Assert.Equal(DetectionResult.PreviouslyDetected, result);
    }

    [Fact]
    public void Roll_NotPassing_AutoDetected()
    {
        var pc = MakeHybrid(passing: false);
        var npc = MakeCanidNpc();
        var result = PassingCheck.Roll(pc, npc, new HashSet<string>(), seed: 0xCAFE);
        Assert.Equal(DetectionResult.NotPassing, result);
    }

    [Fact]
    public void Roll_DeepCoverMask_AlwaysSuppresses()
    {
        var pc = MakeHybrid(passing: true);
        pc.Hybrid!.ActiveMaskTier = ScentMaskTier.DeepCover;
        var npc = MakeCanidNpc();
        // Even Canid Superior Scent fails against deep cover.
        var result = PassingCheck.Roll(pc, npc, new HashSet<string>(), seed: 0xCAFE);
        Assert.Equal(DetectionResult.MaskSuppressed, result);
    }

    [Fact]
    public void Roll_MilitaryMask_SuppressesNonCanid()
    {
        var pc = MakeHybrid(passing: true);
        pc.Hybrid!.ActiveMaskTier = ScentMaskTier.Military;
        // M5 simplification: only Canid NPCs detect scent. Test the path
        // by giving the NPC a non-Canid clade — military mask suppresses
        // automatically for non-superior-scent NPCs.
        var npc = MakeNonCanidNpc();
        var result = PassingCheck.Roll(pc, npc, new HashSet<string>(), seed: 0xCAFE);
        // Non-Canid NPCs lack scent capability anyway, so result is NoCapability.
        // Military mask short-circuits earlier with MaskSuppressed.
        Assert.Equal(DetectionResult.MaskSuppressed, result);
    }

    [Fact]
    public void Roll_NoCapability_ForNonScentNpc()
    {
        var pc = MakeHybrid(passing: true);
        var npc = MakeNonCanidNpc();
        var result = PassingCheck.Roll(pc, npc, new HashSet<string>(), seed: 0xCAFE);
        Assert.Equal(DetectionResult.NoCapability, result);
    }

    [Fact]
    public void Roll_IsDeterministic_ForSameSeed()
    {
        var pc = MakeHybrid(passing: true);
        var npc = MakeCanidNpc();
        // Use a fresh memory set each time so PreviouslyDetected doesn't
        // short-circuit.
        var a = PassingCheck.Roll(pc, npc, new HashSet<string>(), seed: 0x1234);
        var b = PassingCheck.Roll(pc, npc, new HashSet<string>(), seed: 0x1234);
        Assert.Equal(a, b);
    }

    [Fact]
    public void Roll_DifferentSeeds_CanProduceDifferentOutcomes()
    {
        var pc = MakeHybrid(passing: true);
        var npc = MakeCanidNpc();
        // Sweep 50 seeds — at least one should differ from the first
        // (probabilistic detection).
        var first = PassingCheck.Roll(pc, npc, new HashSet<string>(), seed: 1UL);
        bool sawDifferent = false;
        for (ulong s = 2; s <= 50; s++)
        {
            var r = PassingCheck.Roll(pc, npc, new HashSet<string>(), seed: s);
            if (r != first) { sawDifferent = true; break; }
        }
        Assert.True(sawDifferent, "expected some seed variance in detection outcomes");
    }

    [Fact]
    public void Roll_BasicMaskFavoursPc()
    {
        // With a basic mask, the PC's Deception roll gets +5. Sweep many
        // seeds and verify that masked rolls produce *more* Pass outcomes
        // than unmasked rolls on average.
        var pcMasked = MakeHybrid(passing: true);
        pcMasked.Hybrid!.ActiveMaskTier = ScentMaskTier.Basic;
        var pcUnmasked = MakeHybrid(passing: true);
        var npc = MakeCanidNpc();

        int maskedPasses   = 0;
        int unmaskedPasses = 0;
        for (ulong s = 1; s <= 200; s++)
        {
            if (PassingCheck.Roll(pcMasked,   npc, new HashSet<string>(), seed: s) == DetectionResult.Pass)
                maskedPasses++;
            if (PassingCheck.Roll(pcUnmasked, npc, new HashSet<string>(), seed: s) == DetectionResult.Pass)
                unmaskedPasses++;
        }
        Assert.True(maskedPasses > unmaskedPasses,
            $"basic mask should help: masked passes={maskedPasses}, unmasked passes={unmaskedPasses}");
    }

    // ── RollAndApply side effects ─────────────────────────────────────────

    [Fact]
    public void RollAndApply_NotPassing_WritesMemoryAndLedger()
    {
        var pc  = MakeHybrid(passing: false);
        var npc = MakeCanidNpcWithRole("test.canid");
        var rep = new PlayerReputation();

        var result = PassingCheck.RollAndApply(pc, npc, rep,
            worldClockSeconds: 100L, seed: 0xCAFE);

        Assert.Equal(DetectionResult.NotPassing, result);
        Assert.Contains(npc.Id, pc.Hybrid!.NpcsWhoKnow);
        Assert.Contains("knows_hybrid", rep.PersonalFor("test.canid").Memory);
        Assert.Contains(rep.Ledger.Entries,
            ev => ev.Kind == RepEventKind.HybridDetected && ev.RoleTag == "test.canid");
    }

    [Fact]
    public void RollAndApply_PreviouslyDetected_DoesNotReWrite()
    {
        var pc  = MakeHybrid(passing: true);
        var npc = MakeCanidNpcWithRole("test.canid");
        var rep = new PlayerReputation();
        // Pre-seed memory to look like a prior detection.
        rep.PersonalFor("test.canid").Memory.Add("knows_hybrid");

        var result = PassingCheck.RollAndApply(pc, npc, rep,
            worldClockSeconds: 100L, seed: 0xCAFE);

        Assert.Equal(DetectionResult.PreviouslyDetected, result);
        // No new HybridDetected event added (only the pre-existing memory tag).
        Assert.DoesNotContain(rep.Ledger.Entries,
            ev => ev.Kind == RepEventKind.HybridDetected);
    }

    [Fact]
    public void RollAndApply_NotApplicable_NoSideEffects_ForPurebred()
    {
        var pc  = MakePurebred();
        var npc = MakeCanidNpcWithRole("test.canid");
        var rep = new PlayerReputation();

        var result = PassingCheck.RollAndApply(pc, npc, rep,
            worldClockSeconds: 100L, seed: 0xCAFE);

        Assert.Equal(DetectionResult.NotApplicable, result);
        Assert.Empty(rep.Ledger.Entries);
        Assert.False(rep.Personal.ContainsKey("test.canid"));
    }

    // ── EffectiveDisposition + HybridBias consumption ────────────────────

    [Fact]
    public void EffectiveDisposition_DoesNotApplyHybridBias_BeforeDetection()
    {
        var pc  = MakeHybrid(passing: true);
        var npc = MakeNpcWithBiasProfile("CERVID_CAUTIOUS");
        var rep = new PlayerReputation();
        // No detection yet — hybrid bias should not be in the disposition.
        int beforeDisposition = EffectiveDisposition.For(npc, pc, rep, _content);
        // Sanity: just confirm we get a number; what matters is the next
        // assertion shows it differs after detection.
        Assert.True(beforeDisposition > -100 && beforeDisposition < 100);
    }

    [Fact]
    public void EffectiveDisposition_AppliesHybridBias_AfterDetection()
    {
        var pc  = MakeHybrid(passing: true);
        var npc = MakeNpcWithBiasProfile("CERVID_CAUTIOUS");
        var rep = new PlayerReputation();

        int before = EffectiveDisposition.For(npc, pc, rep, _content);
        // Mark NPC as having detected.
        pc.Hybrid!.NpcsWhoKnow.Add(npc.Id);
        int after = EffectiveDisposition.For(npc, pc, rep, _content);

        // CERVID_CAUTIOUS has a *negative* hybrid_bias per bias_profiles.json,
        // so the disposition should drop after detection.
        Assert.True(after < before,
            $"expected disposition to drop after hybrid detection: before={before}, after={after}");
    }

    [Fact]
    public void EffectiveDisposition_ProgressiveProfile_PositiveHybridBias()
    {
        var pc  = MakeHybrid(passing: true);
        var npc = MakeNpcWithBiasProfile("HYBRID_SURVIVOR");
        var rep = new PlayerReputation();

        int before = EffectiveDisposition.For(npc, pc, rep, _content);
        pc.Hybrid!.NpcsWhoKnow.Add(npc.Id);
        int after = EffectiveDisposition.For(npc, pc, rep, _content);

        // HYBRID_SURVIVOR has positive hybrid_bias — disposition rises.
        Assert.True(after > before,
            $"expected disposition to rise for hybrid-friendly profile: before={before}, after={after}");
    }

    // ── Save round-trip mask tier ────────────────────────────────────────

    [Fact]
    public void Hybrid_MaskTier_RoundTripsThroughSave()
    {
        var pc = MakeHybrid(passing: true);
        pc.Hybrid!.ActiveMaskTier = ScentMaskTier.DeepCover;

        var snap = Theriapolis.Core.Persistence.CharacterCodec.Capture(pc);
        Assert.NotNull(snap.Hybrid);
        Assert.Equal((byte)ScentMaskTier.DeepCover, snap.Hybrid!.ActiveMaskTier);

        var restored = Theriapolis.Core.Persistence.CharacterCodec.Restore(snap, _content);
        Assert.Equal(ScentMaskTier.DeepCover, restored.Hybrid!.ActiveMaskTier);
    }

    [Fact]
    public void Hybrid_MaskTier_RoundTripsThroughBinarySaveCodec()
    {
        var pc = MakeHybrid(passing: true);
        pc.Hybrid!.ActiveMaskTier = ScentMaskTier.Military;

        var header = new Theriapolis.Core.Persistence.SaveHeader
        {
            Version       = Theriapolis.Core.C.SAVE_SCHEMA_VERSION,
            WorldSeedHex  = "0xFEED",
        };
        var body = new Theriapolis.Core.Persistence.SaveBody
        {
            PlayerCharacter = Theriapolis.Core.Persistence.CharacterCodec.Capture(pc),
        };
        body.Player.Id = 1;
        body.Player.Name = "Hybrid";

        var bytes = Theriapolis.Core.Persistence.SaveCodec.Serialize(header, body);
        var (_, body2) = Theriapolis.Core.Persistence.SaveCodec.Deserialize(bytes);

        Assert.NotNull(body2.PlayerCharacter?.Hybrid);
        Assert.Equal((byte)ScentMaskTier.Military, body2.PlayerCharacter!.Hybrid!.ActiveMaskTier);
    }

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

    private Theriapolis.Core.Rules.Character.Character MakePurebred()
    {
        var b = new CharacterBuilder
        {
            Clade        = _content.Clades["canidae"],
            Species      = _content.Species["wolf"],
            ClassDef     = _content.Classes["fangsworn"],
            Background   = _content.Backgrounds["pack_raised"],
            BaseAbilities = new AbilityScores(15, 14, 13, 12, 10, 8),
        };
        AutoPickSkills(b);
        return b.Build(_content.Items);
    }

    private Theriapolis.Core.Rules.Character.Character MakeHybrid(bool passing)
    {
        var b = new CharacterBuilder
        {
            ClassDef     = _content.Classes["fangsworn"],
            Background   = _content.Backgrounds["pack_raised"],
            BaseAbilities = new AbilityScores(15, 14, 13, 12, 10, 12),
            IsHybridOrigin = true,
            HybridSireClade   = _content.Clades["canidae"],
            HybridSireSpecies = _content.Species["wolf"],
            HybridDamClade    = _content.Clades["leporidae"],
            HybridDamSpecies  = _content.Species["rabbit"],
            HybridDominantParent = ParentLineage.Sire,
        };
        AutoPickSkills(b);
        bool ok = b.TryBuildHybrid(_content.Items, out var c, out string err);
        Assert.True(ok, err);
        c!.Hybrid!.PassingActive = passing;
        return c;
    }

    private void AutoPickSkills(CharacterBuilder b)
    {
        int n = b.ClassDef!.SkillsChoose;
        foreach (var raw in b.ClassDef.SkillOptions)
        {
            if (b.ChosenClassSkills.Count >= n) break;
            try { b.ChosenClassSkills.Add(SkillIdExtensions.FromJson(raw)); } catch { }
        }
    }

    private NpcActor MakeCanidNpc()
    {
        var resident = new ResidentTemplateDef
        {
            Id      = "test_canid",
            Name    = "Test Canid",
            Clade   = "canidae",
            Species = "wolf",
        };
        return new NpcActor(resident) { Id = 42 };
    }

    private NpcActor MakeCanidNpcWithRole(string roleTag)
    {
        var resident = new ResidentTemplateDef
        {
            Id      = "test_canid",
            Name    = "Test Canid",
            Clade   = "canidae",
            Species = "wolf",
            RoleTag = roleTag,
        };
        return new NpcActor(resident) { Id = 42 };
    }

    private NpcActor MakeNonCanidNpc()
    {
        var resident = new ResidentTemplateDef
        {
            Id      = "test_cervid",
            Name    = "Test Cervid",
            Clade   = "cervidae",
            Species = "deer",
        };
        return new NpcActor(resident) { Id = 99 };
    }

    private NpcActor MakeNpcWithBiasProfile(string biasProfileId)
    {
        var resident = new ResidentTemplateDef
        {
            Id          = "test_npc",
            Name        = "Test NPC",
            Clade       = "cervidae",
            Species     = "deer",
            BiasProfile = biasProfileId,
        };
        return new NpcActor(resident) { Id = 1 };
    }
}
