namespace RpgRoller.Tests;

public sealed class ServiceSharedHelperTests
{
    [Fact]
    public void GameStateStore_TracksCampaignSlicesAndCharacterVersions()
    {
        var campaignId = Guid.NewGuid();
        var characterId = Guid.NewGuid();
        var store = new GameStateStore();

        store.CampaignsById[campaignId] = new()
        {
            Id = campaignId,
            GmUserId = Guid.NewGuid(),
            Name = "Alpha",
            Ruleset = RulesetKind.D6,
            Version = 1
        };
        store.CharactersById[characterId] = new()
        {
            Id = characterId,
            OwnerUserId = Guid.NewGuid(),
            CampaignId = campaignId,
            Name = "Scout"
        };

        store.RebuildCampaignStateLocked();

        var initialState = store.GetOrCreateCampaignStateLocked(campaignId);
        Assert.Equal(1, initialState.CharacterVersions[characterId]);

        store.TouchRosterLocked(campaignId);
        store.TouchCharacterLocked(campaignId, characterId);
        store.TouchLogLocked(campaignId);

        Assert.Equal(4, initialState.TotalVersion);
        Assert.Equal(2, initialState.RosterVersion);
        Assert.Equal(2, initialState.LogVersion);
        Assert.Equal(2, initialState.CharacterVersions[characterId]);

        store.RemoveCharacterStateLocked(campaignId, characterId);
        Assert.Empty(initialState.CharacterVersions);

        store.AddCharacterStateLocked(campaignId, characterId);
        Assert.Equal(1, initialState.CharacterVersions[characterId]);

        store.TouchRosterLocked(null);
        store.TouchCharacterLocked(Guid.NewGuid(), Guid.NewGuid());
        store.TouchLogLocked(Guid.NewGuid());
        store.RemoveCharacterStateLocked(Guid.NewGuid(), Guid.NewGuid());
    }

    [Fact]
    public void GameAuthorization_CoversCampaignAndRollVisibility()
    {
        var adminId = Guid.NewGuid();
        var gmId = Guid.NewGuid();
        var playerId = Guid.NewGuid();
        var outsiderId = Guid.NewGuid();
        var campaignId = Guid.NewGuid();
        var store = new GameStateStore();

        store.UsersById[adminId] = new()
        {
            Id = adminId,
            Username = "admin",
            UsernameNormalized = "ADMIN",
            PasswordHash = "hash",
            DisplayName = "Admin",
            Roles = UserRoles.Admin
        };
        store.UsersById[gmId] = new()
        {
            Id = gmId,
            Username = "gm",
            UsernameNormalized = "GM",
            PasswordHash = "hash",
            DisplayName = "GM",
            Roles = string.Empty
        };
        store.UsersById[playerId] = new()
        {
            Id = playerId,
            Username = "player",
            UsernameNormalized = "PLAYER",
            PasswordHash = "hash",
            DisplayName = "Player",
            Roles = string.Empty
        };
        store.UsersById[outsiderId] = new()
        {
            Id = outsiderId,
            Username = "outsider",
            UsernameNormalized = "OUTSIDER",
            PasswordHash = "hash",
            DisplayName = "Outsider",
            Roles = string.Empty
        };

        var campaign = new Campaign
        {
            Id = campaignId,
            GmUserId = gmId,
            Name = "Alpha",
            Ruleset = RulesetKind.D6,
            Version = 1
        };
        store.CampaignsById[campaignId] = campaign;
        var playerCharacterId = Guid.NewGuid();
        store.CharactersById[playerCharacterId] = new()
        {
            Id = playerCharacterId,
            OwnerUserId = playerId,
            CampaignId = campaignId,
            Name = "Scout"
        };

        var publicEntry = new RollLogEntry
        {
            Id = Guid.NewGuid(),
            CampaignId = campaignId,
            CharacterId = Guid.NewGuid(),
            SkillId = Guid.NewGuid(),
            RollerUserId = playerId,
            Visibility = RollVisibility.Public,
            Result = 12,
            Breakdown = "6+6=12",
            Dice = "[]",
            TimestampUtc = DateTimeOffset.UtcNow
        };
        var privateEntry = new RollLogEntry
        {
            Id = Guid.NewGuid(),
            CampaignId = publicEntry.CampaignId,
            CharacterId = publicEntry.CharacterId,
            SkillId = publicEntry.SkillId,
            RollerUserId = publicEntry.RollerUserId,
            Visibility = RollVisibility.Private,
            Result = publicEntry.Result,
            Breakdown = publicEntry.Breakdown,
            Dice = publicEntry.Dice,
            TimestampUtc = publicEntry.TimestampUtc
        };

        Assert.True(GameAuthorization.HasRole(store.UsersById[adminId], UserRoles.Admin));
        Assert.True(GameAuthorization.CanViewCampaign(store, adminId, campaignId));
        Assert.True(GameAuthorization.CanViewCampaign(store, gmId, campaignId));
        Assert.True(GameAuthorization.CanViewCampaign(store, playerId, campaignId));
        Assert.False(GameAuthorization.CanViewCampaign(store, outsiderId, campaignId));

        Assert.True(GameAuthorization.CanEditCharacter(playerId, store.CharactersById.Values.Single(), campaign));
        Assert.True(GameAuthorization.CanEditCharacter(gmId, store.CharactersById.Values.Single(), campaign));
        Assert.False(GameAuthorization.CanEditCharacter(outsiderId, store.CharactersById.Values.Single(), campaign));

        Assert.True(GameAuthorization.CanViewRoll(store, gmId, campaign, privateEntry));
        Assert.True(GameAuthorization.CanViewRoll(store, playerId, campaign, privateEntry));
        Assert.False(GameAuthorization.CanViewRoll(store, outsiderId, campaign, publicEntry));
        Assert.False(GameAuthorization.CanViewRoll(store, outsiderId, campaign, privateEntry));
    }

    [Fact]
    public void GameContextResolver_HandlesUnauthorizedForbiddenAndSuccessPaths()
    {
        var userId = Guid.NewGuid();
        var otherUserId = Guid.NewGuid();
        var campaignId = Guid.NewGuid();
        var store = new GameStateStore();

        store.UsersById[userId] = new()
        {
            Id = userId,
            Username = "user",
            UsernameNormalized = "USER",
            PasswordHash = "hash",
            DisplayName = "User",
            Roles = string.Empty
        };
        store.UsersById[otherUserId] = new()
        {
            Id = otherUserId,
            Username = "other",
            UsernameNormalized = "OTHER",
            PasswordHash = "hash",
            DisplayName = "Other",
            Roles = string.Empty
        };
        store.SessionsByToken["valid"] = new()
        {
            Token = "valid",
            UserId = userId,
            CreatedAtUtc = DateTimeOffset.UtcNow
        };
        store.CampaignsById[campaignId] = new()
        {
            Id = campaignId,
            GmUserId = otherUserId,
            Name = "Alpha",
            Ruleset = RulesetKind.D6,
            Version = 1
        };
        var participant = new Character
        {
            Id = Guid.NewGuid(),
            OwnerUserId = userId,
            CampaignId = campaignId,
            Name = "Scout"
        };
        store.CharactersById[participant.Id] = participant;

        Assert.Null(GameContextResolver.ResolveUserLocked(store, string.Empty));
        Assert.Null(GameContextResolver.ResolveUserLocked(store, "missing"));
        Assert.Equal(userId, GameContextResolver.ResolveUserLocked(store, "valid")!.Id);

        Assert.Equal("unauthorized", GameContextResolver.ResolveCampaignContextLocked(store, string.Empty, campaignId).Error!.Code);
        Assert.Equal("campaign_not_found", GameContextResolver.ResolveCampaignContextLocked(store, "valid", Guid.NewGuid()).Error!.Code);

        var forbiddenStore = new GameStateStore();
        forbiddenStore.UsersById[userId] = store.UsersById[userId];
        forbiddenStore.SessionsByToken["valid"] = store.SessionsByToken["valid"];
        forbiddenStore.CampaignsById[campaignId] = store.CampaignsById[campaignId];
        Assert.Equal("forbidden", GameContextResolver.ResolveCampaignContextLocked(forbiddenStore, "valid", campaignId).Error!.Code);

        var context = ServiceTestSupport.GetValue(GameContextResolver.ResolveCampaignContextLocked(store, "valid", campaignId));
        Assert.Equal(userId, context.User.Id);
        Assert.Equal(campaignId, context.Campaign.Id);

        var orphan = new Character
        {
            Id = Guid.NewGuid(),
            OwnerUserId = userId,
            CampaignId = null,
            Name = "Orphan"
        };
        Assert.False(GameContextResolver.TryResolveCharacterCampaignLocked(store, orphan, out _, out var orphanError));
        Assert.Equal("character_not_in_campaign", orphanError!.Code);

        var missingCampaignCharacter = new Character
        {
            Id = Guid.NewGuid(),
            OwnerUserId = orphan.OwnerUserId,
            CampaignId = Guid.NewGuid(),
            Name = orphan.Name
        };
        Assert.False(GameContextResolver.TryResolveCharacterCampaignLocked(store, missingCampaignCharacter, out _, out var missingCampaignError));
        Assert.Equal("character_not_in_campaign", missingCampaignError!.Code);

        Assert.True(GameContextResolver.TryResolveCharacterCampaignLocked(store, participant, out var resolvedCampaign, out var noError));
        Assert.Equal(campaignId, resolvedCampaign.Id);
        Assert.Null(noError);
    }

    [Fact]
    public void GameDtoMapper_MapsServiceContractsAndFallbacks()
    {
        var gmId = Guid.NewGuid();
        var ownerId = Guid.NewGuid();
        var blankOwnerId = Guid.NewGuid();
        var campaignId = Guid.NewGuid();
        var characterId = Guid.NewGuid();
        var skillGroupId = Guid.NewGuid();
        var skillId = Guid.NewGuid();
        var rollId = Guid.NewGuid();
        var store = new GameStateStore();

        store.UsersById[gmId] = new()
        {
            Id = gmId,
            Username = "gm",
            UsernameNormalized = "GM",
            PasswordHash = "hash",
            DisplayName = "GM",
            Roles = UserRoles.Admin
        };
        store.UsersById[ownerId] = new()
        {
            Id = ownerId,
            Username = "owner",
            UsernameNormalized = "OWNER",
            PasswordHash = "hash",
            DisplayName = "Owner",
            Roles = string.Empty
        };
        store.UsersById[blankOwnerId] = new()
        {
            Id = blankOwnerId,
            Username = "blank",
            UsernameNormalized = "BLANK",
            PasswordHash = "hash",
            DisplayName = "",
            Roles = string.Empty
        };
        store.CampaignsById[campaignId] = new()
        {
            Id = campaignId,
            GmUserId = gmId,
            Name = "Alpha",
            Ruleset = RulesetKind.Rolemaster,
            Version = 1
        };
        store.CharactersById[characterId] = new()
        {
            Id = characterId,
            OwnerUserId = ownerId,
            CampaignId = campaignId,
            Name = "Scout"
        };
        store.SkillGroupsById[skillGroupId] = new()
        {
            Id = skillGroupId,
            CharacterId = characterId,
            Name = "Awareness",
            DiceRollDefinition = "d100!+15",
            WildDice = 0,
            AllowFumble = false,
            FumbleRange = 5
        };
        store.SkillsById[skillId] = new()
        {
            Id = skillId,
            CharacterId = characterId,
            SkillGroupId = skillGroupId,
            Name = "Perception",
            DiceRollDefinition = "d100!+25",
            WildDice = 0,
            AllowFumble = false,
            FumbleRange = 3,
            RolemasterAutoRetry = true
        };
        store.RebuildCampaignStateLocked();
        store.TouchRosterLocked(campaignId);
        store.TouchCharacterLocked(campaignId, characterId);
        store.TouchLogLocked(campaignId);

        var dice = new[] { new RollDieResult(66, false, false, false, false, false, 1, RollDieKinds.RolemasterStandard, 66) };
        var logEntry = new RollLogEntry
        {
            Id = rollId,
            CampaignId = campaignId,
            CharacterId = characterId,
            SkillId = skillId,
            RollerUserId = ownerId,
            Visibility = RollVisibility.Private,
            Result = 91,
            Breakdown = "66+25=91",
            Dice = "[]",
            TimestampUtc = DateTimeOffset.UtcNow
        };

        var userSummary = GameDtoMapper.ToUserSummary(store.UsersById[gmId]);
        var adminSummary = GameDtoMapper.ToAdminUserSummary(store.UsersById[gmId]);
        var campaignOption = GameDtoMapper.ToCampaignOption(store.CampaignsById[campaignId]);
        var campaignSummary = GameDtoMapper.ToCampaignSummary(store, store.CampaignsById[campaignId]);
        var campaignRoster = GameDtoMapper.ToCampaignRoster(store, store.CampaignsById[campaignId]);
        var characterSummary = GameDtoMapper.ToCharacterSummary(store, store.CharactersById[characterId]);
        var sheet = GameDtoMapper.ToCharacterSheet(store, characterId);
        var groupSummary = GameDtoMapper.ToSkillGroupSummary(store.SkillGroupsById[skillGroupId]);
        var skillSummary = GameDtoMapper.ToSkillSummary(store.SkillsById[skillId]);
        var rollResult = GameDtoMapper.ToRollResult(logEntry, dice);
        var logDto = GameDtoMapper.ToCampaignLogEntry(logEntry, "Scout", "Perception", "Owner", dice);
        var logListDto = GameDtoMapper.ToCampaignLogListEntry(logEntry, "Scout", "Perception", "You", "Private (you)", "private-self", "66 | rolemaster", ["r66"]);
        var detail = GameDtoMapper.ToCampaignRollDetail(logEntry, dice);
        var snapshot = GameDtoMapper.ToCampaignStateSnapshot(store, campaignId);

        Assert.Contains(UserRoles.Admin, userSummary.Roles);
        Assert.Contains(UserRoles.Admin, adminSummary.Roles);
        Assert.Equal("Alpha", campaignOption.Name);
        Assert.Equal("rolemaster", campaignSummary.RulesetId);
        Assert.Single(campaignRoster.Characters);
        Assert.Equal("Owner", characterSummary.OwnerDisplayName);
        Assert.Single(sheet.SkillGroups);
        Assert.Single(sheet.Skills);
        Assert.Equal(5, groupSummary.FumbleRange);
        Assert.Equal(3, skillSummary.FumbleRange);
        Assert.True(skillSummary.RolemasterAutoRetry);
        Assert.Equal("private", rollResult.Visibility);
        Assert.Equal("Owner", logDto.RollerDisplayName);
        Assert.Equal("private-self", logListDto.VisibilityStyle);
        Assert.Equal(logEntry.Breakdown, detail.Breakdown);
        Assert.Equal(campaignId, snapshot.CampaignId);
        Assert.Single(snapshot.CharacterVersions);
        Assert.Equal("fallback", GameDtoMapper.ResolveOwnerDisplayName(store, blankOwnerId, "fallback"));
        Assert.Equal("fallback", GameDtoMapper.ResolveOwnerDisplayName(store, Guid.NewGuid(), "fallback"));
    }
}