using System.Net.Http.Json;
using Microsoft.Extensions.DependencyInjection;
using NeonSprawl.Server.Game.Interaction;
using NeonSprawl.Server.Game.Mastery;
using NeonSprawl.Server.Game.PositionState;
using NeonSprawl.Server.Game.Skills;
using NeonSprawl.Server.Game.World;
using Xunit;

namespace NeonSprawl.Server.Tests.Game.Mastery;

public sealed class PerkUnlockEngineTests
{
    private const string DevPlayer = "dev-local-1";
    private const string UnknownPlayer = "no-such-player-neo-47";
    private const string SalvageSkill = "salvage";
    private const string ScrapBranch = "scrap_efficiency";
    private const string BulkBranch = "bulk_haul";
    private const string ScrapPerk = "salvage_scrap_efficiency_1";
    private const string BulkPerk = "salvage_bulk_haul_1";

    private static (PerkUnlockEngine Engine, IPlayerSkillProgressionStore XpStore, IPlayerPerkStateStore PerkStore) CreateEngine(
        InMemoryWebApplicationFactory factory) =>
        (
            factory.Services.GetRequiredService<PerkUnlockEngine>(),
            factory.Services.GetRequiredService<IPlayerSkillProgressionStore>(),
            factory.Services.GetRequiredService<IPlayerPerkStateStore>());

    [Fact]
    public async Task TrySelectBranch_AtSalvageLevel2_ShouldRecordPick_WithoutUnlockingTier2Perks()
    {
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        _ = factory.CreateClient();
        var (engine, xpStore, perkStore) = CreateEngine(factory);
        Assert.True(xpStore.TryApplyXpDelta(DevPlayer, SalvageSkill, 100, out _, out _));

        // Act
        var outcome = engine.TrySelectBranch(DevPlayer, SalvageSkill, tierIndex: 1, ScrapBranch);

        // Assert
        Assert.True(outcome.Success);
        Assert.Null(outcome.ReasonCode);
        Assert.Empty(outcome.Events);
        var snap = perkStore.GetSnapshot(DevPlayer);
        Assert.Equal(ScrapBranch, snap.BranchPicksBySkillId[SalvageSkill][1]);
        Assert.DoesNotContain(ScrapPerk, snap.UnlockedPerkIds);
        Assert.DoesNotContain(BulkPerk, snap.UnlockedPerkIds);
    }

    [Fact]
    public async Task TrySelectBranch_AtLevel4AfterXpGate_ShouldUnlockTier2PerkWithoutExtraGrant()
    {
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        _ = factory.CreateClient();
        var (engine, xpStore, perkStore) = CreateEngine(factory);
        Assert.True(xpStore.TryApplyXpDelta(DevPlayer, SalvageSkill, 450, out _, out _));

        // Act
        var outcome = engine.TrySelectBranch(DevPlayer, SalvageSkill, tierIndex: 1, ScrapBranch);

        // Assert
        Assert.True(outcome.Success);
        var perk = Assert.Single(outcome.Events);
        Assert.Equal(ScrapPerk, perk.PerkId);
        Assert.Equal(PerkUnlockSource.LevelUp, perk.Source);
        var snap = perkStore.GetSnapshot(DevPlayer);
        Assert.Contains(ScrapPerk, snap.UnlockedPerkIds);
        Assert.DoesNotContain(BulkPerk, snap.UnlockedPerkIds);
    }

    [Fact]
    public async Task TrySelectBranch_WhenLevelTooLow_ShouldDenyWithLevelTooLow()
    {
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        _ = factory.CreateClient();
        var (engine, _, _) = CreateEngine(factory);

        // Act
        var outcome = engine.TrySelectBranch(DevPlayer, SalvageSkill, tierIndex: 1, ScrapBranch);

        // Assert
        Assert.False(outcome.Success);
        Assert.Equal(PerkUnlockReasonCodes.LevelTooLow, outcome.ReasonCode);
    }

    [Fact]
    public async Task TrySelectBranch_WhenUnknownTrack_ShouldDenyWithUnknownTrack()
    {
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        _ = factory.CreateClient();
        var (engine, xpStore, _) = CreateEngine(factory);
        Assert.True(xpStore.TryApplyXpDelta(DevPlayer, SalvageSkill, 100, out _, out _));

        // Act
        var outcome = engine.TrySelectBranch(DevPlayer, "refine", tierIndex: 1, ScrapBranch);

        // Assert
        Assert.False(outcome.Success);
        Assert.Equal(PerkUnlockReasonCodes.UnknownTrack, outcome.ReasonCode);
    }

    [Fact]
    public async Task TrySelectBranch_WhenUnknownTier_ShouldDenyWithUnknownTier()
    {
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        _ = factory.CreateClient();
        var (engine, xpStore, _) = CreateEngine(factory);
        Assert.True(xpStore.TryApplyXpDelta(DevPlayer, SalvageSkill, 100, out _, out _));

        // Act
        var outcome = engine.TrySelectBranch(DevPlayer, SalvageSkill, tierIndex: 99, ScrapBranch);

        // Assert
        Assert.False(outcome.Success);
        Assert.Equal(PerkUnlockReasonCodes.UnknownTier, outcome.ReasonCode);
    }

    [Fact]
    public async Task TrySelectBranch_WhenPlayerNotInStore_ShouldDenyWithPlayerNotFound()
    {
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        _ = factory.CreateClient();
        var (engine, _, _) = CreateEngine(factory);

        // Act
        var outcome = engine.TrySelectBranch(UnknownPlayer, SalvageSkill, tierIndex: 1, ScrapBranch);

        // Assert
        Assert.False(outcome.Success);
        Assert.Equal(PerkUnlockReasonCodes.PlayerNotFound, outcome.ReasonCode);
    }

    [Fact]
    public async Task TrySelectBranch_WhenBranchAlreadyChosen_ShouldDenyWithBranchAlreadyChosen()
    {
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        _ = factory.CreateClient();
        var (engine, xpStore, _) = CreateEngine(factory);
        Assert.True(xpStore.TryApplyXpDelta(DevPlayer, SalvageSkill, 100, out _, out _));
        Assert.True(engine.TrySelectBranch(DevPlayer, SalvageSkill, 1, ScrapBranch).Success);

        // Act
        var outcome = engine.TrySelectBranch(DevPlayer, SalvageSkill, tierIndex: 1, BulkBranch);

        // Assert
        Assert.False(outcome.Success);
        Assert.Equal(PerkUnlockReasonCodes.BranchAlreadyChosen, outcome.ReasonCode);
    }

    [Fact]
    public async Task TrySelectBranch_WhenBranchUnknown_ShouldDenyWithUnknownBranch()
    {
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        _ = factory.CreateClient();
        var (engine, xpStore, _) = CreateEngine(factory);
        Assert.True(xpStore.TryApplyXpDelta(DevPlayer, SalvageSkill, 100, out _, out _));

        // Act
        var outcome = engine.TrySelectBranch(DevPlayer, SalvageSkill, tierIndex: 1, "not_a_branch");

        // Assert
        Assert.False(outcome.Success);
        Assert.Equal(PerkUnlockReasonCodes.UnknownBranch, outcome.ReasonCode);
    }

    [Fact]
    public async Task TrySelectBranch_OnPathAutoTier2_ShouldDenyWithTierBranchNotSelectable()
    {
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        _ = factory.CreateClient();
        var (engine, xpStore, _) = CreateEngine(factory);
        Assert.True(xpStore.TryApplyXpDelta(DevPlayer, SalvageSkill, 450, out _, out _));
        Assert.True(engine.TrySelectBranch(DevPlayer, SalvageSkill, 1, ScrapBranch).Success);

        // Act
        var outcome = engine.TrySelectBranch(DevPlayer, SalvageSkill, tierIndex: 2, ScrapBranch);

        // Assert
        Assert.False(outcome.Success);
        Assert.Equal(PerkUnlockReasonCodes.TierBranchNotSelectable, outcome.ReasonCode);
    }

    [Fact]
    public async Task ReevaluateAfterLevelUp_AtLevel4WithTier1Pick_ShouldUnlockScrapPerkOnly()
    {
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        _ = factory.CreateClient();
        var (engine, xpStore, perkStore) = CreateEngine(factory);
        Assert.True(xpStore.TryApplyXpDelta(DevPlayer, SalvageSkill, 450, out _, out _));
        Assert.True(engine.TrySelectBranch(DevPlayer, SalvageSkill, 1, ScrapBranch).Success);
        var snapAfterPick = perkStore.GetSnapshot(DevPlayer);
        Assert.Contains(ScrapPerk, snapAfterPick.UnlockedPerkIds);

        // Act
        var outcome = engine.ReevaluateAfterLevelUp(DevPlayer, SalvageSkill, newLevel: 4);

        // Assert
        Assert.Empty(outcome.Events);
    }

    [Fact]
    public async Task ReevaluateAfterLevelUp_WhenCalledTwice_ShouldBeIdempotent()
    {
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        _ = factory.CreateClient();
        var (engine, xpStore, perkStore) = CreateEngine(factory);
        Assert.True(xpStore.TryApplyXpDelta(DevPlayer, SalvageSkill, 450, out _, out _));
        Assert.True(engine.TrySelectBranch(DevPlayer, SalvageSkill, 1, ScrapBranch).Success);
        _ = engine.ReevaluateAfterLevelUp(DevPlayer, SalvageSkill, newLevel: 4);

        // Act
        var second = engine.ReevaluateAfterLevelUp(DevPlayer, SalvageSkill, newLevel: 4);

        // Assert
        Assert.Empty(second.Events);
        var snap = perkStore.GetSnapshot(DevPlayer);
        Assert.Single(snap.UnlockedPerkIds);
    }

    [Fact]
    public async Task PostSkillProgressionGrant_AfterTier1PickAndLevel4Xp_ShouldUnlockPerkViaHook()
    {
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();
        var (engine, xpStore, perkStore) = CreateEngine(factory);
        Assert.True(xpStore.TryApplyXpDelta(DevPlayer, SalvageSkill, 100, out _, out _));
        Assert.True(engine.TrySelectBranch(DevPlayer, SalvageSkill, 1, ScrapBranch).Success);
        var grant = new SkillProgressionGrantRequest
        {
            SchemaVersion = SkillProgressionGrantRequest.CurrentSchemaVersion,
            SkillId = SalvageSkill,
            Amount = 350,
            SourceKind = "activity",
        };

        // Act
        var response = await client.PostAsJsonAsync($"/game/players/{DevPlayer}/skill-progression", grant);

        // Assert
        Assert.True(response.IsSuccessStatusCode);
        var snap = perkStore.GetSnapshot(DevPlayer);
        Assert.Contains(ScrapPerk, snap.UnlockedPerkIds);
    }

    [Fact]
    public async Task PostInteract_ResourceNode_AfterTier1PickNearLevel4_ShouldUnlockPerkViaGatherHook()
    {
        // Arrange
        await using var factory = new InMemoryWebApplicationFactory();
        var client = factory.CreateClient();
        var (engine, xpStore, perkStore) = CreateEngine(factory);
        Assert.True(xpStore.TryApplyXpDelta(DevPlayer, SalvageSkill, 440, out _, out _));
        Assert.True(engine.TrySelectBranch(DevPlayer, SalvageSkill, 1, ScrapBranch).Success);
        var move = new MoveCommandRequest
        {
            SchemaVersion = MoveCommandRequest.CurrentSchemaVersion,
            Target = new PositionVector { X = 10, Y = 0.9, Z = -6 },
        };
        await client.PostAsJsonAsync($"/game/players/{DevPlayer}/move", move);

        // Act
        var interact = await client.PostAsJsonAsync(
            $"/game/players/{DevPlayer}/interact",
            new InteractionRequest
            {
                SchemaVersion = InteractionRequest.CurrentSchemaVersion,
                InteractableId = PrototypeInteractableRegistry.PrototypeResourceNodeAlphaId,
            });

        // Assert
        Assert.True(interact.IsSuccessStatusCode);
        var snap = perkStore.GetSnapshot(DevPlayer);
        Assert.Contains(ScrapPerk, snap.UnlockedPerkIds);
    }
}
